feat: improve session history with first prompt + clickable monitor rows (closes #45)

This commit is contained in:
arkon
2026-03-22 01:57:17 +01:00
parent e899a43a18
commit 40dfac3534
5 changed files with 106 additions and 24 deletions
+7 -6
View File
@@ -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
+3 -2
View File
@@ -3049,8 +3049,9 @@ Object.assign(CodemanApp.prototype, {
modelHtml = `<span class="monitor-model-badge ${modelShort}">${modelShort}</span>`;
}
const sid = escapeHtml(muxSession.sessionId);
html += `
<div class="process-item">
<div class="process-item process-item-clickable" onclick="app.selectSession('${sid}')" title="Switch to session">
<span class="monitor-status-badge ${statusClass}">${statusLabel}</span>
<div class="process-info">
<div class="process-name">${modelHtml} ${escapeHtml(muxSession.name || muxSession.muxName)}</div>
@@ -3063,7 +3064,7 @@ Object.assign(CodemanApp.prototype, {
</div>
</div>
<div class="process-actions">
<button class="btn-toolbar btn-sm btn-danger" onclick="app.killMuxSession('${escapeHtml(muxSession.sessionId)}')" title="Kill session">Kill</button>
<button class="btn-toolbar btn-sm btn-danger" onclick="event.stopPropagation(); app.killMuxSession('${sid}')" title="Kill session">Kill</button>
</div>
</div>
`;
+26 -1
View File
@@ -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;
+13 -4
View File
@@ -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);
}
+57 -11
View File
@@ -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<string | null> => {
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,
});
}
}