diff --git a/src/server/routes/session.ts b/src/server/routes/session.ts index 717cb4d47..e2de66a41 100644 --- a/src/server/routes/session.ts +++ b/src/server/routes/session.ts @@ -9,6 +9,7 @@ import { sessionError, conflict, } from '../errors.js' +import { listSessionsImpl } from '../../utils/listSessionsImpl.js' function ssePrompt( handle: import('../sessionHandle.js').SessionHandle, @@ -164,31 +165,149 @@ export function createSessionRoutes( throw sessionError(msg) } }) - .get('/session', c => { + .get('/session', async c => { const url = new URL(c.req.url) const limit = parseInt(url.searchParams.get('limit') ?? '50', 10) const offset = parseInt(url.searchParams.get('offset') ?? '0', 10) + const dir = url.searchParams.get('dir') ?? undefined + // roots=true 时只返回没有 parentID 的顶层 session(csc 无 parent 概念,全部视为 root) + // const rootsOnly = url.searchParams.get('roots') === 'true' - const handles = sessionManager.getAllSessions() - const sessions = handles - .slice(offset, offset + limit) - .map(h => h.getInfo()) + // 从磁盘读取历史 session 列表 + let historySessions: Awaited> = [] + try { + historySessions = await listSessionsImpl({ dir, limit: limit + offset }) + process.stderr.write(`[server:session] listSessionsImpl dir=${dir ?? 'all'} found=${historySessions.length}\n`) + } catch (err) { + process.stderr.write(`[server:session] listSessionsImpl error: ${err}\n`) + } + + // 内存中活跃的 handle,用于覆盖运行时状态 + const handleMap = new Map( + sessionManager.getAllSessions().map(h => [h.sessionId, h]) + ) + + // 把磁盘历史会话转成统一格式,如果内存中有对应 handle 则合并运行时字段 + const merged = historySessions.map(s => { + const handle = handleMap.get(s.sessionId) + const info = handle?.getInfo() + return { + session_id: s.sessionId, + status: info?.status ?? 'stopped', + cwd: info?.cwd ?? s.cwd ?? '', + title: (info?.title ?? s.customTitle ?? s.firstPrompt ?? s.summary) ?? '', + model: info?.model, + permission_mode: info?.permission_mode, + created_at: s.createdAt ?? info?.created_at ?? 0, + last_active_at: s.lastModified ?? info?.last_active_at ?? 0, + cost_usd: info?.cost_usd ?? 0, + input_tokens: info?.input_tokens ?? 0, + output_tokens: info?.output_tokens ?? 0, + } + }) + + // 补充内存中有但磁盘还没落盘的活跃 session(刚创建还没写过消息的) + const historyIds = new Set(historySessions.map(s => s.sessionId)) + for (const handle of sessionManager.getAllSessions()) { + if (!historyIds.has(handle.sessionId)) { + const info = handle.getInfo() + merged.push({ ...info, title: info.title ?? '' }) + } + } + + // 过滤掉已知的无意义管理命令会话(用户直接输入后立即退出,没有实际对话内容) + const BORING_COMMANDS = new Set([ + '/exit', '/quit', '/bye', + '/clear', '/reset', + '/model', '/models', + '/login', '/logout', + '/help', '/version', + '/compact', + ]) + const filtered = merged.filter(s => { + const t = s.title.trim() + if (!t) return false + if (BORING_COMMANDS.has(t)) return false + return true + }) + + // 按最后活跃时间倒序 + filtered.sort((a, b) => (b.last_active_at ?? 0) - (a.last_active_at ?? 0)) + + const sessions = filtered.slice(offset, offset + limit) + process.stderr.write(`[server:session] GET /session -> history=${historySessions.length} active=${handleMap.size} merged=${merged.length} returned=${sessions.length}\n`) + return c.json({ sessions }) + }) + .get('/session/status', async c => { + // 内存中活跃 session 的状态 + const activeStatuses = sessionManager.getSessionStatuses() + + // 补充磁盘历史 session(全部视为 idle) + let historySessions: Awaited> = [] + try { + historySessions = await listSessionsImpl({ limit: 200 }) + } catch {} + + const sessions: Record = {} + + // 先把历史 session 全部标为 idle/stopped + for (const s of historySessions) { + sessions[s.sessionId] = { + status: 'stopped', + state: 'stopped', + has_pending_permission: false, + type: 'idle', + } + } + + // 用内存中活跃的 handle 状态覆盖 + for (const [id, st] of Object.entries(activeStatuses)) { + sessions[id] = { + status: st.status, + state: st.status, + has_pending_permission: st.has_pending_permission, + type: st.status === 'running' ? 'busy' : 'idle', + } + } return c.json({ sessions }) }) - .get('/session/status', c => { - return c.json({ sessions: sessionManager.getSessionStatuses() }) - }) - .get('/session/:sessionID', c => { + .get('/session/:sessionID', async c => { const id = c.req.param('sessionID') const handle = sessionManager.getSession(id) - if (!handle) throw notFound('session not found') - const info = handle.getInfo() - return c.json({ - ...info, - message_count: handle.messageCount, - usage: handle.usage, - }) + if (handle) { + const info = handle.getInfo() + return c.json({ + ...info, + message_count: handle.messageCount, + usage: handle.usage, + }) + } + + // 内存中没有,从磁盘历史记录查找 + try { + const history = await listSessionsImpl({ limit: 1000 }) + const s = history.find(h => h.sessionId === id) + if (s) { + return c.json({ + session_id: s.sessionId, + status: 'stopped', + cwd: s.cwd ?? '', + title: (s.customTitle ?? s.firstPrompt ?? s.summary) ?? '', + model: undefined, + permission_mode: undefined, + created_at: s.createdAt ?? 0, + last_active_at: s.lastModified ?? 0, + cost_usd: 0, + input_tokens: 0, + output_tokens: 0, + message_count: 0, + usage: { input_tokens: 0, output_tokens: 0 }, + }) + } + } catch {} + + throw notFound('session not found') }) .patch('/session/:sessionID', async c => { const id = c.req.param('sessionID') @@ -298,4 +417,29 @@ export function createSessionRoutes( if (!body.command) throw badRequest('command is required') return ssePrompt(handle, id, body.command, c) }) + .post('/session/:sessionID/command_async', async c => { + const id = c.req.param('sessionID') + const handle = sessionManager.getSession(id) + if (!handle) throw notFound('session not found') + + const body = await c.req.json<{ command: string }>() + if (!body.command) throw badRequest('command is required') + if (handle.prompting) throw conflict('session is already processing a prompt') + + handle.prompt(body.command).catch(() => {}) + + return new Response(null, { status: 204 }) + }) + .post('/session/:sessionID/revert', async c => { + const id = c.req.param('sessionID') + const handle = sessionManager.getSession(id) + if (!handle) throw notFound('session not found') + return c.json(handle.getInfo()) + }) + .post('/session/:sessionID/summarize', async c => { + const id = c.req.param('sessionID') + const handle = sessionManager.getSession(id) + if (!handle) throw notFound('session not found') + return c.json({ ok: true }) + }) }