repoburg 1.3.201 → 1.3.204
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-pane/backend/dist/index.js +2714 -32395
- package/agent-pane/backend/dist/server.js +45304 -0
- package/askuser-after-step5-postgres-selected.png +0 -0
- package/askuser-after-step7-answer-sent.png +0 -0
- package/askuser-card-visible.png +0 -0
- package/backend/dist/src/agent-runtime/agent-runtime-event-emitter.d.ts +16 -0
- package/backend/dist/src/agent-runtime/agent-runtime-event-emitter.js +87 -0
- package/backend/dist/src/agent-runtime/agent-runtime-event-emitter.js.map +1 -1
- package/backend/dist/src/agent-runtime/agent-runtime.types.d.ts +16 -1
- package/backend/dist/src/app.controller.d.ts +1 -0
- package/backend/dist/src/app.service.d.ts +1 -0
- package/backend/dist/src/app.service.js +4 -1
- package/backend/dist/src/app.service.js.map +1 -1
- package/backend/dist/src/interactive-chat/chat.service.d.ts +4 -0
- package/backend/dist/src/interactive-chat/chat.service.js +21 -1
- package/backend/dist/src/interactive-chat/chat.service.js.map +1 -1
- package/backend/dist/src/llm-orchestration/action-handlers/invoke-subagent.handler.js +12 -1
- package/backend/dist/src/llm-orchestration/action-handlers/invoke-subagent.handler.js.map +1 -1
- package/backend/dist/src/llm-orchestration/llm-orchestration.interfaces.d.ts +1 -0
- package/backend/dist/src/llm-orchestration/llm-orchestration.interfaces.js +1 -0
- package/backend/dist/src/llm-orchestration/llm-orchestration.interfaces.js.map +1 -1
- package/backend/dist/src/llm-orchestration/llm-turn-processor.service.js +2 -0
- package/backend/dist/src/llm-orchestration/llm-turn-processor.service.js.map +1 -1
- package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
- package/daemon/dist/agentPane.js +128 -0
- package/daemon/dist/agentPane.spec.js +183 -0
- package/daemon/dist/api/services.js +4 -0
- package/daemon/dist/index.js +28 -8
- package/daemon/dist/serviceManager.js +30 -11
- package/daemon/dist/serviceManager.spec.js +26 -0
- package/daemon/dist/workspace-validation.js +63 -0
- package/daemon/dist/workspace-validation.spec.js +63 -0
- package/daemon/package.json +1 -1
- package/package.json +15 -2
- package/r2-step4-apple-banana.png +0 -0
- package/r2-step5-after-rewind-apple-last.png +0 -0
- package/r2-step7-apple-cherry.png +0 -0
- package/r2-step8-cherry-menu-no-restore.png +0 -0
- package/r3-step3-live-task-card-running.png +0 -0
- package/r3-step4-after-completion.png +0 -0
- package/r3-step5-persistence-after-reload.png +0 -0
- package/r3-step6-tasks-tab-card.png +0 -0
- package/reverify-answered-state-answer-sent.png +0 -0
- package/reverify-card-visible-after-completion.png +0 -0
- package/rewind-step3-apple-banana-turns.png +0 -0
- package/rewind-step5-ui-unchanged-after-confirmed-rewind.png +0 -0
- package/rewind-step6-apple-menu-no-restore.png +0 -0
- package/rewind-step7-final-turnlist-unchanged.png +0 -0
- package/rewind-step7-negative-error-toast.png +0 -0
- package/rewind-step7-negative-error-toast2.png +0 -0
- package/subagent-r2-step5-turn-after-reload-no-card.png +0 -0
- package/subagent-r2-step6-tasks-panel-card.png +0 -0
- package/subagent-step3-tasks-tab-done-card.png +0 -0
- package/subagent-step4-turn-stuck-no-cards.png +0 -0
- package/agent-pane/.prettierignore +0 -5
- package/agent-pane/.prettierrc +0 -8
- package/agent-pane/backend/coverage/base.css +0 -224
- package/agent-pane/backend/coverage/block-navigation.js +0 -87
- package/agent-pane/backend/coverage/clover.xml +0 -887
- package/agent-pane/backend/coverage/coverage-final.json +0 -10
- package/agent-pane/backend/coverage/favicon.png +0 -0
- package/agent-pane/backend/coverage/index.html +0 -176
- package/agent-pane/backend/coverage/prettify.css +0 -1
- package/agent-pane/backend/coverage/prettify.js +0 -2
- package/agent-pane/backend/coverage/sort-arrow-sprite.png +0 -0
- package/agent-pane/backend/coverage/sorter.js +0 -210
- package/agent-pane/backend/coverage/src/codex/codex-provider.ts.html +0 -1402
- package/agent-pane/backend/coverage/src/codex/index.html +0 -131
- package/agent-pane/backend/coverage/src/codex/item-mapper.ts.html +0 -331
- package/agent-pane/backend/coverage/src/db/index.html +0 -131
- package/agent-pane/backend/coverage/src/db/index.ts.html +0 -436
- package/agent-pane/backend/coverage/src/db/schema.ts.html +0 -175
- package/agent-pane/backend/coverage/src/index.html +0 -116
- package/agent-pane/backend/coverage/src/index.ts.html +0 -262
- package/agent-pane/backend/coverage/src/routes/index.html +0 -146
- package/agent-pane/backend/coverage/src/routes/sessions.ts.html +0 -991
- package/agent-pane/backend/coverage/src/routes/turns.ts.html +0 -256
- package/agent-pane/backend/coverage/src/routes/workspaces.ts.html +0 -406
- package/agent-pane/backend/coverage/src/ws/index.html +0 -116
- package/agent-pane/backend/coverage/src/ws/index.ts.html +0 -238
- package/agent-pane/backend/data.db +0 -0
- package/agent-pane/backend/data.db-shm +0 -0
- package/agent-pane/backend/data.db-wal +0 -0
- package/agent-pane/backend/package-lock.json +0 -5945
- package/agent-pane/backend/package.json +0 -33
- package/agent-pane/backend/scripts/backfill-from-jsonl.ts +0 -206
- package/agent-pane/backend/scripts/import-session-from-jsonl.ts +0 -264
- package/agent-pane/backend/src/claude/claude-message-mapper.test.ts +0 -271
- package/agent-pane/backend/src/claude/claude-message-mapper.ts +0 -259
- package/agent-pane/backend/src/claude/claude-provider.test.ts +0 -455
- package/agent-pane/backend/src/claude/claude-provider.ts +0 -1319
- package/agent-pane/backend/src/claude/claude-types.ts +0 -45
- package/agent-pane/backend/src/codex/app-server-client.test.ts +0 -183
- package/agent-pane/backend/src/codex/app-server-client.ts +0 -247
- package/agent-pane/backend/src/codex/app-server-types.ts +0 -259
- package/agent-pane/backend/src/codex/codex-provider-runtime-config.test.ts +0 -56
- package/agent-pane/backend/src/codex/codex-provider.test.ts +0 -60
- package/agent-pane/backend/src/codex/codex-provider.ts +0 -619
- package/agent-pane/backend/src/codex/item-mapper.test.ts +0 -221
- package/agent-pane/backend/src/codex/item-mapper.ts +0 -86
- package/agent-pane/backend/src/codex/shared-app-server.ts +0 -25
- package/agent-pane/backend/src/db/index.test.ts +0 -125
- package/agent-pane/backend/src/db/index.ts +0 -230
- package/agent-pane/backend/src/db/schema.ts +0 -34
- package/agent-pane/backend/src/index.ts +0 -89
- package/agent-pane/backend/src/integration/__fixtures__/cancel-turn.json +0 -29
- package/agent-pane/backend/src/integration/__fixtures__/delta-streaming.json +0 -40
- package/agent-pane/backend/src/integration/__fixtures__/failed-turn.json +0 -16
- package/agent-pane/backend/src/integration/__fixtures__/mcp-tool-call.json +0 -60
- package/agent-pane/backend/src/integration/__fixtures__/missing-item-started.json +0 -34
- package/agent-pane/backend/src/integration/__fixtures__/multi-item-turn.json +0 -85
- package/agent-pane/backend/src/integration/__fixtures__/multi-turn.json +0 -48
- package/agent-pane/backend/src/integration/__fixtures__/simple-turn.json +0 -35
- package/agent-pane/backend/src/integration/delta-streaming.test.ts +0 -130
- package/agent-pane/backend/src/integration/helpers/app-server-mock.ts +0 -272
- package/agent-pane/backend/src/integration/helpers/test-app.ts +0 -110
- package/agent-pane/backend/src/integration/helpers/ws-capture.ts +0 -64
- package/agent-pane/backend/src/integration/item-lifecycle.test.ts +0 -133
- package/agent-pane/backend/src/integration/mcp-tool-call.test.ts +0 -102
- package/agent-pane/backend/src/integration/missing-item-started.test.ts +0 -91
- package/agent-pane/backend/src/integration/multi-turn.test.ts +0 -121
- package/agent-pane/backend/src/integration/session-operations.test.ts +0 -175
- package/agent-pane/backend/src/integration/threads-claude-fork.test.ts +0 -241
- package/agent-pane/backend/src/integration/threads.test.ts +0 -309
- package/agent-pane/backend/src/integration/title-generation.test.ts +0 -109
- package/agent-pane/backend/src/integration/turn-cancel.test.ts +0 -111
- package/agent-pane/backend/src/integration/turn-failure.test.ts +0 -97
- package/agent-pane/backend/src/integration/turn-lifecycle.test.ts +0 -141
- package/agent-pane/backend/src/providers/index.ts +0 -16
- package/agent-pane/backend/src/providers/registry.test.ts +0 -92
- package/agent-pane/backend/src/providers/registry.ts +0 -43
- package/agent-pane/backend/src/providers/runtime-provider.ts +0 -43
- package/agent-pane/backend/src/repoburg/action-mapping.ts +0 -216
- package/agent-pane/backend/src/repoburg/agent-runtime-client.test.ts +0 -271
- package/agent-pane/backend/src/repoburg/agent-runtime-client.ts +0 -262
- package/agent-pane/backend/src/repoburg/backend-resolver.test.ts +0 -271
- package/agent-pane/backend/src/repoburg/backend-resolver.ts +0 -246
- package/agent-pane/backend/src/repoburg/repoburg-client.test.ts +0 -159
- package/agent-pane/backend/src/repoburg/repoburg-client.ts +0 -181
- package/agent-pane/backend/src/repoburg/repoburg-provider.test.ts +0 -723
- package/agent-pane/backend/src/repoburg/repoburg-provider.ts +0 -422
- package/agent-pane/backend/src/repoburg/turn-projector.test.ts +0 -475
- package/agent-pane/backend/src/repoburg/turn-projector.ts +0 -537
- package/agent-pane/backend/src/routes/files.ts +0 -31
- package/agent-pane/backend/src/routes/mcp.ts +0 -33
- package/agent-pane/backend/src/routes/models.test.ts +0 -92
- package/agent-pane/backend/src/routes/models.ts +0 -16
- package/agent-pane/backend/src/routes/plugins.ts +0 -40
- package/agent-pane/backend/src/routes/providers.test.ts +0 -86
- package/agent-pane/backend/src/routes/providers.ts +0 -86
- package/agent-pane/backend/src/routes/sessions.test.ts +0 -558
- package/agent-pane/backend/src/routes/sessions.ts +0 -504
- package/agent-pane/backend/src/routes/skills.test.ts +0 -114
- package/agent-pane/backend/src/routes/skills.ts +0 -70
- package/agent-pane/backend/src/routes/threads.ts +0 -323
- package/agent-pane/backend/src/routes/turns.test.ts +0 -196
- package/agent-pane/backend/src/routes/turns.ts +0 -103
- package/agent-pane/backend/src/routes/workspaces.test.ts +0 -197
- package/agent-pane/backend/src/routes/workspaces.ts +0 -128
- package/agent-pane/backend/src/ws/index.test.ts +0 -107
- package/agent-pane/backend/src/ws/index.ts +0 -51
- package/agent-pane/backend/tsconfig.json +0 -25
- package/agent-pane/backend/vitest.config.ts +0 -20
- package/agent-pane/eslint.config.js +0 -33
- package/agent-pane/frontend/coverage/base.css +0 -224
- package/agent-pane/frontend/coverage/block-navigation.js +0 -87
- package/agent-pane/frontend/coverage/clover.xml +0 -2962
- package/agent-pane/frontend/coverage/coverage-final.json +0 -37
- package/agent-pane/frontend/coverage/favicon.png +0 -0
- package/agent-pane/frontend/coverage/index.html +0 -251
- package/agent-pane/frontend/coverage/prettify.css +0 -1
- package/agent-pane/frontend/coverage/prettify.js +0 -2
- package/agent-pane/frontend/coverage/sort-arrow-sprite.png +0 -0
- package/agent-pane/frontend/coverage/sorter.js +0 -210
- package/agent-pane/frontend/coverage/src/App.tsx.html +0 -217
- package/agent-pane/frontend/coverage/src/AppInitializer.tsx.html +0 -223
- package/agent-pane/frontend/coverage/src/api/client.ts.html +0 -187
- package/agent-pane/frontend/coverage/src/api/index.html +0 -161
- package/agent-pane/frontend/coverage/src/api/sessions.ts.html +0 -316
- package/agent-pane/frontend/coverage/src/api/websocket.ts.html +0 -544
- package/agent-pane/frontend/coverage/src/api/workspaces.ts.html +0 -199
- package/agent-pane/frontend/coverage/src/components/CollapsibleToolCalls.tsx.html +0 -226
- package/agent-pane/frontend/coverage/src/components/CommandInputBar.tsx.html +0 -382
- package/agent-pane/frontend/coverage/src/components/CommandPalette.tsx.html +0 -715
- package/agent-pane/frontend/coverage/src/components/IdeWorkspace.tsx.html +0 -160
- package/agent-pane/frontend/coverage/src/components/ModelSelector.tsx.html +0 -478
- package/agent-pane/frontend/coverage/src/components/NewSessionDialog.tsx.html +0 -472
- package/agent-pane/frontend/coverage/src/components/RenderArea.tsx.html +0 -574
- package/agent-pane/frontend/coverage/src/components/RightPanel.tsx.html +0 -205
- package/agent-pane/frontend/coverage/src/components/RuntimeIcon.tsx.html +0 -133
- package/agent-pane/frontend/coverage/src/components/SessionTabs.tsx.html +0 -217
- package/agent-pane/frontend/coverage/src/components/StatusBar.tsx.html +0 -271
- package/agent-pane/frontend/coverage/src/components/StatusDot.tsx.html +0 -109
- package/agent-pane/frontend/coverage/src/components/Toast.tsx.html +0 -313
- package/agent-pane/frontend/coverage/src/components/TurnContent.tsx.html +0 -1216
- package/agent-pane/frontend/coverage/src/components/WorkspaceSidebar.tsx.html +0 -1138
- package/agent-pane/frontend/coverage/src/components/index.html +0 -326
- package/agent-pane/frontend/coverage/src/components/panels/ChangesPanel.tsx.html +0 -247
- package/agent-pane/frontend/coverage/src/components/panels/LogsPanel.tsx.html +0 -160
- package/agent-pane/frontend/coverage/src/components/panels/MetricsPanel.tsx.html +0 -427
- package/agent-pane/frontend/coverage/src/components/panels/TurnsPanel.tsx.html +0 -397
- package/agent-pane/frontend/coverage/src/components/panels/index.html +0 -161
- package/agent-pane/frontend/coverage/src/constants/index.html +0 -116
- package/agent-pane/frontend/coverage/src/constants/models.ts.html +0 -406
- package/agent-pane/frontend/coverage/src/hooks/index.html +0 -146
- package/agent-pane/frontend/coverage/src/hooks/useEscapeKey.ts.html +0 -124
- package/agent-pane/frontend/coverage/src/hooks/useOutsideClick.ts.html +0 -142
- package/agent-pane/frontend/coverage/src/hooks/useSessionStatus.ts.html +0 -187
- package/agent-pane/frontend/coverage/src/index.html +0 -146
- package/agent-pane/frontend/coverage/src/main.tsx.html +0 -115
- package/agent-pane/frontend/coverage/src/store/index.html +0 -116
- package/agent-pane/frontend/coverage/src/store/store.ts.html +0 -2404
- package/agent-pane/frontend/coverage/src/test/component-helpers.ts.html +0 -190
- package/agent-pane/frontend/coverage/src/test/fixtures.ts.html +0 -367
- package/agent-pane/frontend/coverage/src/test/index.html +0 -131
- package/agent-pane/frontend/coverage/src/types/index.html +0 -116
- package/agent-pane/frontend/coverage/src/types/index.ts.html +0 -466
- package/agent-pane/frontend/coverage/src/utils/changes.ts.html +0 -202
- package/agent-pane/frontend/coverage/src/utils/duration.ts.html +0 -127
- package/agent-pane/frontend/coverage/src/utils/index.html +0 -131
- package/agent-pane/frontend/dist/assets/index-34WaZYsW.css +0 -1
- package/agent-pane/frontend/dist/assets/index-y7xlT86e.js +0 -362
- package/agent-pane/frontend/dist/index.html +0 -19
- package/agent-pane/frontend/index.html +0 -18
- package/agent-pane/frontend/package-lock.json +0 -4211
- package/agent-pane/frontend/package.json +0 -35
- package/agent-pane/frontend/src/App.test.tsx +0 -74
- package/agent-pane/frontend/src/App.tsx +0 -44
- package/agent-pane/frontend/src/AppInitializer.test.tsx +0 -142
- package/agent-pane/frontend/src/AppInitializer.tsx +0 -46
- package/agent-pane/frontend/src/__tests__/ThreadsPanel.test.tsx +0 -192
- package/agent-pane/frontend/src/api/client.test.ts +0 -126
- package/agent-pane/frontend/src/api/client.ts +0 -34
- package/agent-pane/frontend/src/api/models.ts +0 -7
- package/agent-pane/frontend/src/api/providers.ts +0 -17
- package/agent-pane/frontend/src/api/sessions.test.ts +0 -245
- package/agent-pane/frontend/src/api/sessions.ts +0 -97
- package/agent-pane/frontend/src/api/skills.ts +0 -12
- package/agent-pane/frontend/src/api/websocket.test.ts +0 -288
- package/agent-pane/frontend/src/api/websocket.ts +0 -238
- package/agent-pane/frontend/src/api/workspaces.test.ts +0 -112
- package/agent-pane/frontend/src/api/workspaces.ts +0 -38
- package/agent-pane/frontend/src/components/CollapsibleToolCalls.test.tsx +0 -49
- package/agent-pane/frontend/src/components/CollapsibleToolCalls.tsx +0 -56
- package/agent-pane/frontend/src/components/CommandInputBar.test.tsx +0 -133
- package/agent-pane/frontend/src/components/CommandInputBar.tsx +0 -584
- package/agent-pane/frontend/src/components/CommandPalette.test.tsx +0 -226
- package/agent-pane/frontend/src/components/CommandPalette.tsx +0 -194
- package/agent-pane/frontend/src/components/CommentableText.test.tsx +0 -196
- package/agent-pane/frontend/src/components/CommentableText.tsx +0 -379
- package/agent-pane/frontend/src/components/IdeWorkspace.test.tsx +0 -39
- package/agent-pane/frontend/src/components/IdeWorkspace.tsx +0 -51
- package/agent-pane/frontend/src/components/ModelSelector.test.tsx +0 -129
- package/agent-pane/frontend/src/components/ModelSelector.tsx +0 -191
- package/agent-pane/frontend/src/components/NewSessionDialog.test.tsx +0 -95
- package/agent-pane/frontend/src/components/NewSessionDialog.tsx +0 -129
- package/agent-pane/frontend/src/components/RenderArea.test.tsx +0 -160
- package/agent-pane/frontend/src/components/RenderArea.tsx +0 -170
- package/agent-pane/frontend/src/components/ResizeHandle.tsx +0 -29
- package/agent-pane/frontend/src/components/RightPanel.test.tsx +0 -43
- package/agent-pane/frontend/src/components/RightPanel.tsx +0 -46
- package/agent-pane/frontend/src/components/RuntimeIcon.test.tsx +0 -32
- package/agent-pane/frontend/src/components/RuntimeIcon.tsx +0 -16
- package/agent-pane/frontend/src/components/SessionTabs.test.tsx +0 -72
- package/agent-pane/frontend/src/components/SessionTabs.tsx +0 -49
- package/agent-pane/frontend/src/components/StatusBar.test.tsx +0 -56
- package/agent-pane/frontend/src/components/StatusBar.tsx +0 -129
- package/agent-pane/frontend/src/components/StatusDot.test.tsx +0 -25
- package/agent-pane/frontend/src/components/StatusDot.tsx +0 -8
- package/agent-pane/frontend/src/components/Toast.test.tsx +0 -79
- package/agent-pane/frontend/src/components/Toast.tsx +0 -76
- package/agent-pane/frontend/src/components/TurnContent.test.tsx +0 -298
- package/agent-pane/frontend/src/components/TurnContent.tsx +0 -548
- package/agent-pane/frontend/src/components/WorkspaceSidebar.test.tsx +0 -297
- package/agent-pane/frontend/src/components/WorkspaceSidebar.tsx +0 -351
- package/agent-pane/frontend/src/components/input/OptionSubPopup.tsx +0 -32
- package/agent-pane/frontend/src/components/input/SkillMentionPopup.test.tsx +0 -63
- package/agent-pane/frontend/src/components/input/SkillMentionPopup.tsx +0 -78
- package/agent-pane/frontend/src/components/input/SlashPopup.test.tsx +0 -96
- package/agent-pane/frontend/src/components/input/SlashPopup.tsx +0 -82
- package/agent-pane/frontend/src/components/panels/ChangesPanel.test.tsx +0 -61
- package/agent-pane/frontend/src/components/panels/ChangesPanel.tsx +0 -54
- package/agent-pane/frontend/src/components/panels/TasksPanel.test.tsx +0 -97
- package/agent-pane/frontend/src/components/panels/TasksPanel.tsx +0 -208
- package/agent-pane/frontend/src/components/panels/ThreadsPanel.tsx +0 -382
- package/agent-pane/frontend/src/components/panels/TurnsPanel.test.tsx +0 -83
- package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +0 -126
- package/agent-pane/frontend/src/constants/models.test.ts +0 -97
- package/agent-pane/frontend/src/constants/models.ts +0 -166
- package/agent-pane/frontend/src/hooks/useEscapeKey.test.ts +0 -42
- package/agent-pane/frontend/src/hooks/useEscapeKey.ts +0 -13
- package/agent-pane/frontend/src/hooks/useOutsideClick.test.ts +0 -92
- package/agent-pane/frontend/src/hooks/useOutsideClick.ts +0 -19
- package/agent-pane/frontend/src/hooks/useProvider.ts +0 -44
- package/agent-pane/frontend/src/hooks/useResizable.test.ts +0 -97
- package/agent-pane/frontend/src/hooks/useResizable.ts +0 -75
- package/agent-pane/frontend/src/hooks/useSessionStatus.test.ts +0 -116
- package/agent-pane/frontend/src/hooks/useSessionStatus.ts +0 -34
- package/agent-pane/frontend/src/main.test.tsx +0 -37
- package/agent-pane/frontend/src/main.tsx +0 -10
- package/agent-pane/frontend/src/store/sessionSlice.ts +0 -440
- package/agent-pane/frontend/src/store/store.test.ts +0 -1319
- package/agent-pane/frontend/src/store/store.ts +0 -37
- package/agent-pane/frontend/src/store/threadSlice.ts +0 -136
- package/agent-pane/frontend/src/store/uiSlice.ts +0 -133
- package/agent-pane/frontend/src/store/workspaceSlice.ts +0 -77
- package/agent-pane/frontend/src/store/wsSlice.ts +0 -361
- package/agent-pane/frontend/src/styles/app.css +0 -611
- package/agent-pane/frontend/src/styles/tokens.css +0 -62
- package/agent-pane/frontend/src/test/component-helpers.ts +0 -38
- package/agent-pane/frontend/src/test/fixtures.ts +0 -100
- package/agent-pane/frontend/src/test-setup.ts +0 -5
- package/agent-pane/frontend/src/types/index.test.ts +0 -128
- package/agent-pane/frontend/src/types/index.ts +0 -132
- package/agent-pane/frontend/src/utils/changes.test.ts +0 -81
- package/agent-pane/frontend/src/utils/changes.ts +0 -39
- package/agent-pane/frontend/src/utils/duration.test.ts +0 -75
- package/agent-pane/frontend/src/utils/duration.ts +0 -14
- package/agent-pane/frontend/tsconfig.json +0 -25
- package/agent-pane/frontend/vite.config.ts +0 -22
- package/agent-pane/frontend/vitest.config.ts +0 -29
- package/agent-pane/package-lock.json +0 -1527
- package/agent-pane/package.json +0 -19
- package/agent-pane/shared/provider-types.ts +0 -67
- package/agent-pane/shared/types.ts +0 -167
|
@@ -1,1319 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Claude provider — implements the RuntimeProvider interface.
|
|
3
|
-
*
|
|
4
|
-
* Uses `@anthropic-ai/claude-agent-sdk` `query()` function in **streaming-input
|
|
5
|
-
* mode** — a single `query()` per session, fed by an async iterable of
|
|
6
|
-
* `SDKUserMessage` objects. This keeps the CLI process alive between turns,
|
|
7
|
-
* which is required for Monitor background tasks to survive turn boundaries.
|
|
8
|
-
*
|
|
9
|
-
* Same WebSocket event protocol as Codex:
|
|
10
|
-
* turn:started, item:started, item:completed, item:delta,
|
|
11
|
-
* turn:completed, turn:tokenUsage, turn:failed, turn:cancelled
|
|
12
|
-
*
|
|
13
|
-
* Frontend needs zero changes.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { query, forkSession as sdkForkSession } from '@anthropic-ai/claude-agent-sdk';
|
|
17
|
-
import type { SDKMessage, SDKUserMessage, Options, ModelInfo as SdkModelInfo, Query, SlashCommand as SdkSlashCommand } from '@anthropic-ai/claude-agent-sdk';
|
|
18
|
-
import type { ModelInfo, TokenUsage, ProviderSlashCommand, Attachment, ContextUsage } from '@agentpane/shared';
|
|
19
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
20
|
-
import { readFileSync } from 'fs';
|
|
21
|
-
import { tmpdir } from 'os';
|
|
22
|
-
import { join } from 'path';
|
|
23
|
-
import type Database from 'better-sqlite3';
|
|
24
|
-
import { getDb } from '../db/index.ts';
|
|
25
|
-
import { broadcast } from '../ws/index.ts';
|
|
26
|
-
import { mapAssistantContent, mapPartialDelta, mapUsage, applyToolResult, type ItemDescriptor, type ToolResultLike } from './claude-message-mapper.ts';
|
|
27
|
-
import { EFFORT_LABELS, type ClaudeSessionEntry } from './claude-types.ts';
|
|
28
|
-
import type { Session, Turn, Item, ItemStatus } from '../db/schema.ts';
|
|
29
|
-
import type { RuntimeProvider } from '../providers/runtime-provider.ts';
|
|
30
|
-
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// PromptQueue — async iterable that feeds user messages to the CLI
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
/** Async iterable queue of `SDKUserMessage` objects.
|
|
36
|
-
*
|
|
37
|
-
* The `query()` function receives this as its `prompt` argument. Each
|
|
38
|
-
* `push()` enqueues a message and resolves the pending `next()` call from
|
|
39
|
-
* the SDK's internal consumer. `close()` signals end-of-iteration, which
|
|
40
|
-
* causes the CLI process to exit gracefully.
|
|
41
|
-
*
|
|
42
|
-
* This is what keeps the CLI alive between turns: the SDK's for-await loop
|
|
43
|
-
* blocks on `next()` waiting for the next user message, so the process
|
|
44
|
-
* stays resident and background tasks (Monitor) keep running. */
|
|
45
|
-
class PromptQueue {
|
|
46
|
-
private buffer: SDKUserMessage[] = [];
|
|
47
|
-
private resolver: ((value: IteratorResult<SDKUserMessage>) => void) | null = null;
|
|
48
|
-
private done = false;
|
|
49
|
-
|
|
50
|
-
push(message: SDKUserMessage): void {
|
|
51
|
-
if (this.done) return;
|
|
52
|
-
if (this.resolver) {
|
|
53
|
-
const r = this.resolver;
|
|
54
|
-
this.resolver = null;
|
|
55
|
-
r({ value: message, done: false });
|
|
56
|
-
} else {
|
|
57
|
-
this.buffer.push(message);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
close(): void {
|
|
62
|
-
this.done = true;
|
|
63
|
-
if (this.resolver) {
|
|
64
|
-
const r = this.resolver;
|
|
65
|
-
this.resolver = null;
|
|
66
|
-
r({ value: undefined, done: true });
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
[Symbol.asyncIterator](): AsyncIterator<SDKUserMessage> {
|
|
71
|
-
return {
|
|
72
|
-
next: (): Promise<IteratorResult<SDKUserMessage>> => {
|
|
73
|
-
if (this.buffer.length > 0) {
|
|
74
|
-
return Promise.resolve({ value: this.buffer.shift()!, done: false });
|
|
75
|
-
}
|
|
76
|
-
if (this.done) {
|
|
77
|
-
return Promise.resolve({ value: undefined, done: true });
|
|
78
|
-
}
|
|
79
|
-
return new Promise((resolve) => {
|
|
80
|
-
this.resolver = resolve;
|
|
81
|
-
});
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
// Per-session state
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
|
|
91
|
-
/** Per-session Claude query instances. */
|
|
92
|
-
const sessionMap = new Map<string, ClaudeSessionEntry>();
|
|
93
|
-
|
|
94
|
-
/** Sessions currently generating titles — prevents duplicate throwaway queries. */
|
|
95
|
-
const titleGenerating = new Set<string>();
|
|
96
|
-
|
|
97
|
-
/** Cached model list from the SDK. */
|
|
98
|
-
let cachedModels: ModelInfo[] | null = null;
|
|
99
|
-
|
|
100
|
-
/** Cached provider-native slash commands from the SDK (e.g. /compact, /clear). */
|
|
101
|
-
let cachedCommands: ProviderSlashCommand[] | null = null;
|
|
102
|
-
|
|
103
|
-
/** Accumulated thinking_delta text per assistant message UUID → content block index → text.
|
|
104
|
-
* Stream events arrive before the final assistant message; the thinking block in the
|
|
105
|
-
* assistant message is signed-empty, so we accumulate the real text from thinking_delta
|
|
106
|
-
* events and feed it to mapAssistantContent. */
|
|
107
|
-
const thinkingAccumulator = new Map<string, Map<number, string>>();
|
|
108
|
-
|
|
109
|
-
/** Anthropic message ID captured from `message_start` stream events, keyed by turnId.
|
|
110
|
-
* Each SDKPartialAssistantMessage has a unique uuid (different per event), but the
|
|
111
|
-
* Anthropic message ID from `message_start` is stable across all stream events for one
|
|
112
|
-
* assistant message. We use it as the itemId for text delta broadcasts so the frontend
|
|
113
|
-
* accumulates into one item instead of creating N separate items. */
|
|
114
|
-
const streamingMessageIds = new Map<string, string>();
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// ---------------------------------------------------------------------------
|
|
119
|
-
// DB helpers
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
|
|
122
|
-
/** Update the running state and status of a session. */
|
|
123
|
-
function setSessionStatus(
|
|
124
|
-
db: Database.Database,
|
|
125
|
-
sessionId: string,
|
|
126
|
-
isRunning: 0 | 1,
|
|
127
|
-
status: string,
|
|
128
|
-
now: string,
|
|
129
|
-
hasUnreviewed = false,
|
|
130
|
-
): void {
|
|
131
|
-
if (hasUnreviewed) {
|
|
132
|
-
db.prepare('UPDATE sessions SET is_running = ?, status = ?, has_unreviewed = 1, updated_at = ? WHERE id = ?').run(
|
|
133
|
-
isRunning, status, now, sessionId,
|
|
134
|
-
);
|
|
135
|
-
} else {
|
|
136
|
-
db.prepare('UPDATE sessions SET is_running = ?, status = ?, updated_at = ? WHERE id = ?').run(
|
|
137
|
-
isRunning, status, now, sessionId,
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
|
-
// ensureSessionStarted — create streaming-input query for a session
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
|
|
146
|
-
/** Create (or reuse) a streaming-input query for a session.
|
|
147
|
-
*
|
|
148
|
-
* Builds options from the session's DB row (cwd, model, permissionMode, resume,
|
|
149
|
-
* etc.), creates a `PromptQueue`, calls `query({ prompt: queue, options })`,
|
|
150
|
-
* and starts a background message loop that dispatches SDK messages to
|
|
151
|
-
* `handleMessage` using the current `entry.turnId`.
|
|
152
|
-
*
|
|
153
|
-
* The CLI process stays alive as long as the PromptQueue is not closed —
|
|
154
|
-
* this is what allows Monitor background tasks to survive between turns. */
|
|
155
|
-
async function ensureSessionStarted(
|
|
156
|
-
session: Session,
|
|
157
|
-
mapKey?: string,
|
|
158
|
-
resumeSessionId?: string,
|
|
159
|
-
): Promise<ClaudeSessionEntry> {
|
|
160
|
-
const db = getDb();
|
|
161
|
-
const key = mapKey ?? session.id;
|
|
162
|
-
// Main session (no mapKey) falls back to its persisted external_thread_id so it
|
|
163
|
-
// resumes the existing conversation. Thread (forked) entries must NOT fall back
|
|
164
|
-
// to the main session's external_thread_id — that would append the thread's
|
|
165
|
-
// messages to the MAIN session transcript (resume appends to the same file).
|
|
166
|
-
// An explicit undefined resume for a thread starts a fresh isolated session.
|
|
167
|
-
const resumeId = mapKey !== undefined ? resumeSessionId : (resumeSessionId ?? session.external_thread_id ?? undefined);
|
|
168
|
-
|
|
169
|
-
// Build options — same as the old per-turn options, but applied once per session.
|
|
170
|
-
const options: Options = {
|
|
171
|
-
cwd: session.cwd ?? process.cwd(),
|
|
172
|
-
model: session.model ?? undefined,
|
|
173
|
-
permissionMode: 'bypassPermissions',
|
|
174
|
-
allowDangerouslySkipPermissions: true,
|
|
175
|
-
includePartialMessages: true,
|
|
176
|
-
...(process.env.CLAUDE_BIN ? { pathToClaudeCodeExecutable: process.env.CLAUDE_BIN } : {}),
|
|
177
|
-
...(session.model_reasoning_effort ? { effort: session.model_reasoning_effort as Options['effort'] } : {}),
|
|
178
|
-
...(resumeId ? { resume: resumeId } : {}),
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const promptQueue = new PromptQueue();
|
|
182
|
-
const q: Query = query({ prompt: promptQueue, options });
|
|
183
|
-
|
|
184
|
-
const entry: ClaudeSessionEntry = {
|
|
185
|
-
query: q,
|
|
186
|
-
sessionId: null,
|
|
187
|
-
turnId: null,
|
|
188
|
-
cwd: session.cwd ?? process.cwd(),
|
|
189
|
-
promptQueue,
|
|
190
|
-
messageLoop: null,
|
|
191
|
-
};
|
|
192
|
-
sessionMap.set(key, entry);
|
|
193
|
-
|
|
194
|
-
// Background message loop — runs for the lifetime of the CLI process.
|
|
195
|
-
// Dispatches each SDK message to handleMessage using the current entry.turnId.
|
|
196
|
-
entry.messageLoop = (async () => {
|
|
197
|
-
try {
|
|
198
|
-
for await (const message of q) {
|
|
199
|
-
const currentTurnId = entry.turnId;
|
|
200
|
-
if (!currentTurnId) continue; // no active turn — skip (shouldn't happen normally)
|
|
201
|
-
await handleMessage(message, session.id, currentTurnId, entry, db);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Generator ended (PromptQueue was closed via clearThread).
|
|
205
|
-
// Complete the current turn if it's still running.
|
|
206
|
-
if (entry.turnId) {
|
|
207
|
-
const currentTurn = db.prepare('SELECT status, token_usage FROM turns WHERE id = ?').get(entry.turnId) as
|
|
208
|
-
| { status: string; token_usage: string | null }
|
|
209
|
-
| undefined;
|
|
210
|
-
if (currentTurn && currentTurn.status === 'running') {
|
|
211
|
-
const usage = currentTurn.token_usage ? (JSON.parse(currentTurn.token_usage) as TokenUsage) : null;
|
|
212
|
-
completeTurn(db, session.id, entry.turnId, usage);
|
|
213
|
-
}
|
|
214
|
-
syncThreadTitle(session.id);
|
|
215
|
-
}
|
|
216
|
-
} catch (err) {
|
|
217
|
-
const errMsg = (err as Error).message;
|
|
218
|
-
console.error(`[claude] message loop error for session ${session.id}:`, errMsg);
|
|
219
|
-
if (entry.turnId) {
|
|
220
|
-
const now = new Date().toISOString();
|
|
221
|
-
// Mark ALL in_progress items for this session as failed — the CLI died so
|
|
222
|
-
// no background task (Monitor/Subagent) can still be running, even ones
|
|
223
|
-
// started in earlier completed turns.
|
|
224
|
-
const staleItems = db.prepare(
|
|
225
|
-
"UPDATE items SET status = 'failed', updated_at = ? WHERE status = 'in_progress' AND turn_id IN (SELECT id FROM turns WHERE session_id = ?)",
|
|
226
|
-
).run(now, session.id) as { changes: number };
|
|
227
|
-
|
|
228
|
-
db.prepare('UPDATE turns SET status = ?, error = ?, updated_at = ? WHERE id = ?').run('failed', errMsg, now, entry.turnId);
|
|
229
|
-
setSessionStatus(db, session.id, 0, 'failed', now);
|
|
230
|
-
broadcast('turn:failed', { sessionId: session.id, turnId: entry.turnId, error: errMsg });
|
|
231
|
-
|
|
232
|
-
// Broadcast item:completed for each stale item so the frontend updates.
|
|
233
|
-
if (staleItems.changes > 0) {
|
|
234
|
-
const staleRows = db.prepare(
|
|
235
|
-
"SELECT id, turn_id, external_item_id, type, data, created_at FROM items WHERE turn_id IN (SELECT id FROM turns WHERE session_id = ?) AND status = 'failed' AND updated_at = ?",
|
|
236
|
-
).all(session.id, now) as { id: string; turn_id: string; external_item_id: string; type: string; data: string | null; created_at: string }[];
|
|
237
|
-
for (const row of staleRows) {
|
|
238
|
-
broadcast('item:completed', {
|
|
239
|
-
sessionId: session.id,
|
|
240
|
-
turnId: row.turn_id,
|
|
241
|
-
item: {
|
|
242
|
-
id: row.id,
|
|
243
|
-
turn_id: row.turn_id,
|
|
244
|
-
external_item_id: row.external_item_id,
|
|
245
|
-
type: row.type,
|
|
246
|
-
data: row.data ? JSON.parse(row.data) : {},
|
|
247
|
-
status: 'failed',
|
|
248
|
-
created_at: row.created_at,
|
|
249
|
-
updated_at: now,
|
|
250
|
-
},
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
} finally {
|
|
256
|
-
// Clean up streaming state
|
|
257
|
-
if (entry.turnId) {
|
|
258
|
-
streamingMessageIds.delete(entry.turnId);
|
|
259
|
-
}
|
|
260
|
-
sessionMap.delete(key);
|
|
261
|
-
}
|
|
262
|
-
})();
|
|
263
|
-
|
|
264
|
-
return entry;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// ---------------------------------------------------------------------------
|
|
268
|
-
// startTurn
|
|
269
|
-
// ---------------------------------------------------------------------------
|
|
270
|
-
|
|
271
|
-
/** Start a Claude turn using the streaming-input query.
|
|
272
|
-
*
|
|
273
|
-
* 1. Ensure the session's streaming query is running (create if needed).
|
|
274
|
-
* 2. Complete the previous turn if it's still running.
|
|
275
|
-
* 3. Set the new turn as active.
|
|
276
|
-
* 4. Push the user message to the PromptQueue — the SDK picks it up and
|
|
277
|
-
* starts generating. No new `query()` call is made. */
|
|
278
|
-
async function startTurn(session: Session, turn: Turn): Promise<void> {
|
|
279
|
-
const db = getDb();
|
|
280
|
-
const now = new Date().toISOString();
|
|
281
|
-
|
|
282
|
-
// Get or create the streaming-input session entry.
|
|
283
|
-
// Thread turns use a composite key so they get their own forked Claude session.
|
|
284
|
-
const mapKey = turn.thread_id ? `${session.id}:thread:${turn.thread_id}` : session.id;
|
|
285
|
-
let entry = sessionMap.get(mapKey);
|
|
286
|
-
if (!entry) {
|
|
287
|
-
try {
|
|
288
|
-
if (turn.thread_id) {
|
|
289
|
-
// Thread turn — resume from the thread's OWN forked session id. Do NOT
|
|
290
|
-
// fall back to the main session's external_thread_id: if the fork failed
|
|
291
|
-
// (thread has no external_thread_id), start a FRESH isolated session
|
|
292
|
-
// instead, so the thread never appends to the main session transcript.
|
|
293
|
-
const thread = db
|
|
294
|
-
.prepare('SELECT external_thread_id FROM discussion_threads WHERE id = ?')
|
|
295
|
-
.get(turn.thread_id) as { external_thread_id: string | null } | undefined;
|
|
296
|
-
entry = await ensureSessionStarted(session, mapKey, thread?.external_thread_id ?? undefined);
|
|
297
|
-
} else {
|
|
298
|
-
entry = await ensureSessionStarted(session);
|
|
299
|
-
}
|
|
300
|
-
} catch (err) {
|
|
301
|
-
const errMsg = `Failed to start Claude query: ${(err as Error).message}`;
|
|
302
|
-
const now2 = new Date().toISOString();
|
|
303
|
-
db.prepare('UPDATE turns SET status = ?, error = ?, updated_at = ? WHERE id = ?').run('failed', errMsg, now2, turn.id);
|
|
304
|
-
setSessionStatus(db, session.id, 0, 'failed', now2);
|
|
305
|
-
broadcast('turn:failed', { sessionId: session.id, turnId: turn.id, error: errMsg });
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Tag thread entries so the system/init handler can avoid corrupting the
|
|
311
|
-
// main session's external_thread_id with the fork's session id. Set on every
|
|
312
|
-
// startTurn (covers both freshly-created and reused sessionMap entries).
|
|
313
|
-
entry.threadId = turn.thread_id ?? null;
|
|
314
|
-
|
|
315
|
-
// Complete previous turn if still running (e.g. Monitor was active).
|
|
316
|
-
// All messages between user prompts belong to one AgentPane turn —
|
|
317
|
-
// Monitor-triggered internal turns do NOT create separate AgentPane turns.
|
|
318
|
-
if (entry.turnId) {
|
|
319
|
-
const prevTurn = db.prepare('SELECT status, token_usage FROM turns WHERE id = ?').get(entry.turnId) as
|
|
320
|
-
| { status: string; token_usage: string | null }
|
|
321
|
-
| undefined;
|
|
322
|
-
if (prevTurn && prevTurn.status === 'running') {
|
|
323
|
-
const usage = prevTurn.token_usage ? (JSON.parse(prevTurn.token_usage) as TokenUsage) : null;
|
|
324
|
-
completeTurn(db, session.id, entry.turnId, usage);
|
|
325
|
-
}
|
|
326
|
-
syncThreadTitle(session.id);
|
|
327
|
-
streamingMessageIds.delete(entry.turnId);
|
|
328
|
-
}
|
|
329
|
-
entry.turnId = turn.id;
|
|
330
|
-
db.prepare('UPDATE turns SET status = ?, updated_at = ? WHERE id = ?').run('running', now, turn.id);
|
|
331
|
-
setSessionStatus(db, session.id, 1, 'running', now);
|
|
332
|
-
broadcast('turn:started', { sessionId: session.id, turnId: turn.id });
|
|
333
|
-
|
|
334
|
-
// Push the user message to the PromptQueue — the SDK picks it up and starts generating.
|
|
335
|
-
// No new query() call — the CLI process is already alive.
|
|
336
|
-
// Build content blocks if attachments exist (images, PDFs, text files).
|
|
337
|
-
// turn.attachments is stored as a JSON string in the DB — parse it.
|
|
338
|
-
let content: string | SDKUserMessage['message']['content'];
|
|
339
|
-
const rawAttachments = turn.attachments as unknown as string | null;
|
|
340
|
-
let attachments: Attachment[] | null = null;
|
|
341
|
-
if (rawAttachments) {
|
|
342
|
-
try {
|
|
343
|
-
attachments = JSON.parse(rawAttachments) as Attachment[];
|
|
344
|
-
} catch {
|
|
345
|
-
attachments = null;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
if (attachments && attachments.length > 0) {
|
|
349
|
-
const blocks: NonNullable<SDKUserMessage['message']['content']> = [{ type: 'text', text: turn.user_prompt }];
|
|
350
|
-
for (const att of attachments) {
|
|
351
|
-
if (att.media_type.startsWith('image/')) {
|
|
352
|
-
blocks.push({
|
|
353
|
-
type: 'image',
|
|
354
|
-
source: { type: 'base64', media_type: att.media_type as 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp', data: att.data },
|
|
355
|
-
});
|
|
356
|
-
} else if (att.media_type === 'application/pdf') {
|
|
357
|
-
blocks.push({
|
|
358
|
-
type: 'document',
|
|
359
|
-
source: { type: 'base64', media_type: att.media_type as 'application/pdf', data: att.data },
|
|
360
|
-
});
|
|
361
|
-
} else {
|
|
362
|
-
// Text files: inline as additional text blocks
|
|
363
|
-
const decoded = Buffer.from(att.data, 'base64').toString('utf-8');
|
|
364
|
-
blocks.push({ type: 'text', text: `\n--- ${att.name} ---\n${decoded}\n--- end ---` });
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
content = blocks;
|
|
368
|
-
} else {
|
|
369
|
-
content = turn.user_prompt; // plain string (backward compatible)
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
entry.promptQueue.push({
|
|
373
|
-
type: 'user',
|
|
374
|
-
message: { role: 'user', content },
|
|
375
|
-
parent_tool_use_id: null,
|
|
376
|
-
} as SDKUserMessage);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// ---------------------------------------------------------------------------
|
|
380
|
-
// handleMessage — dispatch SDK messages to the correct handler
|
|
381
|
-
// ---------------------------------------------------------------------------
|
|
382
|
-
|
|
383
|
-
/** Handle a single SDK message — dispatch to the correct handler. */
|
|
384
|
-
export async function handleMessage(
|
|
385
|
-
message: SDKMessage,
|
|
386
|
-
sessionId: string,
|
|
387
|
-
turnId: string,
|
|
388
|
-
entry: ClaudeSessionEntry,
|
|
389
|
-
db: Database.Database,
|
|
390
|
-
): Promise<void> {
|
|
391
|
-
switch (message.type) {
|
|
392
|
-
case 'system': {
|
|
393
|
-
if ('subtype' in message) {
|
|
394
|
-
const sub = (message as { subtype: string }).subtype;
|
|
395
|
-
if (sub === 'init') {
|
|
396
|
-
// Always capture the Claude session id (needed for Monitor output-path
|
|
397
|
-
// construction). Idempotent — Monitor triggers multiple inits.
|
|
398
|
-
entry.sessionId = (message as { session_id: string }).session_id;
|
|
399
|
-
// Only persist the resume id for the MAIN session. For thread (forked)
|
|
400
|
-
// entries, the fork's session id already lives in
|
|
401
|
-
// discussion_threads.external_thread_id; writing it here would corrupt
|
|
402
|
-
// the MAIN session's external_thread_id, causing the main session to
|
|
403
|
-
// resume the fork after a restart/rewind and "remember" the thread's
|
|
404
|
-
// discussion.
|
|
405
|
-
if (!entry.threadId) {
|
|
406
|
-
const now = new Date().toISOString();
|
|
407
|
-
db.prepare('UPDATE sessions SET external_thread_id = ?, updated_at = ? WHERE id = ?').run(
|
|
408
|
-
(message as { session_id: string }).session_id, now, sessionId,
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
} else if (sub === 'compact_boundary') {
|
|
412
|
-
handleCompactBoundary(message as SDKMessage & { type: 'system'; subtype: 'compact_boundary' }, sessionId, turnId, db);
|
|
413
|
-
} else if (sub === 'task_started') {
|
|
414
|
-
handleTaskStarted(message as SDKMessage & { type: 'system'; subtype: 'task_started' }, sessionId, turnId, entry, db);
|
|
415
|
-
} else if (sub === 'task_progress') {
|
|
416
|
-
handleTaskProgress(message as SDKMessage & { type: 'system'; subtype: 'task_progress' }, sessionId, turnId, entry, db);
|
|
417
|
-
} else if (sub === 'task_updated') {
|
|
418
|
-
handleTaskUpdated(message as SDKMessage & { type: 'system'; subtype: 'task_updated' }, sessionId, turnId, db);
|
|
419
|
-
} else if (sub === 'task_notification') {
|
|
420
|
-
handleTaskNotification(message as SDKMessage & { type: 'system'; subtype: 'task_notification' }, sessionId, turnId, entry, db);
|
|
421
|
-
} else if (sub === 'background_tasks_changed') {
|
|
422
|
-
handleBackgroundTasksChanged(message as SDKMessage & { type: 'system'; subtype: 'background_tasks_changed' }, sessionId);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
break;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
case 'assistant': {
|
|
429
|
-
handleAssistantMessage(message, sessionId, turnId, db);
|
|
430
|
-
break;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
case 'user': {
|
|
434
|
-
handleUserMessage(message, sessionId, turnId, db);
|
|
435
|
-
break;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
case 'stream_event': {
|
|
439
|
-
handleStreamEvent(message, sessionId, turnId);
|
|
440
|
-
break;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
case 'result': {
|
|
444
|
-
handleResultMessage(message, sessionId, turnId, entry, db);
|
|
445
|
-
break;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
default:
|
|
449
|
-
break;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// ---------------------------------------------------------------------------
|
|
454
|
-
// Item / message handlers (unchanged from one-shot mode)
|
|
455
|
-
// ---------------------------------------------------------------------------
|
|
456
|
-
|
|
457
|
-
/** Item types produced from a tool_use block — they start `in_progress` and only
|
|
458
|
-
* complete when the matching tool_result arrives (handled in handleUserMessage). */
|
|
459
|
-
const TOOL_CALL_TYPES = new Set(['command_execution', 'file_change', 'mcp_tool_call']);
|
|
460
|
-
|
|
461
|
-
/** Handle SDKAssistantMessage — map content blocks to items.
|
|
462
|
-
* Text/thinking blocks are complete on arrival; tool_use blocks are in_progress
|
|
463
|
-
* until their tool_result is delivered by a later SDKUserMessage. */
|
|
464
|
-
function handleAssistantMessage(
|
|
465
|
-
message: SDKMessage & { type: 'assistant' },
|
|
466
|
-
sessionId: string,
|
|
467
|
-
turnId: string,
|
|
468
|
-
db: Database.Database,
|
|
469
|
-
): void {
|
|
470
|
-
const messageId = message.uuid;
|
|
471
|
-
const parentToolUseId = (message as { parent_tool_use_id?: string | null }).parent_tool_use_id ?? null;
|
|
472
|
-
|
|
473
|
-
// Subagent assistant message — don't create items in the parent transcript.
|
|
474
|
-
// Subagent progress is shown via background_task items (task_started/progress/notification).
|
|
475
|
-
// t3code approach: early return, only update external_turn_id for session forking.
|
|
476
|
-
if (parentToolUseId) {
|
|
477
|
-
const thinkingTexts = thinkingAccumulator.get(messageId);
|
|
478
|
-
if (thinkingTexts) thinkingAccumulator.delete(messageId);
|
|
479
|
-
const nowExt = new Date().toISOString();
|
|
480
|
-
db.prepare('UPDATE turns SET external_turn_id = ?, updated_at = ? WHERE id = ?').run(messageId, nowExt, turnId);
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// Main agent — normal handling
|
|
485
|
-
const content = message.message.content;
|
|
486
|
-
const anthropicMsgId = message.message?.id ?? messageId;
|
|
487
|
-
const thinkingTexts = thinkingAccumulator.get(messageId);
|
|
488
|
-
const descriptors = mapAssistantContent(content, messageId, thinkingTexts);
|
|
489
|
-
// Override externalId for agent_message items to match the streaming itemId
|
|
490
|
-
// (Anthropic message ID captured from message_start event, not the per-event SDK uuid).
|
|
491
|
-
for (const desc of descriptors) {
|
|
492
|
-
if (desc.type === 'agent_message') desc.externalId = anthropicMsgId;
|
|
493
|
-
}
|
|
494
|
-
if (thinkingTexts) thinkingAccumulator.delete(messageId);
|
|
495
|
-
streamingMessageIds.delete(turnId);
|
|
496
|
-
|
|
497
|
-
for (const desc of descriptors) {
|
|
498
|
-
const isToolCall = TOOL_CALL_TYPES.has(desc.type);
|
|
499
|
-
const status: ItemStatus = isToolCall ? 'in_progress' : 'completed';
|
|
500
|
-
const broadcastEvent = isToolCall ? 'item:started' : 'item:completed';
|
|
501
|
-
upsertItem(db, desc, sessionId, turnId, status, broadcastEvent);
|
|
502
|
-
|
|
503
|
-
// Update turn-level fields only for completed content blocks from the main agent
|
|
504
|
-
if (!isToolCall) {
|
|
505
|
-
const now = new Date().toISOString();
|
|
506
|
-
if (desc.type === 'agent_message') {
|
|
507
|
-
const text = (desc.data as { text: string }).text;
|
|
508
|
-
db.prepare('UPDATE turns SET agent_response = ?, updated_at = ? WHERE id = ?').run(text, now, turnId);
|
|
509
|
-
}
|
|
510
|
-
if (desc.type === 'reasoning') {
|
|
511
|
-
const text = (desc.data as { text: string }).text;
|
|
512
|
-
if (text) {
|
|
513
|
-
db.prepare('UPDATE turns SET thoughts = ?, updated_at = ? WHERE id = ?').run(text, now, turnId);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Store the SDK assistant message UUID as external_turn_id — used as upToMessageId
|
|
520
|
-
// by forkSession() when branching from this turn.
|
|
521
|
-
const nowExt = new Date().toISOString();
|
|
522
|
-
db.prepare('UPDATE turns SET external_turn_id = ?, updated_at = ? WHERE id = ?').run(messageId, nowExt, turnId);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/** Handle SDKUserMessage — tool_result blocks carry the output of a prior tool_use.
|
|
526
|
-
* Match each tool_result to the existing item by external_item_id (tool_use_id),
|
|
527
|
-
* apply the result to the item's data, and mark it completed/failed. */
|
|
528
|
-
function handleUserMessage(
|
|
529
|
-
message: SDKMessage & { type: 'user' },
|
|
530
|
-
sessionId: string,
|
|
531
|
-
turnId: string,
|
|
532
|
-
db: Database.Database,
|
|
533
|
-
): void {
|
|
534
|
-
const parentToolUseId = (message as { parent_tool_use_id?: string | null }).parent_tool_use_id ?? null;
|
|
535
|
-
|
|
536
|
-
// Subagent tool results — skip entirely. The matching tool_use item was never
|
|
537
|
-
// created (handleAssistantMessage early-returns for subagent messages), so
|
|
538
|
-
// there's nothing to update.
|
|
539
|
-
if (parentToolUseId) return;
|
|
540
|
-
|
|
541
|
-
const content = message.message.content;
|
|
542
|
-
if (!Array.isArray(content)) return;
|
|
543
|
-
|
|
544
|
-
for (const block of content) {
|
|
545
|
-
if (typeof block !== 'object' || block === null) continue;
|
|
546
|
-
const b = block as { type?: string; tool_use_id?: string; is_error?: boolean; content?: unknown };
|
|
547
|
-
if (b.type !== 'tool_result' || !b.tool_use_id) continue;
|
|
548
|
-
|
|
549
|
-
const toolUseId = b.tool_use_id;
|
|
550
|
-
const isError = !!b.is_error;
|
|
551
|
-
|
|
552
|
-
// Find the item created from the matching tool_use block.
|
|
553
|
-
const row = db
|
|
554
|
-
.prepare('SELECT type, data FROM items WHERE external_item_id = ? AND turn_id = ?')
|
|
555
|
-
.get(toolUseId, turnId) as { type: string; data: string | null } | undefined;
|
|
556
|
-
if (!row) continue;
|
|
557
|
-
|
|
558
|
-
const existing = row.data ? (JSON.parse(row.data) as Record<string, unknown>) : {};
|
|
559
|
-
const updatedData = applyToolResult(row.type, existing, b as ToolResultLike, message.tool_use_result, isError);
|
|
560
|
-
|
|
561
|
-
upsertItem(
|
|
562
|
-
db,
|
|
563
|
-
{ type: row.type, data: updatedData, externalId: toolUseId },
|
|
564
|
-
sessionId,
|
|
565
|
-
turnId,
|
|
566
|
-
isError ? 'failed' : 'completed',
|
|
567
|
-
'item:completed',
|
|
568
|
-
);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
function handleStreamEvent(
|
|
572
|
-
message: SDKMessage & { type: 'stream_event' },
|
|
573
|
-
sessionId: string,
|
|
574
|
-
turnId: string,
|
|
575
|
-
): void {
|
|
576
|
-
const rawEvent = message.event as {
|
|
577
|
-
type: string;
|
|
578
|
-
message?: { id: string };
|
|
579
|
-
content_block?: { type: string; id?: string };
|
|
580
|
-
};
|
|
581
|
-
|
|
582
|
-
// t3code approach: SDK already provides parent_tool_use_id on subagent stream events.
|
|
583
|
-
// If this stream event belongs to a subagent, drop text/thinking deltas so they
|
|
584
|
-
// don't leak into the main session's agent_response.
|
|
585
|
-
const parentToolUseId = (message as { parent_tool_use_id?: string | null }).parent_tool_use_id ?? null;
|
|
586
|
-
|
|
587
|
-
// Capture the Anthropic message ID from message_start events (stable across all stream
|
|
588
|
-
// events for one assistant message, unlike SDKPartialAssistantMessage.uuid which is unique per event).
|
|
589
|
-
// Skip for subagent streams — we don't want to overwrite the main agent's streamingMessageId.
|
|
590
|
-
if (rawEvent.type === 'message_start' && rawEvent.message?.id && !parentToolUseId) {
|
|
591
|
-
streamingMessageIds.set(turnId, rawEvent.message.id);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
const delta = mapPartialDelta(message.event);
|
|
595
|
-
if (!delta) return;
|
|
596
|
-
|
|
597
|
-
// Drop subagent text/thinking deltas — they must not write into the parent transcript.
|
|
598
|
-
if (parentToolUseId) return;
|
|
599
|
-
|
|
600
|
-
// Broadcast text deltas to frontend — use the stable Anthropic message ID as itemId
|
|
601
|
-
// so the frontend accumulates all deltas into one item.
|
|
602
|
-
if (delta.kind === 'text') {
|
|
603
|
-
const msgId = streamingMessageIds.get(turnId) ?? `stream-text-${turnId}`;
|
|
604
|
-
broadcast('item:delta', {
|
|
605
|
-
sessionId,
|
|
606
|
-
turnId,
|
|
607
|
-
itemId: msgId,
|
|
608
|
-
delta: delta.text,
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
// Accumulate thinking_delta text for the final assistant message (which has signed-empty thinking).
|
|
612
|
-
if (delta.kind === 'thinking') {
|
|
613
|
-
let msgMap = thinkingAccumulator.get(message.uuid);
|
|
614
|
-
if (!msgMap) {
|
|
615
|
-
msgMap = new Map();
|
|
616
|
-
thinkingAccumulator.set(message.uuid, msgMap);
|
|
617
|
-
}
|
|
618
|
-
const existing = msgMap.get(delta.index) ?? '';
|
|
619
|
-
msgMap.set(delta.index, existing + delta.text);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/** Handle SDKCompactBoundaryMessage — context was compacted.
|
|
624
|
-
* Insert a `context_compaction` item so the UI can render a "Context compacted" row. */
|
|
625
|
-
function handleCompactBoundary(
|
|
626
|
-
message: SDKMessage & { type: 'system'; subtype: 'compact_boundary' },
|
|
627
|
-
sessionId: string,
|
|
628
|
-
turnId: string,
|
|
629
|
-
db: Database.Database,
|
|
630
|
-
): void {
|
|
631
|
-
const meta = (message.compact_metadata ?? {}) as {
|
|
632
|
-
trigger?: string;
|
|
633
|
-
pre_tokens?: number;
|
|
634
|
-
post_tokens?: number;
|
|
635
|
-
};
|
|
636
|
-
const externalId = `compact-${message.uuid ?? Date.now()}`;
|
|
637
|
-
upsertItem(
|
|
638
|
-
db,
|
|
639
|
-
{
|
|
640
|
-
type: 'context_compaction',
|
|
641
|
-
data: {
|
|
642
|
-
trigger: meta.trigger ?? 'auto',
|
|
643
|
-
pre_tokens: meta.pre_tokens ?? 0,
|
|
644
|
-
post_tokens: meta.post_tokens ?? 0,
|
|
645
|
-
},
|
|
646
|
-
externalId,
|
|
647
|
-
},
|
|
648
|
-
sessionId,
|
|
649
|
-
turnId,
|
|
650
|
-
'completed',
|
|
651
|
-
'item:completed',
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
/** Construct the expected Monitor output file path for a given task.
|
|
656
|
-
*
|
|
657
|
-
* Monitor (task_type=local_bash) writes stdout to a file at a predictable path:
|
|
658
|
-
* {tmpdir}/claude-{uid}/{encoded-cwd}/{claude-session-id}/tasks/{task_id}.output
|
|
659
|
-
*
|
|
660
|
-
* The SDK only exposes `output_file` on `task_notification` (not on `task_started`
|
|
661
|
-
* or `task_progress`), so we construct the path ourselves to read live output
|
|
662
|
-
* during execution. Returns empty string if the Claude session ID is not yet
|
|
663
|
-
* known (entry.sessionId is set on the first `system/init` message). */
|
|
664
|
-
function constructMonitorOutputPath(entry: ClaudeSessionEntry, taskId: string): string {
|
|
665
|
-
if (!entry.sessionId) return '';
|
|
666
|
-
const uid = typeof process.getuid === 'function' ? process.getuid() : 0;
|
|
667
|
-
const encodedCwd = entry.cwd.replace(/\//g, '-');
|
|
668
|
-
return join(tmpdir(), `claude-${uid}`, encodedCwd, entry.sessionId, 'tasks', `${taskId}.output`);
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
/** Handle SDKTaskStartedMessage — a background task (Monitor or subagent) started.
|
|
672
|
-
* Insert a `background_task` item so the UI can show the task lifecycle. */
|
|
673
|
-
function handleTaskStarted(
|
|
674
|
-
message: SDKMessage & { type: 'system'; subtype: 'task_started' },
|
|
675
|
-
sessionId: string,
|
|
676
|
-
turnId: string,
|
|
677
|
-
entry: ClaudeSessionEntry,
|
|
678
|
-
db: Database.Database,
|
|
679
|
-
): void {
|
|
680
|
-
const m = message as unknown as {
|
|
681
|
-
task_id: string;
|
|
682
|
-
description: string;
|
|
683
|
-
task_type?: string;
|
|
684
|
-
subagent_type?: string;
|
|
685
|
-
prompt?: string;
|
|
686
|
-
skip_transcript?: boolean;
|
|
687
|
-
};
|
|
688
|
-
// Respect skip_transcript — ambient tasks should not appear in the inline transcript.
|
|
689
|
-
if (m.skip_transcript) return;
|
|
690
|
-
|
|
691
|
-
// Construct the Monitor output file path for local_bash tasks so we can read
|
|
692
|
-
// live output on task_progress (the SDK only exposes output_file on task_notification).
|
|
693
|
-
const isMonitor = m.task_type === 'local_bash';
|
|
694
|
-
const outputFile = isMonitor ? constructMonitorOutputPath(entry, m.task_id) : null;
|
|
695
|
-
|
|
696
|
-
upsertItem(
|
|
697
|
-
db,
|
|
698
|
-
{
|
|
699
|
-
type: 'background_task',
|
|
700
|
-
data: {
|
|
701
|
-
task_id: m.task_id,
|
|
702
|
-
description: m.description,
|
|
703
|
-
task_type: m.task_type ?? 'unknown',
|
|
704
|
-
subagent_type: m.subagent_type ?? null,
|
|
705
|
-
prompt: m.prompt ?? null,
|
|
706
|
-
status: 'running',
|
|
707
|
-
summary: null,
|
|
708
|
-
output_file: outputFile,
|
|
709
|
-
output: '',
|
|
710
|
-
token_count: null,
|
|
711
|
-
last_tool_name: null,
|
|
712
|
-
},
|
|
713
|
-
externalId: m.task_id,
|
|
714
|
-
},
|
|
715
|
-
sessionId,
|
|
716
|
-
turnId,
|
|
717
|
-
'in_progress',
|
|
718
|
-
'item:started',
|
|
719
|
-
);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
/** Handle SDKTaskProgressMessage — update the background_task item with progress info. */
|
|
723
|
-
function handleTaskProgress(
|
|
724
|
-
message: SDKMessage & { type: 'system'; subtype: 'task_progress' },
|
|
725
|
-
sessionId: string,
|
|
726
|
-
turnId: string,
|
|
727
|
-
entry: ClaudeSessionEntry,
|
|
728
|
-
db: Database.Database,
|
|
729
|
-
): void {
|
|
730
|
-
const m = message as unknown as {
|
|
731
|
-
task_id: string;
|
|
732
|
-
description: string;
|
|
733
|
-
subagent_type?: string;
|
|
734
|
-
last_tool_name?: string;
|
|
735
|
-
summary?: string;
|
|
736
|
-
usage?: { total_tokens: number; tool_uses: number; duration_ms: number };
|
|
737
|
-
};
|
|
738
|
-
const row = db
|
|
739
|
-
.prepare('SELECT data FROM items WHERE external_item_id = ? AND turn_id = ?')
|
|
740
|
-
.get(m.task_id, turnId) as { data: string | null } | undefined;
|
|
741
|
-
if (!row) return;
|
|
742
|
-
|
|
743
|
-
const existing = row.data ? (JSON.parse(row.data) as Record<string, unknown>) : {};
|
|
744
|
-
|
|
745
|
-
// Read live Monitor output — only for local_bash tasks (subagent outputs are
|
|
746
|
-
// JSONL transcripts, too large and not meant for display).
|
|
747
|
-
let output = (existing.output as string) ?? '';
|
|
748
|
-
const taskType = (existing.task_type as string) ?? 'unknown';
|
|
749
|
-
if (taskType === 'local_bash') {
|
|
750
|
-
const outputPath = (existing.output_file as string) || constructMonitorOutputPath(entry, m.task_id);
|
|
751
|
-
if (outputPath) {
|
|
752
|
-
try {
|
|
753
|
-
output = readFileSync(outputPath, 'utf-8');
|
|
754
|
-
} catch {
|
|
755
|
-
// File may not exist yet or may be temporarily unavailable — keep previous output
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
const updatedData = {
|
|
761
|
-
...existing,
|
|
762
|
-
description: m.description,
|
|
763
|
-
subagent_type: m.subagent_type ?? existing.subagent_type ?? null,
|
|
764
|
-
summary: m.summary ?? existing.summary ?? null,
|
|
765
|
-
last_tool_name: m.last_tool_name ?? null,
|
|
766
|
-
token_count: m.usage?.total_tokens ?? existing.token_count ?? null,
|
|
767
|
-
output,
|
|
768
|
-
};
|
|
769
|
-
|
|
770
|
-
upsertItem(
|
|
771
|
-
db,
|
|
772
|
-
{ type: 'background_task', data: updatedData, externalId: m.task_id },
|
|
773
|
-
sessionId,
|
|
774
|
-
turnId,
|
|
775
|
-
'in_progress',
|
|
776
|
-
'item:updated',
|
|
777
|
-
);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/** Handle SDKTaskUpdatedMessage — patch the background_task item's status. */
|
|
781
|
-
function handleTaskUpdated(
|
|
782
|
-
message: SDKMessage & { type: 'system'; subtype: 'task_updated' },
|
|
783
|
-
sessionId: string,
|
|
784
|
-
turnId: string,
|
|
785
|
-
db: Database.Database,
|
|
786
|
-
): void {
|
|
787
|
-
const m = message as unknown as {
|
|
788
|
-
task_id: string;
|
|
789
|
-
patch: { status?: string; description?: string; error?: string; is_backgrounded?: boolean };
|
|
790
|
-
};
|
|
791
|
-
const row = db
|
|
792
|
-
.prepare('SELECT data FROM items WHERE external_item_id = ? AND turn_id = ?')
|
|
793
|
-
.get(m.task_id, turnId) as { data: string | null } | undefined;
|
|
794
|
-
if (!row) return;
|
|
795
|
-
|
|
796
|
-
const existing = row.data ? (JSON.parse(row.data) as Record<string, unknown>) : {};
|
|
797
|
-
const updatedData = {
|
|
798
|
-
...existing,
|
|
799
|
-
status: m.patch.status ?? existing.status,
|
|
800
|
-
description: m.patch.description ?? existing.description,
|
|
801
|
-
error: m.patch.error ?? existing.error ?? null,
|
|
802
|
-
};
|
|
803
|
-
|
|
804
|
-
upsertItem(
|
|
805
|
-
db,
|
|
806
|
-
{ type: 'background_task', data: updatedData, externalId: m.task_id },
|
|
807
|
-
sessionId,
|
|
808
|
-
turnId,
|
|
809
|
-
'in_progress',
|
|
810
|
-
'item:updated',
|
|
811
|
-
);
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
/** Handle SDKTaskNotificationMessage — the background task completed/failed/stopped. */
|
|
815
|
-
function handleTaskNotification(
|
|
816
|
-
message: SDKMessage & { type: 'system'; subtype: 'task_notification' },
|
|
817
|
-
sessionId: string,
|
|
818
|
-
turnId: string,
|
|
819
|
-
entry: ClaudeSessionEntry,
|
|
820
|
-
db: Database.Database,
|
|
821
|
-
): void {
|
|
822
|
-
const m = message as unknown as {
|
|
823
|
-
task_id: string;
|
|
824
|
-
status: 'completed' | 'failed' | 'stopped';
|
|
825
|
-
summary: string;
|
|
826
|
-
output_file: string;
|
|
827
|
-
usage?: { total_tokens: number; tool_uses: number; duration_ms: number };
|
|
828
|
-
};
|
|
829
|
-
const row = db
|
|
830
|
-
.prepare('SELECT data FROM items WHERE external_item_id = ? AND turn_id = ?')
|
|
831
|
-
.get(m.task_id, turnId) as { data: string | null } | undefined;
|
|
832
|
-
if (!row) return;
|
|
833
|
-
|
|
834
|
-
const existing = row.data ? (JSON.parse(row.data) as Record<string, unknown>) : {};
|
|
835
|
-
|
|
836
|
-
// Determine the output file path — prefer the SDK-provided path, fall back to
|
|
837
|
-
// the constructed path (for cases where the SDK doesn't provide it).
|
|
838
|
-
const taskType = (existing.task_type as string) ?? 'unknown';
|
|
839
|
-
const sdkOutputFile = m.output_file || null;
|
|
840
|
-
const constructedPath = taskType === 'local_bash' ? constructMonitorOutputPath(entry, m.task_id) : '';
|
|
841
|
-
const outputPath = sdkOutputFile || constructedPath;
|
|
842
|
-
|
|
843
|
-
// Read the raw stdout output from the Monitor's output file.
|
|
844
|
-
let output = '';
|
|
845
|
-
if (outputPath) {
|
|
846
|
-
try {
|
|
847
|
-
output = readFileSync(outputPath, 'utf-8');
|
|
848
|
-
} catch {
|
|
849
|
-
// File may have been cleaned up or is inaccessible — leave output empty
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
const updatedData = {
|
|
854
|
-
...existing,
|
|
855
|
-
status: m.status,
|
|
856
|
-
summary: m.summary,
|
|
857
|
-
output_file: outputPath,
|
|
858
|
-
output,
|
|
859
|
-
token_count: m.usage?.total_tokens ?? existing.token_count ?? null,
|
|
860
|
-
};
|
|
861
|
-
|
|
862
|
-
const itemStatus: ItemStatus = m.status === 'completed' ? 'completed' : 'failed';
|
|
863
|
-
|
|
864
|
-
upsertItem(
|
|
865
|
-
db,
|
|
866
|
-
{ type: 'background_task', data: updatedData, externalId: m.task_id },
|
|
867
|
-
sessionId,
|
|
868
|
-
turnId,
|
|
869
|
-
itemStatus,
|
|
870
|
-
'item:completed',
|
|
871
|
-
);
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
/** Handle SDKBackgroundTasksChangedMessage — broadcast the full live task list.
|
|
875
|
-
* REPLACE semantics: frontend replaces its entire background task set. */
|
|
876
|
-
function handleBackgroundTasksChanged(
|
|
877
|
-
message: SDKMessage & { type: 'system'; subtype: 'background_tasks_changed' },
|
|
878
|
-
sessionId: string,
|
|
879
|
-
): void {
|
|
880
|
-
const m = message as unknown as {
|
|
881
|
-
tasks: Array<{ task_id: string; task_type: string; description: string }>;
|
|
882
|
-
};
|
|
883
|
-
broadcast('background_tasks', { sessionId, tasks: m.tasks ?? [] });
|
|
884
|
-
}
|
|
885
|
-
/** Handle SDKResultMessage — accumulate usage across multi-result turns (Monitor).
|
|
886
|
-
* Does NOT complete the turn on intermediate results — the turn completes when
|
|
887
|
-
* the next user message is pushed (in startTurn) or when the PromptQueue is
|
|
888
|
-
* closed (in the message loop's generator-end block). Only error results abort early. */
|
|
889
|
-
export async function handleResultMessage(
|
|
890
|
-
message: SDKMessage & { type: 'result' },
|
|
891
|
-
sessionId: string,
|
|
892
|
-
turnId: string,
|
|
893
|
-
entry: ClaudeSessionEntry,
|
|
894
|
-
db: Database.Database,
|
|
895
|
-
): Promise<void> { // Subagent result — skip entirely. Prevents subagent token/context data from
|
|
896
|
-
// overwriting the main session's token_usage and contextUsage.
|
|
897
|
-
// SDKResultSuccess doesn't declare parent_tool_use_id in its type, but the SDK
|
|
898
|
-
// may set it at runtime (same pattern as handleAssistantMessage/handleUserMessage).
|
|
899
|
-
const parentToolUseId = (message as { parent_tool_use_id?: string | null }).parent_tool_use_id ?? null;
|
|
900
|
-
if (parentToolUseId) return;
|
|
901
|
-
|
|
902
|
-
if (message.subtype === 'success') {
|
|
903
|
-
// Update token usage and complete the turn immediately.
|
|
904
|
-
// The SDK emits exactly one result/success per turn — this is the turn-complete signal.
|
|
905
|
-
// The CLI process stays alive (PromptQueue not closed) so Monitor background tasks survive.
|
|
906
|
-
// entry.turnId is NOT set to null — Monitor-triggered internal turn messages still attach
|
|
907
|
-
// to this turn via entry.turnId. Multiple completeTurn calls are idempotent (status check).
|
|
908
|
-
const usage = mapUsage(message.usage);
|
|
909
|
-
const now = new Date().toISOString();
|
|
910
|
-
db.prepare('UPDATE turns SET token_usage = ?, updated_at = ? WHERE id = ?').run(
|
|
911
|
-
JSON.stringify(usage), now, turnId,
|
|
912
|
-
);
|
|
913
|
-
broadcast('turn:tokenUsage', { sessionId, turnId, usage });
|
|
914
|
-
|
|
915
|
-
// Context-window usage: use the SDK's own /context computation (Query.getContextUsage),
|
|
916
|
-
// NOT the accumulated result.usage input/cache sum. The hand formula double-counts /
|
|
917
|
-
// accumulates in streaming-input mode (e.g. shows 800K while /context shows the real
|
|
918
|
-
// 130K). getContextUsage returns the same structured data as the /context command —
|
|
919
|
-
// the authoritative current in-window fill, computed locally by the CLI.
|
|
920
|
-
try {
|
|
921
|
-
const ctx = await entry.query.getContextUsage();
|
|
922
|
-
if (ctx.totalTokens > 0 && ctx.maxTokens > 0) {
|
|
923
|
-
const contextUsage: ContextUsage = {
|
|
924
|
-
totalTokens: ctx.totalTokens,
|
|
925
|
-
contextWindow: ctx.maxTokens,
|
|
926
|
-
percentage: Math.round(ctx.percentage),
|
|
927
|
-
};
|
|
928
|
-
broadcast('turn:contextUsage', { sessionId, ...contextUsage });
|
|
929
|
-
// Persist so the status-bar counter survives page reload.
|
|
930
|
-
const cuNow = new Date().toISOString();
|
|
931
|
-
db.prepare('UPDATE sessions SET context_usage = ?, updated_at = ? WHERE id = ?').run(
|
|
932
|
-
JSON.stringify(contextUsage), cuNow, sessionId,
|
|
933
|
-
);
|
|
934
|
-
}
|
|
935
|
-
} catch {
|
|
936
|
-
// getContextUsage unavailable (older CLI) or failed — leave contextUsage unchanged.
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
completeTurn(db, sessionId, turnId, usage);
|
|
940
|
-
} else {
|
|
941
|
-
// Error result — abort the turn immediately.
|
|
942
|
-
const errorMsg = message.errors?.join('; ') ?? 'Claude turn failed';
|
|
943
|
-
const now = new Date().toISOString();
|
|
944
|
-
db.prepare('UPDATE turns SET status = ?, error = ?, updated_at = ? WHERE id = ?').run('failed', errorMsg, now, turnId);
|
|
945
|
-
setSessionStatus(db, sessionId, 0, 'failed', now);
|
|
946
|
-
broadcast('turn:failed', { sessionId, turnId, error: errorMsg });
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
// ---------------------------------------------------------------------------
|
|
951
|
-
// completeTurn / upsertItem
|
|
952
|
-
// ---------------------------------------------------------------------------
|
|
953
|
-
|
|
954
|
-
/** Complete a turn — set status, broadcast turn:completed. */
|
|
955
|
-
function completeTurn(db: Database.Database, sessionId: string, turnId: string, usage: TokenUsage | null): void {
|
|
956
|
-
const now = new Date().toISOString();
|
|
957
|
-
if (usage) {
|
|
958
|
-
db.prepare('UPDATE turns SET status = ?, token_usage = ?, updated_at = ? WHERE id = ?').run(
|
|
959
|
-
'completed', JSON.stringify(usage), now, turnId,
|
|
960
|
-
);
|
|
961
|
-
broadcast('turn:completed', { sessionId, turnId, usage, hasUnreviewed: true });
|
|
962
|
-
} else {
|
|
963
|
-
db.prepare('UPDATE turns SET status = ?, updated_at = ? WHERE id = ?').run('completed', now, turnId);
|
|
964
|
-
broadcast('turn:completed', { sessionId, turnId, hasUnreviewed: true });
|
|
965
|
-
}
|
|
966
|
-
setSessionStatus(db, sessionId, 0, 'active', now, true);
|
|
967
|
-
syncThreadTitle(sessionId);
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
/** Insert or update an item in the DB and broadcast. */
|
|
971
|
-
function upsertItem(
|
|
972
|
-
db: Database.Database,
|
|
973
|
-
desc: ItemDescriptor,
|
|
974
|
-
sessionId: string,
|
|
975
|
-
turnId: string,
|
|
976
|
-
status: ItemStatus,
|
|
977
|
-
broadcastEvent: string,
|
|
978
|
-
): void {
|
|
979
|
-
const now = new Date().toISOString();
|
|
980
|
-
|
|
981
|
-
// Try to update existing row
|
|
982
|
-
const result = db
|
|
983
|
-
.prepare('UPDATE items SET data = ?, status = ?, updated_at = ? WHERE external_item_id = ? AND turn_id = ?')
|
|
984
|
-
.run(JSON.stringify(desc.data), status, now, desc.externalId, turnId);
|
|
985
|
-
|
|
986
|
-
let itemId: string;
|
|
987
|
-
let createdAt: string;
|
|
988
|
-
|
|
989
|
-
if (result.changes === 0) {
|
|
990
|
-
itemId = uuidv4();
|
|
991
|
-
createdAt = now;
|
|
992
|
-
db.prepare(
|
|
993
|
-
'INSERT INTO items (id, turn_id, external_item_id, type, data, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
|
994
|
-
).run(itemId, turnId, desc.externalId, desc.type as Item['type'], JSON.stringify(desc.data), status, now, now);
|
|
995
|
-
} else {
|
|
996
|
-
const row = db
|
|
997
|
-
.prepare('SELECT id, created_at FROM items WHERE external_item_id = ? AND turn_id = ?')
|
|
998
|
-
.get(desc.externalId, turnId) as { id: string; created_at: string };
|
|
999
|
-
itemId = row.id;
|
|
1000
|
-
createdAt = row.created_at;
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
const wsItem: Item = {
|
|
1004
|
-
id: itemId,
|
|
1005
|
-
turn_id: turnId,
|
|
1006
|
-
external_item_id: desc.externalId,
|
|
1007
|
-
type: desc.type as Item['type'],
|
|
1008
|
-
data: desc.data,
|
|
1009
|
-
status,
|
|
1010
|
-
created_at: createdAt,
|
|
1011
|
-
updated_at: now,
|
|
1012
|
-
};
|
|
1013
|
-
|
|
1014
|
-
broadcast(broadcastEvent, { sessionId, turnId, item: wsItem });
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
// ---------------------------------------------------------------------------
|
|
1018
|
-
// interruptTurn
|
|
1019
|
-
// ---------------------------------------------------------------------------
|
|
1020
|
-
|
|
1021
|
-
/** Cancel a running turn by session ID. Returns true if a turn was cancelled.
|
|
1022
|
-
*
|
|
1023
|
-
* Calls `query.interrupt()` to abort the current generation, but does NOT
|
|
1024
|
-
* close the query or the PromptQueue — the CLI process stays alive so the
|
|
1025
|
-
* user can send the next message without spawning a new process. */
|
|
1026
|
-
async function interruptTurn(sessionId: string): Promise<boolean> {
|
|
1027
|
-
// Check main entry first, then thread entries
|
|
1028
|
-
let entry = sessionMap.get(sessionId);
|
|
1029
|
-
if (!entry || !entry.turnId) {
|
|
1030
|
-
// Search thread entries for this session
|
|
1031
|
-
for (const [key, threadEntry] of sessionMap) {
|
|
1032
|
-
if (key.startsWith(`${sessionId}:thread:`) && threadEntry.turnId) {
|
|
1033
|
-
entry = threadEntry;
|
|
1034
|
-
break;
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
if (!entry || !entry.turnId) return false;
|
|
1039
|
-
|
|
1040
|
-
try {
|
|
1041
|
-
await entry.query.interrupt();
|
|
1042
|
-
|
|
1043
|
-
const db = getDb();
|
|
1044
|
-
const now = new Date().toISOString();
|
|
1045
|
-
db.prepare('UPDATE turns SET status = ?, updated_at = ? WHERE id = ?').run('aborted', now, entry.turnId);
|
|
1046
|
-
setSessionStatus(db, sessionId, 0, 'active', now);
|
|
1047
|
-
broadcast('turn:cancelled', { sessionId, turnId: entry.turnId });
|
|
1048
|
-
|
|
1049
|
-
streamingMessageIds.delete(entry.turnId);
|
|
1050
|
-
entry.turnId = null;
|
|
1051
|
-
return true;
|
|
1052
|
-
} catch {
|
|
1053
|
-
return false;
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
// ---------------------------------------------------------------------------
|
|
1058
|
-
// clearThread / clearAllThreads
|
|
1059
|
-
// ---------------------------------------------------------------------------
|
|
1060
|
-
|
|
1061
|
-
/** Remove a session's streaming query from the in-memory map.
|
|
1062
|
-
*
|
|
1063
|
-
* Closes the PromptQueue (signals end-of-iteration → CLI exits gracefully)
|
|
1064
|
-
* and closes the Query object (forceful cleanup). Used after rewind, branch,
|
|
1065
|
-
* provider change, or restore — the next startTurn will create a fresh query. */
|
|
1066
|
-
function clearThread(sessionId: string): void {
|
|
1067
|
-
// Main entry
|
|
1068
|
-
const entry = sessionMap.get(sessionId);
|
|
1069
|
-
if (entry) {
|
|
1070
|
-
entry.promptQueue.close();
|
|
1071
|
-
entry.query.close();
|
|
1072
|
-
if (entry.turnId) {
|
|
1073
|
-
streamingMessageIds.delete(entry.turnId);
|
|
1074
|
-
}
|
|
1075
|
-
sessionMap.delete(sessionId);
|
|
1076
|
-
}
|
|
1077
|
-
// All thread entries for this session
|
|
1078
|
-
for (const [key, threadEntry] of sessionMap) {
|
|
1079
|
-
if (key.startsWith(`${sessionId}:thread:`)) {
|
|
1080
|
-
threadEntry.promptQueue.close();
|
|
1081
|
-
threadEntry.query.close();
|
|
1082
|
-
if (threadEntry.turnId) {
|
|
1083
|
-
streamingMessageIds.delete(threadEntry.turnId);
|
|
1084
|
-
}
|
|
1085
|
-
sessionMap.delete(key);
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
/** Remove a single thread's branched session entry. Used when a thread is deleted. */
|
|
1091
|
-
function clearThreadBranch(sessionId: string, threadId: string): void {
|
|
1092
|
-
const key = `${sessionId}:thread:${threadId}`;
|
|
1093
|
-
const entry = sessionMap.get(key);
|
|
1094
|
-
if (entry) {
|
|
1095
|
-
entry.promptQueue.close();
|
|
1096
|
-
entry.query.close();
|
|
1097
|
-
if (entry.turnId) {
|
|
1098
|
-
streamingMessageIds.delete(entry.turnId);
|
|
1099
|
-
}
|
|
1100
|
-
sessionMap.delete(key);
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
/** Clear all session entries. Used in test cleanup. */
|
|
1105
|
-
export function clearAllThreads(): void {
|
|
1106
|
-
for (const [, entry] of sessionMap) {
|
|
1107
|
-
entry.promptQueue.close();
|
|
1108
|
-
entry.query.close();
|
|
1109
|
-
}
|
|
1110
|
-
sessionMap.clear();
|
|
1111
|
-
titleGenerating.clear();
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
// ---------------------------------------------------------------------------
|
|
1115
|
-
// setModel — change model mid-session without restarting the CLI
|
|
1116
|
-
// ---------------------------------------------------------------------------
|
|
1117
|
-
|
|
1118
|
-
/** Change the model for a running session. Only works in streaming-input mode.
|
|
1119
|
-
* If the session hasn't started yet (no entry in sessionMap), the new model
|
|
1120
|
-
* will be picked up when ensureSessionStarted creates the query. */
|
|
1121
|
-
async function setModel(sessionId: string, model: string): Promise<void> {
|
|
1122
|
-
const entry = sessionMap.get(sessionId);
|
|
1123
|
-
if (!entry) return; // session not started — model will be read from DB on next startTurn
|
|
1124
|
-
await entry.query.setModel(model);
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
// ---------------------------------------------------------------------------
|
|
1128
|
-
// stopTask — kill a background task (Monitor/subagent) by task ID
|
|
1129
|
-
// ---------------------------------------------------------------------------
|
|
1130
|
-
|
|
1131
|
-
/** Stop a running background task by task ID. Emits a task_notification with
|
|
1132
|
-
* status 'stopped'. Enables a future "Kill Monitor" button in the UI. */
|
|
1133
|
-
async function stopTask(sessionId: string, taskId: string): Promise<void> {
|
|
1134
|
-
const entry = sessionMap.get(sessionId);
|
|
1135
|
-
if (!entry) return;
|
|
1136
|
-
await entry.query.stopTask(taskId);
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
// ---------------------------------------------------------------------------
|
|
1140
|
-
// getModels / getCommands
|
|
1141
|
-
// ---------------------------------------------------------------------------
|
|
1142
|
-
|
|
1143
|
-
/** Fetch available models from the Claude SDK and map to AgentPane ModelInfo[]. */
|
|
1144
|
-
async function getModels(): Promise<ModelInfo[]> {
|
|
1145
|
-
if (cachedModels) return cachedModels;
|
|
1146
|
-
|
|
1147
|
-
try {
|
|
1148
|
-
const q = query({
|
|
1149
|
-
prompt: '',
|
|
1150
|
-
options: {
|
|
1151
|
-
cwd: process.cwd(),
|
|
1152
|
-
permissionMode: 'bypassPermissions',
|
|
1153
|
-
allowDangerouslySkipPermissions: true,
|
|
1154
|
-
},
|
|
1155
|
-
});
|
|
1156
|
-
|
|
1157
|
-
const initResult = await q.initializationResult();
|
|
1158
|
-
q.close();
|
|
1159
|
-
|
|
1160
|
-
// Cache provider-native slash commands alongside models (single init call).
|
|
1161
|
-
cachedCommands = mapSdkCommands(initResult.commands ?? []);
|
|
1162
|
-
|
|
1163
|
-
cachedModels = (initResult.models ?? []).map((m: SdkModelInfo) => ({
|
|
1164
|
-
slug: m.value,
|
|
1165
|
-
displayName: m.displayName,
|
|
1166
|
-
description: m.description,
|
|
1167
|
-
defaultReasoning: m.supportedEffortLevels?.[0] ?? 'medium',
|
|
1168
|
-
reasoningLevels: (m.supportedEffortLevels ?? []).map((e) => ({
|
|
1169
|
-
effort: e,
|
|
1170
|
-
label: EFFORT_LABELS[e] ?? e,
|
|
1171
|
-
})),
|
|
1172
|
-
}));
|
|
1173
|
-
|
|
1174
|
-
return cachedModels;
|
|
1175
|
-
} catch {
|
|
1176
|
-
return [];
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
/** Fetch provider-native slash commands (cached from the models init call). */
|
|
1181
|
-
async function getCommands(): Promise<ProviderSlashCommand[]> {
|
|
1182
|
-
if (cachedCommands) return cachedCommands;
|
|
1183
|
-
// Trigger an init by fetching models (which caches commands as a side effect).
|
|
1184
|
-
await getModels();
|
|
1185
|
-
return cachedCommands ?? [];
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
/** Map SDK SlashCommand[] to the shared ProviderSlashCommand shape. */
|
|
1189
|
-
function mapSdkCommands(commands: SdkSlashCommand[]): ProviderSlashCommand[] {
|
|
1190
|
-
return commands.map((c) => ({
|
|
1191
|
-
name: c.name,
|
|
1192
|
-
description: c.description,
|
|
1193
|
-
...(c.argumentHint ? { argumentHint: c.argumentHint } : {}),
|
|
1194
|
-
...(c.aliases?.length ? { aliases: c.aliases } : {}),
|
|
1195
|
-
}));
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
// ---------------------------------------------------------------------------
|
|
1199
|
-
// shutdown
|
|
1200
|
-
// ---------------------------------------------------------------------------
|
|
1201
|
-
|
|
1202
|
-
/** Graceful shutdown — close all query instances. */
|
|
1203
|
-
async function shutdown(): Promise<void> {
|
|
1204
|
-
clearAllThreads();
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
// ---------------------------------------------------------------------------
|
|
1208
|
-
// Title generation
|
|
1209
|
-
// ---------------------------------------------------------------------------
|
|
1210
|
-
|
|
1211
|
-
/** Generate a title for the session using a throwaway Claude query. */
|
|
1212
|
-
function syncThreadTitle(sessionId: string): void {
|
|
1213
|
-
const db = getDb();
|
|
1214
|
-
const session = db.prepare('SELECT title FROM sessions WHERE id = ?').get(sessionId) as
|
|
1215
|
-
| { title: string | null }
|
|
1216
|
-
| undefined;
|
|
1217
|
-
if (!session || session.title !== null) return;
|
|
1218
|
-
if (titleGenerating.has(sessionId)) return;
|
|
1219
|
-
|
|
1220
|
-
const turn = db
|
|
1221
|
-
.prepare('SELECT user_prompt, agent_response FROM turns WHERE session_id = ? ORDER BY created_at ASC LIMIT 1')
|
|
1222
|
-
.get(sessionId) as { user_prompt: string; agent_response: string | null } | undefined;
|
|
1223
|
-
if (!turn) return;
|
|
1224
|
-
|
|
1225
|
-
titleGenerating.add(sessionId);
|
|
1226
|
-
generateTitleAsync(sessionId, turn.user_prompt, turn.agent_response).finally(() => {
|
|
1227
|
-
titleGenerating.delete(sessionId);
|
|
1228
|
-
});
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
/** Create a throwaway Claude query, ask for a title, update the session. */
|
|
1232
|
-
async function generateTitleAsync(
|
|
1233
|
-
sessionId: string,
|
|
1234
|
-
userPrompt: string,
|
|
1235
|
-
agentResponse: string | null,
|
|
1236
|
-
): Promise<void> {
|
|
1237
|
-
let q: Query | null = null;
|
|
1238
|
-
try {
|
|
1239
|
-
const truncatedPrompt = userPrompt.slice(0, 500);
|
|
1240
|
-
const truncatedResponse = (agentResponse ?? '').slice(0, 500);
|
|
1241
|
-
const titlePrompt = `Generate a concise 3-5 word title for this conversation. Reply with only the title, nothing else.\n\nUser asked: "${truncatedPrompt}"\n\nAgent responded: "${truncatedResponse}"`;
|
|
1242
|
-
|
|
1243
|
-
q = query({
|
|
1244
|
-
prompt: titlePrompt,
|
|
1245
|
-
options: {
|
|
1246
|
-
cwd: process.cwd(),
|
|
1247
|
-
permissionMode: 'bypassPermissions',
|
|
1248
|
-
allowDangerouslySkipPermissions: true,
|
|
1249
|
-
},
|
|
1250
|
-
});
|
|
1251
|
-
|
|
1252
|
-
let titleResponse = '';
|
|
1253
|
-
|
|
1254
|
-
for await (const message of q) {
|
|
1255
|
-
if (message.type === 'assistant') {
|
|
1256
|
-
for (const block of message.message.content) {
|
|
1257
|
-
if (block.type === 'text') {
|
|
1258
|
-
titleResponse += block.text;
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
const cleanTitle = titleResponse.trim().replace(/^["']|["']$/g, '').slice(0, 100);
|
|
1265
|
-
|
|
1266
|
-
if (cleanTitle) {
|
|
1267
|
-
const db = getDb();
|
|
1268
|
-
const now = new Date().toISOString();
|
|
1269
|
-
db.prepare('UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?').run(cleanTitle, now, sessionId);
|
|
1270
|
-
broadcast('session:title', { sessionId, title: cleanTitle });
|
|
1271
|
-
}
|
|
1272
|
-
} catch {
|
|
1273
|
-
// Silently skip on error — session keeps null title
|
|
1274
|
-
} finally {
|
|
1275
|
-
if (q) {
|
|
1276
|
-
q.close();
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
// ---------------------------------------------------------------------------
|
|
1282
|
-
// forkSession
|
|
1283
|
-
// ---------------------------------------------------------------------------
|
|
1284
|
-
|
|
1285
|
-
/** Fork the Claude session at a branch point using the SDK's forkSession().
|
|
1286
|
-
* Copies the transcript up to the target turn's last assistant message,
|
|
1287
|
-
* returning a new Claude session ID that can be resumed with full context. */
|
|
1288
|
-
async function forkClaudeSession(session: Session, targetTurn: Turn): Promise<string | null> {
|
|
1289
|
-
if (!session.external_thread_id) return null;
|
|
1290
|
-
try {
|
|
1291
|
-
const result = await sdkForkSession(session.external_thread_id, {
|
|
1292
|
-
dir: session.cwd ?? process.cwd(),
|
|
1293
|
-
...(targetTurn.external_turn_id ? { upToMessageId: targetTurn.external_turn_id } : {}),
|
|
1294
|
-
});
|
|
1295
|
-
return result.sessionId;
|
|
1296
|
-
} catch (err) {
|
|
1297
|
-
console.error('[claude] forkSession failed:', err);
|
|
1298
|
-
return null;
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
// ---------------------------------------------------------------------------
|
|
1303
|
-
// Provider export
|
|
1304
|
-
// ---------------------------------------------------------------------------
|
|
1305
|
-
|
|
1306
|
-
/** Claude provider — implements RuntimeProvider interface. */
|
|
1307
|
-
export const claudeProvider: RuntimeProvider = {
|
|
1308
|
-
startTurn,
|
|
1309
|
-
interruptTurn,
|
|
1310
|
-
clearThread,
|
|
1311
|
-
clearThreadBranch,
|
|
1312
|
-
clearAllThreads,
|
|
1313
|
-
getModels,
|
|
1314
|
-
getCommands,
|
|
1315
|
-
forkSession: forkClaudeSession,
|
|
1316
|
-
setModel,
|
|
1317
|
-
stopTask,
|
|
1318
|
-
shutdown,
|
|
1319
|
-
};
|