diff --git a/build.ts b/build.ts index fceb03b98..5a2ad896e 100644 --- a/build.ts +++ b/build.ts @@ -8,15 +8,19 @@ const outdir = 'dist' const { rmSync } = await import('fs') rmSync(outdir, { recursive: true, force: true }) -// Step 1.5: Generate review builtin files -console.log('Generating review builtin files...') -const { spawnSync: genSpawnSync } = await import('child_process') -const genResult = genSpawnSync('bun', ['run', 'scripts/generate-review-builtin.ts'], { - stdio: 'inherit', - cwd: process.cwd(), -}) -if (genResult.status !== 0) { - console.warn('Warning: generate-review-builtin.ts failed, using existing files') +// Step 1.5: Generate review builtin files (skip with SKIP_REVIEW_BUILTIN=1) +if (process.env.SKIP_REVIEW_BUILTIN) { + console.log('Skipping review builtin generation (SKIP_REVIEW_BUILTIN is set)') +} else { + console.log('Generating review builtin files...') + const { spawnSync: genSpawnSync } = await import('child_process') + const genResult = genSpawnSync('bun', ['run', 'scripts/generate-review-builtin.ts'], { + stdio: 'inherit', + cwd: process.cwd(), + }) + if (genResult.status !== 0) { + console.warn('Warning: generate-review-builtin.ts failed, using existing files') + } } // Default features that match the official CLI build. diff --git a/scripts/dev.ts b/scripts/dev.ts index 18bf21547..39d258284 100644 --- a/scripts/dev.ts +++ b/scripts/dev.ts @@ -73,14 +73,18 @@ const inspectArgs = process.env.BUN_INSPECT // npm, etc.) and on all platforms. const bunCmd = process.execPath; -// Generate review builtin files before dev launch -console.log('[dev] Generating review builtin files...'); -const genResult = Bun.spawnSync(['bun', 'run', 'scripts/generate-review-builtin.ts'], { - stdio: ["inherit", "inherit", "inherit"], - cwd: projectRoot, -}); -if (!genResult.success) { - console.warn('[dev] Warning: generate-review-builtin.ts failed, using existing files'); +// Generate review builtin files before dev launch (skip with SKIP_REVIEW_BUILTIN=1) +if (process.env.SKIP_REVIEW_BUILTIN) { + console.log('[dev] Skipping review builtin generation (SKIP_REVIEW_BUILTIN is set)'); +} else { + console.log('[dev] Generating review builtin files...'); + const genResult = Bun.spawnSync(['bun', 'run', 'scripts/generate-review-builtin.ts'], { + stdio: ["inherit", "inherit", "inherit"], + cwd: projectRoot, + }); + if (!genResult.success) { + console.warn('[dev] Warning: generate-review-builtin.ts failed, using existing files'); + } } const args = [bunCmd, ...inspectArgs, "run", ...defineArgs, ...featureArgs, cliPath, ...process.argv.slice(2)]; diff --git a/src/cli/print.ts b/src/cli/print.ts index 412cd38c6..51ba7b830 100644 --- a/src/cli/print.ts +++ b/src/cli/print.ts @@ -161,7 +161,7 @@ import { DEFAULT_OUTPUT_STYLE_NAME, getAllOutputStyles, } from 'src/constants/outputStyles.js' -import { TEAMMATE_MESSAGE_TAG, TICK_TAG } from 'src/constants/xml.js' +import { TASK_NOTIFICATION_TAG, TEAMMATE_MESSAGE_TAG, TICK_TAG } from 'src/constants/xml.js' import { getSettings_DEPRECATED, getSettingsWithSources, @@ -4360,29 +4360,44 @@ function runHeadlessStreaming( trackReceivedMessageUuid(userMsg.uuid as UUID) } - enqueue({ - mode: 'prompt' as const, - // file_attachments rides the protobuf catchall from the web composer. - // Same-ref no-op when absent (no 'file_attachments' key). - value: await resolveAndPrepend( - userMsg, - (userMsg.message as { content: ContentBlockParam[] }).content, - ), - uuid: userMsg.uuid as `${string}-${string}-${string}-${string}-${string}`, - priority: (userMsg as { priority?: string }) - .priority as import('src/types/textInputTypes.js').QueuePriority, - }) - // Increment prompt count for attribution tracking and save snapshot - // The snapshot persists promptCount so it survives compaction - if (feature('COMMIT_ATTRIBUTION')) { - setAppState(prev => ({ - ...prev, - attribution: incrementPromptCount(prev.attribution, snapshot => { - void recordAttributionSnapshot(snapshot).catch(error => { - logForDebugging(`Attribution: Failed to save snapshot: ${error}`) - }) - }), - })) + const rawContent = typeof userMsg.content === 'string' ? userMsg.content : '' + const isTaskNotification = rawContent.trimStart().startsWith( + `<${TASK_NOTIFICATION_TAG}>`, + ) + + if (isTaskNotification) { + enqueue({ + mode: 'task-notification' as const, + value: rawContent, + uuid: userMsg.uuid as `${string}-${string}-${string}-${string}-${string}`, + priority: (userMsg as { priority?: string }) + .priority as import('src/types/textInputTypes.js').QueuePriority, + }) + } else { + enqueue({ + mode: 'prompt' as const, + // file_attachments rides the protobuf catchall from the web composer. + // Same-ref no-op when absent (no 'file_attachments' key). + value: await resolveAndPrepend( + userMsg, + (userMsg.message as { content: ContentBlockParam[] }).content, + ), + uuid: userMsg.uuid as `${string}-${string}-${string}-${string}-${string}`, + priority: (userMsg as { priority?: string }) + .priority as import('src/types/textInputTypes.js').QueuePriority, + }) + // Increment prompt count for attribution tracking and save snapshot + // The snapshot persists promptCount so it survives compaction + if (feature('COMMIT_ATTRIBUTION')) { + setAppState(prev => ({ + ...prev, + attribution: incrementPromptCount(prev.attribution, snapshot => { + void recordAttributionSnapshot(snapshot).catch(error => { + logForDebugging(`Attribution: Failed to save snapshot: ${error}`) + }) + }), + })) + } } void run() } diff --git a/src/server/eventBus.ts b/src/server/eventBus.ts index 9f40aade4..899eb5299 100644 --- a/src/server/eventBus.ts +++ b/src/server/eventBus.ts @@ -74,7 +74,7 @@ export class EventBus { const sid = dataObj?.session_id ?? dataObj?.sessionID if (typeof sid === 'string') { const sessionCwd = this.sessionCwds.get(sid) - if (sessionCwd && sessionCwd !== client.cwdFilter) { + if (sessionCwd !== client.cwdFilter) { continue } } diff --git a/src/server/routes/session.ts b/src/server/routes/session.ts index baf51a361..5f13d1339 100644 --- a/src/server/routes/session.ts +++ b/src/server/routes/session.ts @@ -221,14 +221,12 @@ export function createSessionRoutes( 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) - // 优先从请求头 x-csc-directory 取工作目录(cs-cloud 通过此头传递), - // fallback 到 query string 的 dir 参数 const headerDir = c.req.header('x-csc-directory') const dir = (headerDir ? decodeURIComponent(headerDir) : undefined) ?? url.searchParams.get('dir') ?? undefined - // roots=true 时只返回没有 parentID 的顶层 session(csc 无 parent 概念,全部视为 root) - // const rootsOnly = url.searchParams.get('roots') === 'true' + + const canonicalDir = dir ? await canonicalizePath(dir).catch(() => dir) : undefined // 从磁盘读取历史 session 列表 let historySessions: Awaited> = [] @@ -271,7 +269,6 @@ export function createSessionRoutes( // 补充内存中有但磁盘还没落盘的活跃 session(刚创建还没写过消息的) const historyIds = new Set(historySessions.map(s => s.sessionId)) - const canonicalDir = dir ? await canonicalizePath(dir).catch(() => dir) : undefined for (const handle of sessionManager.getAllSessions()) { if (!historyIds.has(handle.sessionId)) { if (canonicalDir) { @@ -283,7 +280,31 @@ export function createSessionRoutes( } } - // 过滤掉已知的无意义管理命令会话(用户直接输入后立即退出,没有实际对话内容) + // Pre-canonicalize all session cwds for consistent cwd filtering + const cwdCache = new Map() + const getCwd = async (p: string) => { + const hit = cwdCache.get(p) + if (hit !== undefined) return hit + const c = await canonicalizePath(p).catch(() => p) + cwdCache.set(p, c) + return c + } + + // Filter by cwd to prevent cross-workspace leakage + const cwdFiltered: typeof merged = [] + for (const s of merged) { + if (canonicalDir) { + const sessionCwd = s.cwd ?? s.directory ?? '' + if (sessionCwd) { + const c = await getCwd(sessionCwd) + if (c !== canonicalDir) continue + } else { + continue + } + } + cwdFiltered.push(s) + } + const BORING_COMMANDS = new Set([ '/exit', '/quit', '/bye', '/clear', '/reset', @@ -292,7 +313,7 @@ export function createSessionRoutes( '/help', '/version', '/compact', ]) - const filtered = merged.filter(s => { + const filtered = cwdFiltered.filter(s => { const t = s.title.trim() if (!t) return false if (BORING_COMMANDS.has(t)) return false @@ -308,7 +329,10 @@ export function createSessionRoutes( .get('/session/status', async c => { const headerDir = c.req.header('x-csc-directory') const dir = headerDir ? decodeURIComponent(headerDir) : undefined - const statuses = sessionManager.getSessionStatuses(dir) + const activeCount = sessionManager.getActiveCount() + const indexCount = sessionManager.getLoadedIndexCount() + const statuses = await sessionManager.getSessionStatuses(dir) + process.stderr.write(`[status] dir=${dir ?? '(none)'} active=${activeCount} index=${indexCount} result=${JSON.stringify(statuses)}\n`) return c.json(statuses) }) .get('/session/:sessionID', async c => { @@ -318,6 +342,7 @@ export function createSessionRoutes( const info = handle.getInfo() return c.json({ ...info, + busy_status: handle.getEffectiveBusyStatus(), message_count: handle.messageCount, usage: handle.usage, }) diff --git a/src/server/sessionHandle.ts b/src/server/sessionHandle.ts index 238b5e222..e101cffd5 100644 --- a/src/server/sessionHandle.ts +++ b/src/server/sessionHandle.ts @@ -141,6 +141,16 @@ export class SessionHandle implements DisposableChildProcess { return this._lastMessageUuid } + getEffectiveBusyStatus(): SessionBusyStatus { + if (this._prompting) { + return { type: 'busy' } + } + if (this._status === 'stopped') { + return { type: 'idle' } + } + return this._busyStatus + } + get ready(): boolean { return this._status === 'running' } diff --git a/src/server/sessionManager.ts b/src/server/sessionManager.ts index 30f50a7d7..7e930d730 100644 --- a/src/server/sessionManager.ts +++ b/src/server/sessionManager.ts @@ -147,6 +147,10 @@ export class SessionManager { return this.sessions.size } + getLoadedIndexCount(): number { + return this.loadedIndex.size + } + getSession(id: string): SessionHandle | undefined { return this.sessions.get(id) } @@ -163,7 +167,20 @@ export class SessionManager { const handleCwd = await canonicalizePath(handle.cwd) if (handleCwd !== canonicalCwd) continue } - result[id] = handle.busyStatus + result[id] = handle.getEffectiveBusyStatus() + } + // Supplement with persisted sessions that are no longer in memory + // (evicted by idle timeout / LRU). They are always idle. + if (canonicalCwd) { + for (const [id, entry] of this.loadedIndex) { + if (result[id]) continue + try { + const entryCwd = await canonicalizePath(entry.cwd) + if (entryCwd === canonicalCwd) { + result[id] = { type: 'idle' } + } + } catch {} + } } return result } @@ -220,9 +237,8 @@ export class SessionManager { this.sessions.set(sessionId, handle) if (!opts.silent) { - void canonicalizePath(cwd).then(canonical => { - this.eventBus.registerSessionCwd(sessionId, canonical) - }) + const canonical = await canonicalizePath(cwd).catch(() => cwd) + this.eventBus.registerSessionCwd(sessionId, canonical) this.eventBus.publishSessionEvent(sessionId, 'created', { status: 'starting', created_at: Date.now(),