- src/http/history.ts: listSessions() reads ~/.claude/projects/*/*.jsonl
(mtime-sorted, top 50), parseSessionMeta() extracts cwd + first user prompt
(pure, 4 unit tests); GET /sessions
- terminal-session: opts.initialInput (typed ~700ms after the shell is ready)
- tabs.newTabForResume(cwd,id): new tab in the project dir running
'claude --resume <id>'
- public/history.ts: 🕘 panel listing sessions (project · time · preview) with
Resume buttons
- Verified: /sessions returns 44 real sessions; panel lists project/time/preview.
216 tests green.
110 lines
3.1 KiB
TypeScript
110 lines
3.1 KiB
TypeScript
/**
|
|
* public/history.ts — resume a past Claude Code session (O2).
|
|
*
|
|
* A 🕘 toolbar button opens a list of past sessions (GET /sessions); clicking
|
|
* "Resume" opens a new tab in that project's directory and runs
|
|
* `claude --resume <id>`.
|
|
*/
|
|
|
|
export interface HistoryHooks {
|
|
resume: (cwd: string, id: string) => void
|
|
}
|
|
|
|
interface HistorySession {
|
|
id: string
|
|
cwd: string
|
|
project: string
|
|
mtimeMs: number
|
|
preview: string
|
|
}
|
|
|
|
function relTime(ms: number): string {
|
|
const s = Math.max(0, (Date.now() - ms) / 1000)
|
|
if (s < 60) return 'just now'
|
|
if (s < 3600) return `${Math.floor(s / 60)}m ago`
|
|
if (s < 86400) return `${Math.floor(s / 3600)}h ago`
|
|
return `${Math.floor(s / 86400)}d ago`
|
|
}
|
|
|
|
export function mountHistory(toolbar: HTMLElement, hooks: HistoryHooks): void {
|
|
const overlay = document.createElement('div')
|
|
overlay.id = 'historymodal'
|
|
overlay.style.display = 'none'
|
|
const card = document.createElement('div')
|
|
card.className = 'hist-card'
|
|
overlay.appendChild(card)
|
|
document.body.appendChild(overlay)
|
|
|
|
const hide = (): void => {
|
|
overlay.style.display = 'none'
|
|
}
|
|
overlay.addEventListener('click', (e) => {
|
|
if (e.target === overlay) hide()
|
|
})
|
|
|
|
const open = async (): Promise<void> => {
|
|
card.replaceChildren()
|
|
const title = document.createElement('div')
|
|
title.className = 'hist-title'
|
|
title.textContent = 'Resume a Claude session'
|
|
card.appendChild(title)
|
|
const status = document.createElement('div')
|
|
status.className = 'hist-empty'
|
|
status.textContent = 'Loading…'
|
|
card.appendChild(status)
|
|
overlay.style.display = 'flex'
|
|
|
|
let sessions: HistorySession[]
|
|
try {
|
|
const res = await fetch('/sessions')
|
|
sessions = (await res.json()) as HistorySession[]
|
|
} catch {
|
|
status.textContent = 'Failed to load sessions.'
|
|
return
|
|
}
|
|
if (sessions.length === 0) {
|
|
status.textContent = 'No past Claude sessions found.'
|
|
return
|
|
}
|
|
status.remove()
|
|
|
|
for (const s of sessions) {
|
|
const row = document.createElement('div')
|
|
row.className = 'hist-row'
|
|
|
|
const main = document.createElement('div')
|
|
main.className = 'hist-main'
|
|
const proj = document.createElement('div')
|
|
proj.className = 'hist-proj'
|
|
proj.textContent = `${s.project} · ${relTime(s.mtimeMs)}`
|
|
const prev = document.createElement('div')
|
|
prev.className = 'hist-prev'
|
|
prev.textContent = s.preview || '(no preview)'
|
|
prev.title = s.cwd
|
|
main.append(proj, prev)
|
|
|
|
const btn = document.createElement('button')
|
|
btn.className = 'hist-resume'
|
|
btn.textContent = 'Resume'
|
|
btn.addEventListener('click', () => {
|
|
hooks.resume(s.cwd, s.id)
|
|
hide()
|
|
})
|
|
|
|
row.append(main, btn)
|
|
card.appendChild(row)
|
|
}
|
|
}
|
|
|
|
const toggle = document.createElement('button')
|
|
toggle.className = 'toolbtn'
|
|
toggle.textContent = '🕘'
|
|
toggle.title = 'Resume a past Claude session'
|
|
toggle.setAttribute('aria-label', 'Session history')
|
|
toggle.addEventListener('click', () => {
|
|
if (overlay.style.display === 'none') void open()
|
|
else hide()
|
|
})
|
|
toolbar.appendChild(toggle)
|
|
}
|