From 40dfac3534288f19edef6c7fffe9d1b424f44a7b Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 22 Mar 2026 01:57:17 +0100 Subject: [PATCH] feat: improve session history with first prompt + clickable monitor rows (closes #45) --- CLAUDE.md | 13 +++--- src/web/public/panels-ui.js | 5 ++- src/web/public/styles.css | 27 ++++++++++++- src/web/public/terminal-ui.js | 17 ++++++-- src/web/routes/session-routes.ts | 68 ++++++++++++++++++++++++++------ 5 files changed, 106 insertions(+), 24 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0ed1f408..6a043ae4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -102,15 +102,16 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph | **Mux** | `src/mux-interface.ts`, `src/mux-factory.ts`, `src/tmux-manager.ts` | | | **Respawn** | `src/respawn-controller.ts` ★ + 4 helpers (`-adaptive-timing`, `-health`, `-metrics`, `-patterns`) | Read `docs/respawn-state-machine.md` first | | **Ralph** | `src/ralph-tracker.ts` ★, `src/ralph-loop.ts` + 5 helpers (`-config`, `-fix-plan-watcher`, `-plan-tracker`, `-stall-detector`, `-status-parser`) | Read `docs/ralph-wiggum-guide.md` first | +| **Orchestrator** | `src/orchestrator-loop.ts`, `src/orchestrator-planner.ts`, `src/orchestrator-verifier.ts` | Read `docs/orchestrator-loop-architecture.md` first | | **Agents** | `src/subagent-watcher.ts` ★, `src/team-watcher.ts`, `src/bash-tool-parser.ts`, `src/transcript-watcher.ts` | | | **AI** | `src/ai-checker-base.ts`, `src/ai-idle-checker.ts`, `src/ai-plan-checker.ts` | | | **Tasks** | `src/task.ts`, `src/task-queue.ts`, `src/task-tracker.ts` | | | **State** | `src/state-store.ts`, `src/run-summary.ts`, `src/session-lifecycle-log.ts` | | | **Infra** | `src/hooks-config.ts`, `src/push-store.ts`, `src/tunnel-manager.ts`, `src/image-watcher.ts`, `src/file-stream-manager.ts` | | | **Plan** | `src/plan-orchestrator.ts`, `src/prompts/*.ts`, `src/templates/claude-md.ts` | | -| **Web** | `src/web/server.ts`, `src/web/sse-events.ts`, `src/web/routes/*.ts` (13 route modules incl. `ws-routes.ts` + barrel), `src/web/route-helpers.ts`, `src/web/ports/*.ts`, `src/web/middleware/auth.ts`, `src/web/schemas.ts` | | -| **Frontend** | `src/web/public/app.js` (~2.6K lines, core) + 5 infra modules (`constants.js`, `mobile-handlers.js`, `voice-input.js`, `notification-manager.js`, `keyboard-accessory.js`) + 6 domain modules (`terminal-ui.js`, `respawn-ui.js`, `ralph-panel.js`, `settings-ui.js`, `panels-ui.js`, `session-ui.js`) + 4 feature modules (`ralph-wizard.js`, `api-client.js`, `subagent-windows.js`, `input-cjk.js`) + `sw.js` | | -| **Types** | `src/types/index.ts` → 13 domain files | See `@fileoverview` in index.ts | +| **Web** | `src/web/server.ts`, `src/web/sse-events.ts`, `src/web/routes/*.ts` (14 route modules incl. `ws-routes.ts` + barrel), `src/web/route-helpers.ts`, `src/web/ports/*.ts`, `src/web/middleware/auth.ts`, `src/web/schemas.ts` | | +| **Frontend** | `src/web/public/app.js` (~2.6K lines, core) + 5 infra modules (`constants.js`, `mobile-handlers.js`, `voice-input.js`, `notification-manager.js`, `keyboard-accessory.js`) + 7 domain modules (`terminal-ui.js`, `respawn-ui.js`, `ralph-panel.js`, `orchestrator-panel.js`, `settings-ui.js`, `panels-ui.js`, `session-ui.js`) + 4 feature modules (`ralph-wizard.js`, `api-client.js`, `subagent-windows.js`, `input-cjk.js`) + `sw.js` | | +| **Types** | `src/types/index.ts` → 15 domain files | See `@fileoverview` in index.ts | ★ = Large file (>50KB). All files have `@fileoverview` JSDoc — read that before diving in. @@ -143,7 +144,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph ### Frontend -Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. Load order: `constants.js`(1) → `mobile-handlers.js`(2) → `voice-input.js`(3) → `notification-manager.js`(4) → `keyboard-accessory.js`(5) → `input-cjk.js`(5.5) → `app.js`(6) → `terminal-ui.js`(7) → `respawn-ui.js`(8) → `ralph-panel.js`(9) → `settings-ui.js`(10) → `panels-ui.js`(11) → `session-ui.js`(12) → `ralph-wizard.js`(13) → `api-client.js`(14) → `subagent-windows.js`(15). `input-cjk.js` handles CJK IME composition via an always-visible textarea below the terminal (`window.cjkActive` blocks xterm's onData). +Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. Load order: `constants.js`(1) → `mobile-handlers.js`(2) → `voice-input.js`(3) → `notification-manager.js`(4) → `keyboard-accessory.js`(5) → `input-cjk.js`(5.5) → `app.js`(6) → `terminal-ui.js`(7) → `respawn-ui.js`(8) → `ralph-panel.js`(9) → `orchestrator-panel.js`(9.5) → `settings-ui.js`(10) → `panels-ui.js`(11) → `session-ui.js`(12) → `ralph-wizard.js`(13) → `api-client.js`(14) → `subagent-windows.js`(15). `input-cjk.js` handles CJK IME composition via an always-visible textarea below the terminal (`window.cjkActive` blocks xterm's onData). **Z-index layers**: subagent windows (1000), plan agents (1100), log viewers (2000), image popups (3000), local echo overlay (7). @@ -170,7 +171,7 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L ### API Routes -~114 handlers across 13 route files in `src/web/routes/`: system (36), sessions (25), ralph (9), plan (8), respawn (7), cases (7), files (5), mux (5), scheduled (4), push (4), teams (2), hooks (1), ws (1 WebSocket). Each file has `@fileoverview` with endpoint details. +~124 handlers across 14 route files in `src/web/routes/`: system (36), sessions (25), orchestrator (10), ralph (9), plan (8), respawn (7), cases (7), files (5), mux (5), scheduled (4), push (4), teams (2), hooks (1), ws (1 WebSocket). Each file has `@fileoverview` with endpoint details. ## Adding Features @@ -225,7 +226,7 @@ Target: 20 sessions, 50 agent windows at 60fps. Limits in `src/config/`: termina ## References -Deep-dive docs in `docs/`: `respawn-state-machine.md`, `ralph-wiggum-guide.md`, `claude-code-hooks-reference.md`, `terminal-anti-flicker.md`, `opencode-integration.md`, `qr-auth-plan.md`. Agent Teams: `agent-teams/README.md`. SSE events: `src/web/sse-events.ts` + `constants.js`. +Deep-dive docs in `docs/`: `respawn-state-machine.md`, `ralph-wiggum-guide.md`, `claude-code-hooks-reference.md`, `terminal-anti-flicker.md`, `opencode-integration.md`, `qr-auth-plan.md`, `orchestrator-loop-architecture.md`. Agent Teams: `agent-teams/README.md`. SSE events: `src/web/sse-events.ts` + `constants.js`. ## Scripts diff --git a/src/web/public/panels-ui.js b/src/web/public/panels-ui.js index c3dab11d..a182247d 100644 --- a/src/web/public/panels-ui.js +++ b/src/web/public/panels-ui.js @@ -3049,8 +3049,9 @@ Object.assign(CodemanApp.prototype, { modelHtml = `${modelShort}`; } + const sid = escapeHtml(muxSession.sessionId); html += ` -
+
${statusLabel}
${modelHtml} ${escapeHtml(muxSession.name || muxSession.muxName)}
@@ -3063,7 +3064,7 @@ Object.assign(CodemanApp.prototype, {
- +
`; diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 3f996aa8..5a87b455 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -2134,8 +2134,15 @@ body { background: var(--bg-hover); } -.history-item-dir { +.history-item-text { flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 0.15rem; +} + +.history-item-title { font-size: 0.8rem; color: var(--text); overflow: hidden; @@ -2143,6 +2150,14 @@ body { white-space: nowrap; } +.history-item-subtitle { + font-size: 0.65rem; + color: var(--text-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .history-item-meta { font-size: 0.7rem; color: var(--text-muted); @@ -5011,6 +5026,16 @@ kbd { background: var(--bg-hover); } +.process-item-clickable { + cursor: pointer; + transition: border-color 0.15s, background 0.15s; + border: 1px solid transparent; +} + +.process-item-clickable:hover { + border-color: var(--accent); +} + .process-info { flex: 1; min-width: 0; diff --git a/src/web/public/terminal-ui.js b/src/web/public/terminal-ui.js index 28984cff..0b206f24 100644 --- a/src/web/public/terminal-ui.js +++ b/src/web/public/terminal-ui.js @@ -704,9 +704,18 @@ Object.assign(CodemanApp.prototype, { item.title = s.workingDir; item.addEventListener('click', () => this.resumeHistorySession(s.sessionId, s.workingDir)); - const dirSpan = document.createElement('span'); - dirSpan.className = 'history-item-dir'; - dirSpan.textContent = shortDir; + const textCol = document.createElement('div'); + textCol.className = 'history-item-text'; + + const titleSpan = document.createElement('span'); + titleSpan.className = 'history-item-title'; + titleSpan.textContent = s.firstPrompt || shortDir; + + const subtitleSpan = document.createElement('span'); + subtitleSpan.className = 'history-item-subtitle'; + subtitleSpan.textContent = shortDir; + + textCol.append(titleSpan, subtitleSpan); const metaSpan = document.createElement('span'); metaSpan.className = 'history-item-meta'; @@ -716,7 +725,7 @@ Object.assign(CodemanApp.prototype, { sizeSpan.className = 'history-item-size'; sizeSpan.textContent = size; - item.append(dirSpan, metaSpan, sizeSpan); + item.append(textCol, metaSpan, sizeSpan); list.appendChild(item); } diff --git a/src/web/routes/session-routes.ts b/src/web/routes/session-routes.ts index 71c657cb..5c78f97e 100644 --- a/src/web/routes/session-routes.ts +++ b/src/web/routes/session-routes.ts @@ -962,6 +962,39 @@ export function registerSessionRoutes( // History — list past Claude conversations for resume // ═══════════════════════════════════════════════════════════════ + /** Extract the text of the first user message from a JSONL transcript head. */ + function extractFirstUserPrompt(head: string): string | undefined { + const MAX_PROMPT_LEN = 120; + for (const line of head.split('\n')) { + if (!line.includes('"type":"user"')) continue; + try { + const entry = JSON.parse(line); + if (entry.type !== 'user' || !entry.message) continue; + const content = entry.message.content; + let text: string | undefined; + if (typeof content === 'string') { + text = content; + } else if (Array.isArray(content)) { + const textBlock = content.find((b: { type: string }) => b.type === 'text'); + if (textBlock) text = textBlock.text; + } + if (!text) continue; + // Strip XML-like system/command tags that Claude Code injects into transcripts + text = text + .replace(/<[^>]+>/g, '') + .trim() + .replace(/\s+/g, ' '); + if (!text) continue; + // Skip system-injected messages and slash command artifacts (not real user prompts) + if (/^(Caveat:|init\b|clear\b|\/\w+ \w+$|You are a )/i.test(text)) continue; + return text.length > MAX_PROMPT_LEN ? text.slice(0, MAX_PROMPT_LEN) + '…' : text; + } catch { + // Malformed line — skip + } + } + return undefined; + } + app.get('/api/history/sessions', async () => { const projectsDir = join(process.env.HOME || '/tmp', '.claude', 'projects'); const results: Array<{ @@ -970,6 +1003,7 @@ export function registerSessionRoutes( projectKey: string; sizeBytes: number; lastModified: string; + firstPrompt?: string; }> = []; try { @@ -1006,24 +1040,35 @@ export function registerSessionRoutes( // Quick content check: verify actual conversation data exists. // Sessions with only file-history-snapshot or hook_progress entries have // no "user"/"assistant" messages and will fail claude --resume. - // Files > 50KB are almost certainly real conversations (skip the read). - if (fileStat.size < 50000) { + // Read first 16KB to check content and extract first user prompt. + let firstPrompt: string | undefined; + const readHead = async (): Promise => { try { const fd = await fs.open(filePath, 'r'); const buf = Buffer.alloc(16384); const { bytesRead } = await fd.read(buf, 0, 16384, 0); await fd.close(); - const head = buf.toString('utf8', 0, bytesRead); - if ( - !head.includes('"type":"user"') && - !head.includes('"type":"assistant"') && - !head.includes('"type":"summary"') - ) { - continue; // No conversation content — skip - } + return buf.toString('utf8', 0, bytesRead); } catch { - continue; + return null; } + }; + + if (fileStat.size < 50000) { + const head = await readHead(); + if ( + !head || + (!head.includes('"type":"user"') && + !head.includes('"type":"assistant"') && + !head.includes('"type":"summary"')) + ) { + continue; // No conversation content — skip + } + firstPrompt = extractFirstUserPrompt(head); + } else { + // Large files are almost certainly real conversations; still read head for prompt + const head = await readHead(); + if (head) firstPrompt = extractFirstUserPrompt(head); } results.push({ @@ -1032,6 +1077,7 @@ export function registerSessionRoutes( projectKey: projDir, sizeBytes: fileStat.size, lastModified: fileStat.mtime.toISOString(), + firstPrompt, }); } }