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:
Codeman maintainer
2026-08-10 03:02:11 +02:00
parent c942bb5dfb
commit 5d42f64393
15 changed files with 1206 additions and 54 deletions
+84 -2
View File
@@ -3666,6 +3666,13 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
color: #95e6b3;
}
/* Past session (issue #261): activating the card resumes the conversation
rather than switching to a tab, so the badge says so. */
.search-badge-past {
background: rgba(245, 158, 11, 0.18);
color: #f0c073;
}
.search-result-name {
flex: 1;
min-width: 0;
@@ -3715,24 +3722,99 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
.search-select {
max-width: 7rem;
}
/* Tablet/narrow: let the controls drop under the title instead of squeezing it. */
.history-header {
flex-wrap: wrap;
}
.history-controls {
width: 100%;
margin-left: 0;
}
.history-filter {
flex: 1;
width: auto;
}
}
/* Title row for the past-session list: label + count on the left, filter and
sort on the right (issue #260 — 35 conversations in a 4-row box). */
.history-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.history-title {
font-size: 0.85rem;
color: var(--text-dim);
margin-bottom: 0.5rem;
font-weight: 500;
text-align: left;
}
.history-count {
font-size: 0.68rem;
color: var(--text-dim);
background: rgba(255, 255, 255, 0.05);
border-radius: 999px;
padding: 0.1rem 0.45rem;
white-space: nowrap;
}
.history-controls {
display: flex;
align-items: center;
gap: 0.35rem;
margin-left: auto;
}
.history-filter {
width: 8.5rem;
box-sizing: border-box;
padding: 0.28rem 0.5rem;
font-size: 0.68rem;
color: var(--text);
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
outline: none;
transition: border-color var(--transition-smooth), background var(--transition-smooth);
}
.history-filter:focus {
border-color: rgba(59, 130, 246, 0.5);
background: rgba(255, 255, 255, 0.06);
}
.history-filter::placeholder {
color: var(--text-dim);
}
.history-sort {
max-width: 7.5rem;
}
.history-empty {
padding: 0.75rem 0.5rem;
font-size: 0.75rem;
color: var(--text-dim);
text-align: center;
}
/* Collapsed height fits the initial page of rows; expanding the LIST has to
expand the BOX too, or "Show more" just deepens a scroll well. */
.history-list {
display: flex;
flex-direction: column;
gap: 0.35rem;
max-height: 240px;
max-height: min(42vh, 360px);
overflow-y: auto;
}
.history-list.expanded {
max-height: min(64vh, 660px);
}
.history-item {
display: flex;
flex-direction: column;