Long chats stay fast; search in a chat; media overview
Performance (measured with 5,900 messages: open 9 s -> 0.7 s, typing 440 ms -> 25 ms)
- GET /sessions/{id}?limit=N loads the end of a chat (from the last summary
on, at most N); /sessions/{id}/messages?before|after|around loads more
(scrolling up stops at a summary unless asked). Without parameters the
full history as before (terminal client).
- Chat view: memoised message list (typing, streaming and status changes no
longer re-render the history), finished turns fold into "N steps · M files"
with the answer visible, tool card bodies and summaries are built only when
opened, scroll-to-bottom only when content changes.
Search: /sessions/{id}/search over the whole chat; a hit loads its part
(around) and marks the message, "back to the latest messages" returns.
Media: /sessions/{id}/media lists images, videos and audio of a chat and its
subagents (generated images, tool output, files written, media in answers,
attachments); ⋯ -> Media shows them with filters, a large view and a jump to
the message.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6e9eda28b5
commit
3b0764450a
14 files changed
+1278
-99
No files matched your search
+73
-3
@@ -81,6 +81,9 @@ pub fn router(app: AppState) -> Router {
|
||||
.route("/sessions/{id}/tabs", get(session_tabs).put(put_file_tabs))
|
||||
.route("/sessions/{id}/export", get(export_session))
|
||||
.route("/blobs/{hash}", get(get_blob))
|
||||
.route("/sessions/{id}/messages", get(session_messages))
|
||||
.route("/sessions/{id}/search", get(search_in_session))
|
||||
.route("/sessions/{id}/media", get(session_media))
|
||||
.route("/sessions/{id}", get(get_session).patch(update_session).delete(delete_session))
|
||||
.route("/sessions/{id}/prompt", post(prompt))
|
||||
.route("/sessions/{id}/abort", post(abort))
|
||||
@@ -813,14 +816,81 @@ async fn export_session(Extension(user): Extension<User>, State(app): State<AppS
|
||||
.into_response())
|
||||
}
|
||||
|
||||
async fn get_session(Extension(user): Extension<User>, State(app): State<AppState>, Path(id): Path<String>) -> ApiResult {
|
||||
#[derive(Deserialize)]
|
||||
struct WindowQ {
|
||||
/// Only the end of the chat (from the last summary on, at most this many messages);
|
||||
/// without it, all messages (terminal client).
|
||||
limit: Option<i64>,
|
||||
before: Option<String>,
|
||||
after: Option<String>,
|
||||
around: Option<String>,
|
||||
/// Loading older messages stops at a summary (the history before it on request).
|
||||
#[serde(default)]
|
||||
stop_at_summary: bool,
|
||||
}
|
||||
|
||||
impl WindowQ {
|
||||
fn window(&self) -> Option<crate::store::MessageWindow> {
|
||||
use crate::store::MessageWindow as W;
|
||||
let limit = self.limit.unwrap_or(200).clamp(1, 1000);
|
||||
Some(match (&self.before, &self.after, &self.around) {
|
||||
(Some(id), _, _) => W::Before { id: id.clone(), limit, stop_at_summary: self.stop_at_summary },
|
||||
(_, Some(id), _) => W::After { id: id.clone(), limit },
|
||||
(_, _, Some(id)) => W::Around { id: id.clone(), limit },
|
||||
_ if self.limit.is_some() => W::Tail { limit },
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_session(Extension(user): Extension<User>, State(app): State<AppState>, Path(id): Path<String>, Query(q): Query<WindowQ>) -> ApiResult {
|
||||
let s = readable(&app, &user, &id)?;
|
||||
let messages = app.store.messages(&id)?;
|
||||
let busy = app.is_busy(&id);
|
||||
let can_write = app.access(&user, &id) == Access::Full;
|
||||
let queue = app.store.queue_list(&id)?;
|
||||
let step = app.with_runtime(&id, |r| r.step.clone());
|
||||
Ok(Json(json!({"session": s, "messages": messages, "busy": busy, "can_write": can_write, "queue": queue, "step": step})))
|
||||
let mut out = json!({"session": s, "busy": busy, "can_write": can_write, "queue": queue, "step": step});
|
||||
match q.window() {
|
||||
Some(w) => {
|
||||
let (messages, older, newer) = app.store.messages_window(&id, &w)?;
|
||||
out["messages"] = json!(messages);
|
||||
out["has_older"] = json!(older);
|
||||
out["has_newer"] = json!(newer);
|
||||
}
|
||||
None => out["messages"] = json!(app.store.messages(&id)?),
|
||||
}
|
||||
Ok(Json(out))
|
||||
}
|
||||
|
||||
/// More messages of a chat: before / after / around a message.
|
||||
async fn session_messages(Extension(user): Extension<User>, State(app): State<AppState>, Path(id): Path<String>, Query(q): Query<WindowQ>) -> ApiResult {
|
||||
readable(&app, &user, &id)?;
|
||||
let w = q.window().ok_or_else(|| ApiError(StatusCode::BAD_REQUEST, "before, after, around or limit is required".into()))?;
|
||||
let (messages, older, newer) = app.store.messages_window(&id, &w).map_err(|e| ApiError(StatusCode::NOT_FOUND, format!("{e:#}")))?;
|
||||
Ok(Json(json!({"messages": messages, "has_older": older, "has_newer": newer})))
|
||||
}
|
||||
|
||||
/// Images, videos and audio files shown in a chat (all of it, with subagents).
|
||||
async fn session_media(Extension(user): Extension<User>, State(app): State<AppState>, Path(id): Path<String>) -> ApiResult {
|
||||
readable(&app, &user, &id)?;
|
||||
let a = app.clone();
|
||||
let items = tokio::task::spawn_blocking(move || crate::media::collect(&a, &id)).await.map_err(|e| ApiError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))??;
|
||||
Ok(Json(json!(items)))
|
||||
}
|
||||
|
||||
/// Search within one chat (all of it, not only the loaded part).
|
||||
async fn search_in_session(Extension(user): Extension<User>, State(app): State<AppState>, Path(id): Path<String>, Query(q): Query<SearchQ>) -> ApiResult {
|
||||
readable(&app, &user, &id)?;
|
||||
if q.q.trim().chars().count() < 2 {
|
||||
return Ok(Json(json!([])));
|
||||
}
|
||||
let hits: Vec<Value> = app
|
||||
.store
|
||||
.search_messages(&id, q.q.trim(), 100)?
|
||||
.into_iter()
|
||||
.map(|(mid, role, at, snippet)| json!({"id": mid, "role": role, "created_at": at, "snippet": snippet}))
|
||||
.collect();
|
||||
Ok(Json(json!(hits)))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
Reference in new issue
Block a user