mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 21:49:42 +02:00
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>
92 lines
3.6 KiB
TypeScript
92 lines
3.6 KiB
TypeScript
/**
|
|
* @fileoverview Cross-session federated search types (COD-9).
|
|
*
|
|
* Defines the typed shapes for `GET /api/search` — a bounded, in-memory
|
|
* federated search across three v1 sources: live sessions/cases, run-summary
|
|
* timeline events, and per-session attachment file paths. Terminal-buffer scans
|
|
* and any persisted index are explicitly out of scope for v1.
|
|
*
|
|
* Key exports:
|
|
* - SearchSourceType — the federated source kinds, also the group order key.
|
|
* - SearchResult — a single typed result card (source, session id/name,
|
|
* timestamp, snippet, jump-to action target).
|
|
* - SearchJumpTarget — where the frontend should navigate when a card is opened.
|
|
* - SearchResponseData — grouped result payload returned in the ApiResponse envelope.
|
|
*
|
|
* No I/O, no dependencies on other domain modules. The pure search core lives
|
|
* in `src/search-service.ts`; the route wrapper in `src/web/routes/search-routes.ts`.
|
|
*/
|
|
|
|
/** Federated source kinds. Group/render order is sessions → events → files. */
|
|
export type SearchSourceType = 'session' | 'event' | 'file';
|
|
|
|
/** Where the frontend should jump when a result card is activated. */
|
|
export interface SearchJumpTarget {
|
|
/**
|
|
* Kind of navigation target. `resume-session` marks a session that is no longer
|
|
* running: selecting it has to REPLAY the conversation rather than switch to a
|
|
* tab that does not exist.
|
|
*/
|
|
kind: 'session' | 'run-summary' | 'file-preview' | 'resume-session';
|
|
/** Owning Codeman session id (always present — every result is session-scoped). */
|
|
sessionId: string;
|
|
/**
|
|
* Secondary identifier for the target:
|
|
* - kind 'run-summary': the run-summary event id
|
|
* - kind 'file-preview': the attachment history item id
|
|
* - kind 'session' / 'resume-session': undefined (the sessionId is sufficient)
|
|
*/
|
|
targetId?: string;
|
|
/**
|
|
* Workspace-relative path for file-preview targets. Never an absolute path —
|
|
* server-private external paths are intentionally omitted to avoid leakage.
|
|
*/
|
|
relativePath?: string;
|
|
/**
|
|
* `resume-session` only: the Claude conversation UUID to resume, when it differs
|
|
* from the Codeman session id (resumed and `/clear`-respawned sessions).
|
|
*/
|
|
claudeSessionId?: string;
|
|
/**
|
|
* `resume-session` only: the directory to resume in. Already visible in the
|
|
* result snippet for session rows, so this exposes nothing new.
|
|
*/
|
|
workingDir?: string;
|
|
}
|
|
|
|
/** A single typed search result card. */
|
|
export interface SearchResult {
|
|
/** Which federated source produced this result. */
|
|
type: SearchSourceType;
|
|
/** Owning Codeman session id. */
|
|
sessionId: string;
|
|
/** Display name of the owning session / case. */
|
|
sessionName: string;
|
|
/** Millisecond timestamp used for recency ranking and display. */
|
|
timestamp: number;
|
|
/** Short, already-truncated snippet describing the match. */
|
|
snippet: string;
|
|
/** True when the query matched the primary name/path exactly (case-insensitive). */
|
|
exactMatch: boolean;
|
|
/** Navigation target for the jump-to action. */
|
|
jumpTo: SearchJumpTarget;
|
|
}
|
|
|
|
/** A group of results for one source type, in render order. */
|
|
export interface SearchResultGroup {
|
|
type: SearchSourceType;
|
|
results: SearchResult[];
|
|
}
|
|
|
|
/** Payload returned as `data` inside the standard ApiResponse envelope. */
|
|
export interface SearchResponseData {
|
|
/** The normalized query that was executed. */
|
|
query: string;
|
|
/** Results grouped by source type, ordered sessions → events → files. */
|
|
groups: SearchResultGroup[];
|
|
/** Total number of results across all groups (after caps applied). */
|
|
totalResults: number;
|
|
/** True if any group or the total was capped (more matches existed). */
|
|
truncated: boolean;
|
|
}
|