mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 00:49:41 +02:00
fix(web): usable past-conversation list, and search that finds past sessions
Two home-screen reports from @jordan8037310, both about history that is present but unreachable. #260 — "Resume Conversation" rendered 4 rows, then a button that appended every remaining row into a `max-height: 240px` box, so 35 conversations landed in a four-row scroll well with no ordering or filtering. Rendering now goes through `_renderHistoryList()` over a cached corpus: 10 rows to start, Show more/Show less that grows and shrinks the box (the height cap is class-driven, `.history-list.expanded`), plus a filter box (name, folder, #case label, prompts), a sort control (recent / name / folder, pinned rows still first) and a shown-of-total count. A filter implies expansion, so every match is visible, and the whole header hides as one unit while a federated search is active. The A-Z sort keys off the same string the row renders, since most rows are transcript-backed and carry no session name at all. #261 — the search box could not match a past project by folder name: `harvestSources()` built its session corpus from the live in-memory map, while past sessions come from `/api/sessions/unified` (lifecycle log + transcript scan). Folding that scan into the request path would have cost the search its no-filesystem-reads property, so the corpus arrives via a bounded snapshot instead: `session-history-index.ts` is published as a side effect of `/api/sessions/unified` (the home screen fetches it on open, which is the same screen the search box lives on) and rebuilt fire-and-forget, single-flight and TTL-guarded when a search finds it stale. A result for a closed session now resumes the conversation rather than selecting a tab that no longer exists, and is badged RESUME. The snapshot is stored unscoped with a per-row owner and re-filtered through canAccessOwned() on read, so multi-user sees exactly what /api/sessions/unified exposes: own sessions only, host-wide transcript history admin-only. Live rows are harvested first and win the dedupe. Verified end-to-end against a real instance with 60 past sessions: cold process answers its first search without history and its second with it; folder-name queries return resume targets; clicking one posts the right resumeSessionId + workingDir. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+23
-4
@@ -36,13 +36,21 @@ export const SEARCH_PER_GROUP_CAP = 25;
|
||||
/** Maximum characters in a result snippet. */
|
||||
export const SEARCH_SNIPPET_MAX = 200;
|
||||
|
||||
/** A live-session row harvested for the session/case source. */
|
||||
/** A session row harvested for the session/case source (live or past). */
|
||||
export interface SessionSearchInput {
|
||||
sessionId: string;
|
||||
sessionName: string;
|
||||
workingDir: string;
|
||||
/** Recency timestamp (e.g. lastActivityAt or createdAt). */
|
||||
timestamp: number;
|
||||
/**
|
||||
* True for a session that is no longer running (issue #261 — past sessions come
|
||||
* from the history index, not the live map). Such a result resumes the
|
||||
* conversation instead of switching to a tab that no longer exists.
|
||||
*/
|
||||
history?: boolean;
|
||||
/** Claude conversation UUID to resume, when it differs from the Codeman id. */
|
||||
claudeSessionId?: string;
|
||||
}
|
||||
|
||||
/** A run-summary timeline event harvested for the event source. */
|
||||
@@ -121,14 +129,25 @@ export function searchSources(query: string, sources: SearchSources): SearchResp
|
||||
const sessionRows: SearchResult[] = [];
|
||||
for (const s of sources.sessions) {
|
||||
if (contains(s.sessionName) || contains(s.workingDir) || contains(s.sessionId)) {
|
||||
const label = s.sessionName || s.workingDir.split('/').pop() || s.sessionId;
|
||||
sessionRows.push({
|
||||
type: 'session',
|
||||
sessionId: s.sessionId,
|
||||
sessionName: s.sessionName,
|
||||
sessionName: label,
|
||||
timestamp: s.timestamp,
|
||||
snippet: truncate(s.workingDir ? `${s.sessionName} — ${s.workingDir}` : s.sessionName),
|
||||
snippet: truncate(s.workingDir ? `${label} — ${s.workingDir}` : label),
|
||||
exactMatch: isExact(s.sessionName),
|
||||
jumpTo: { kind: 'session', sessionId: s.sessionId },
|
||||
// A resume needs a directory to run in, so a history row without one
|
||||
// stays a plain session target rather than an action that cannot work.
|
||||
jumpTo:
|
||||
s.history && s.workingDir
|
||||
? {
|
||||
kind: 'resume-session',
|
||||
sessionId: s.sessionId,
|
||||
claudeSessionId: s.claudeSessionId,
|
||||
workingDir: s.workingDir,
|
||||
}
|
||||
: { kind: 'session', sessionId: s.sessionId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user