diff --git a/src/web/public/index.html b/src/web/public/index.html index 59d08694..70fe4d75 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -256,6 +256,10 @@
+Or press Ctrl+Enter to start
diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 03efdc78..4827785b 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -2080,6 +2080,70 @@ body { text-align: center; } +/* History sessions panel */ +.history-sessions { + margin-top: 1.5rem; + width: 100%; + max-width: 560px; +} + +.history-title { + font-size: 0.85rem; + color: var(--text-dim); + margin-bottom: 0.5rem; + font-weight: 500; + text-align: left; +} + +.history-list { + display: flex; + flex-direction: column; + gap: 0.35rem; + max-height: 240px; + overflow-y: auto; +} + +.history-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem 0.75rem; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 6px; + cursor: pointer; + transition: border-color 0.15s, background 0.15s; + text-align: left; +} + +.history-item:hover { + border-color: var(--accent); + background: var(--bg-hover); +} + +.history-item-dir { + flex: 1; + font-size: 0.8rem; + color: var(--text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.history-item-meta { + font-size: 0.7rem; + color: var(--text-muted); + white-space: nowrap; +} + +.history-item-size { + font-size: 0.7rem; + color: var(--text-dim); + white-space: nowrap; + min-width: 45px; + text-align: right; +} + .welcome-hint { color: var(--text-muted); font-size: 0.8rem; diff --git a/src/web/routes/session-routes.ts b/src/web/routes/session-routes.ts index 1ff303c0..804c816a 100644 --- a/src/web/routes/session-routes.ts +++ b/src/web/routes/session-routes.ts @@ -144,6 +144,7 @@ export function registerSessionRoutes( claudeMode: claudeModeConfig.claudeMode, allowedTools: claudeModeConfig.allowedTools, openCodeConfig: mode === 'opencode' ? body.openCodeConfig : undefined, + resumeSessionId: body.resumeSessionId, }); ctx.addSession(session); @@ -922,4 +923,67 @@ export function registerSessionRoutes( return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err)); } }); + + // ═══════════════════════════════════════════════════════════════ + // History — list past Claude conversations for resume + // ═══════════════════════════════════════════════════════════════ + + app.get('/api/history/sessions', async () => { + const projectsDir = join(process.env.HOME || '/tmp', '.claude', 'projects'); + const results: Array<{ + sessionId: string; + workingDir: string; + projectKey: string; + sizeBytes: number; + lastModified: string; + }> = []; + + try { + const projectDirs = await fs.readdir(projectsDir); + for (const projDir of projectDirs) { + const projPath = join(projectsDir, projDir); + const stat = await fs.stat(projPath).catch(() => null); + if (!stat?.isDirectory()) continue; + + // Decode project key to working dir. The encoding replaces '/' with '-', + // which is lossy when path components contain '-'. Do naive decode first, + // then verify it exists. Fall back to HOME if the decoded path is invalid. + const naiveDecode = projDir.replace(/^-/, '/').replace(/-/g, '/'); + const dirExists = await fs + .access(naiveDecode) + .then(() => true) + .catch(() => false); + const workingDir = dirExists ? naiveDecode : process.env.HOME || '/tmp'; + + const entries = await fs.readdir(projPath); + for (const entry of entries) { + if (!entry.endsWith('.jsonl')) continue; + const sessionId = entry.replace('.jsonl', ''); + // Only valid UUIDs + if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(sessionId)) continue; + + const filePath = join(projPath, entry); + const fileStat = await fs.stat(filePath).catch(() => null); + if (!fileStat) continue; + // Skip tiny files (empty or just init) + if (fileStat.size < 500) continue; + + results.push({ + sessionId, + workingDir, + projectKey: projDir, + sizeBytes: fileStat.size, + lastModified: fileStat.mtime.toISOString(), + }); + } + } + } catch { + // Projects dir may not exist + } + + // Sort by lastModified descending + results.sort((a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()); + + return { sessions: results.slice(0, 50) }; + }); }