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,323 +0,0 @@
|
|
|
1
|
-
import type { FastifyInstance } from 'fastify';
|
|
2
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
-
import { getDb } from '../db/index.ts';
|
|
4
|
-
import { getProvider } from '../providers/registry.ts';
|
|
5
|
-
import { broadcast } from '../ws/index.ts';
|
|
6
|
-
import type { Session, Turn, DiscussionThread } from '../db/schema.ts';
|
|
7
|
-
|
|
8
|
-
/** Raw DB row for turns — token_usage is a JSON string, not parsed; no items field. */
|
|
9
|
-
interface TurnRow extends Omit<Turn, 'token_usage' | 'items'> {
|
|
10
|
-
token_usage: string | null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** Raw DB row for items — data is a JSON string, not parsed. */
|
|
14
|
-
interface ItemRow extends Omit<Turn['items'][number], 'data'> {
|
|
15
|
-
data: string | null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function registerThreadRoutes(fastify: FastifyInstance): Promise<void> {
|
|
19
|
-
// POST /api/sessions/:sessionId/threads — create thread + first AI turn
|
|
20
|
-
fastify.post<{
|
|
21
|
-
Params: { sessionId: string };
|
|
22
|
-
Body: {
|
|
23
|
-
itemId: string;
|
|
24
|
-
turnId: string;
|
|
25
|
-
startOffset: number;
|
|
26
|
-
endOffset: number;
|
|
27
|
-
selectedText: string;
|
|
28
|
-
message: string;
|
|
29
|
-
};
|
|
30
|
-
}>(
|
|
31
|
-
'/api/sessions/:sessionId/threads',
|
|
32
|
-
{
|
|
33
|
-
schema: {
|
|
34
|
-
body: {
|
|
35
|
-
type: 'object',
|
|
36
|
-
required: ['itemId', 'turnId', 'startOffset', 'endOffset', 'selectedText', 'message'],
|
|
37
|
-
properties: {
|
|
38
|
-
itemId: { type: 'string' },
|
|
39
|
-
turnId: { type: 'string' },
|
|
40
|
-
startOffset: { type: 'number' },
|
|
41
|
-
endOffset: { type: 'number' },
|
|
42
|
-
selectedText: { type: 'string' },
|
|
43
|
-
message: { type: 'string', minLength: 1 },
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
async (request, reply) => {
|
|
49
|
-
const { sessionId } = request.params;
|
|
50
|
-
const { itemId, turnId, startOffset, endOffset, selectedText, message } = request.body || {};
|
|
51
|
-
|
|
52
|
-
const db = getDb();
|
|
53
|
-
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId) as Session | undefined;
|
|
54
|
-
if (!session) {
|
|
55
|
-
return reply.code(404).send({ error: 'Session not found' });
|
|
56
|
-
}
|
|
57
|
-
if (session.is_running) {
|
|
58
|
-
return reply.code(409).send({ error: 'A turn is already running for this session' });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const threadId = uuidv4();
|
|
62
|
-
const now = new Date().toISOString();
|
|
63
|
-
|
|
64
|
-
// 1. Create the discussion thread row
|
|
65
|
-
db.prepare(
|
|
66
|
-
`INSERT INTO discussion_threads (id, session_id, item_id, turn_id, start_offset, end_offset, selected_text, status, created_at, updated_at)
|
|
67
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, 'open', ?, ?)`,
|
|
68
|
-
).run(threadId, sessionId, itemId, turnId, startOffset, endOffset, selectedText, now, now);
|
|
69
|
-
|
|
70
|
-
// 1b. Fork the Claude session at the anchor turn so the thread has its own
|
|
71
|
-
// isolated conversation — main session context stays unaffected.
|
|
72
|
-
const provider = getProvider(session.runtime_type);
|
|
73
|
-
if (provider.forkSession) {
|
|
74
|
-
try {
|
|
75
|
-
if (!session.external_thread_id) {
|
|
76
|
-
// No main session to fork from — the thread will start a fresh
|
|
77
|
-
// isolated session (startTurn skips the resume fallback), so the
|
|
78
|
-
// main session is never contaminated.
|
|
79
|
-
console.warn(`[threads] cannot fork for thread ${threadId}: session ${sessionId} has no external_thread_id — thread will start a fresh isolated session`);
|
|
80
|
-
} else {
|
|
81
|
-
const anchorTurn = db.prepare('SELECT * FROM turns WHERE id = ?').get(turnId) as Turn | undefined;
|
|
82
|
-
if (anchorTurn) {
|
|
83
|
-
const forkedSessionId = await provider.forkSession(session, anchorTurn);
|
|
84
|
-
if (forkedSessionId) {
|
|
85
|
-
db.prepare('UPDATE discussion_threads SET external_thread_id = ? WHERE id = ?').run(forkedSessionId, threadId);
|
|
86
|
-
console.log(`[threads] forked to ${forkedSessionId} for thread ${threadId} (session ${sessionId})`);
|
|
87
|
-
} else {
|
|
88
|
-
// forkSession returned null — thread will start a fresh isolated
|
|
89
|
-
// session in startTurn (NOT the main session), so isolation holds.
|
|
90
|
-
console.warn(`[threads] forkSession returned null for thread ${threadId} (external_thread_id=${session.external_thread_id}, upToMessageId=${anchorTurn.external_turn_id}) — thread will start a fresh isolated session`);
|
|
91
|
-
}
|
|
92
|
-
} else {
|
|
93
|
-
console.warn(`[threads] anchor turn ${turnId} not found for thread ${threadId} — cannot fork`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
} catch (err) {
|
|
97
|
-
console.error(`[threads] forkSession failed for thread ${threadId}:`, err);
|
|
98
|
-
// Non-fatal — thread falls back to a fresh isolated session (NOT the
|
|
99
|
-
// main session) so the main conversation is never contaminated.
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 2. Format prompt with quoted text for AI context
|
|
104
|
-
const formattedPrompt = `[Quoting from your previous response]:\n> ${selectedText}\n\n${message}`;
|
|
105
|
-
|
|
106
|
-
// 3. Create the turn row with thread_id
|
|
107
|
-
const newTurnId = uuidv4();
|
|
108
|
-
db.prepare(
|
|
109
|
-
`INSERT INTO turns (id, session_id, user_prompt, agent_response, thoughts, token_usage, external_turn_id, status, error, thread_id, created_at, updated_at)
|
|
110
|
-
VALUES (?, ?, ?, NULL, NULL, NULL, NULL, 'pending', NULL, ?, ?, ?)`,
|
|
111
|
-
).run(newTurnId, sessionId, formattedPrompt, threadId, now, now);
|
|
112
|
-
|
|
113
|
-
const turn = db.prepare('SELECT * FROM turns WHERE id = ?').get(newTurnId) as Turn;
|
|
114
|
-
|
|
115
|
-
// 4. Start the AI turn (fire and forget — events stream via WS)
|
|
116
|
-
provider
|
|
117
|
-
.startTurn(session, turn)
|
|
118
|
-
.catch((err) => {
|
|
119
|
-
console.error(`[threads] Unhandled error for session ${sessionId}:`, err);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// 5. Build the thread response
|
|
123
|
-
const threadRow = db.prepare('SELECT * FROM discussion_threads WHERE id = ?').get(threadId) as DiscussionThread;
|
|
124
|
-
const thread: DiscussionThread = {
|
|
125
|
-
...threadRow,
|
|
126
|
-
turns: [{ ...turn, items: [] }],
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// 6. Broadcast thread creation
|
|
130
|
-
broadcast('thread:created', { sessionId, thread, turn });
|
|
131
|
-
|
|
132
|
-
return { thread, turn };
|
|
133
|
-
},
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
// POST /api/sessions/:sessionId/threads/:threadId/followup — add follow-up message to thread
|
|
137
|
-
fastify.post<{
|
|
138
|
-
Params: { sessionId: string; threadId: string };
|
|
139
|
-
Body: { message: string };
|
|
140
|
-
}>(
|
|
141
|
-
'/api/sessions/:sessionId/threads/:threadId/followup',
|
|
142
|
-
{
|
|
143
|
-
schema: {
|
|
144
|
-
body: {
|
|
145
|
-
type: 'object',
|
|
146
|
-
required: ['message'],
|
|
147
|
-
properties: {
|
|
148
|
-
message: { type: 'string', minLength: 1 },
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
async (request, reply) => {
|
|
154
|
-
const { sessionId, threadId } = request.params;
|
|
155
|
-
const { message } = request.body || {};
|
|
156
|
-
|
|
157
|
-
const db = getDb();
|
|
158
|
-
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId) as Session | undefined;
|
|
159
|
-
if (!session) {
|
|
160
|
-
return reply.code(404).send({ error: 'Session not found' });
|
|
161
|
-
}
|
|
162
|
-
if (session.is_running) {
|
|
163
|
-
return reply.code(409).send({ error: 'A turn is already running for this session' });
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const thread = db
|
|
167
|
-
.prepare('SELECT * FROM discussion_threads WHERE id = ? AND session_id = ?')
|
|
168
|
-
.get(threadId, sessionId) as DiscussionThread | undefined;
|
|
169
|
-
if (!thread) {
|
|
170
|
-
return reply.code(404).send({ error: 'Thread not found' });
|
|
171
|
-
}
|
|
172
|
-
if (thread.status !== 'open') {
|
|
173
|
-
return reply.code(409).send({ error: 'Thread is resolved — reopen it first' });
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Create new turn with same thread_id
|
|
177
|
-
const newTurnId = uuidv4();
|
|
178
|
-
const now = new Date().toISOString();
|
|
179
|
-
db.prepare(
|
|
180
|
-
`INSERT INTO turns (id, session_id, user_prompt, agent_response, thoughts, token_usage, external_turn_id, status, error, thread_id, created_at, updated_at)
|
|
181
|
-
VALUES (?, ?, ?, NULL, NULL, NULL, NULL, 'pending', NULL, ?, ?, ?)`,
|
|
182
|
-
).run(newTurnId, sessionId, message, threadId, now, now);
|
|
183
|
-
|
|
184
|
-
const turn = db.prepare('SELECT * FROM turns WHERE id = ?').get(newTurnId) as Turn;
|
|
185
|
-
|
|
186
|
-
// Start the AI turn
|
|
187
|
-
getProvider(session.runtime_type)
|
|
188
|
-
.startTurn(session, turn)
|
|
189
|
-
.catch((err) => {
|
|
190
|
-
console.error(`[threads] followup error for session ${sessionId}:`, err);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
broadcast('thread:followup', { sessionId, threadId, turn });
|
|
194
|
-
|
|
195
|
-
return { turn };
|
|
196
|
-
},
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
// PATCH /api/sessions/:sessionId/threads/:threadId — resolve or reopen
|
|
200
|
-
fastify.patch<{
|
|
201
|
-
Params: { sessionId: string; threadId: string };
|
|
202
|
-
Body: { status: 'resolved' | 'open' };
|
|
203
|
-
}>(
|
|
204
|
-
'/api/sessions/:sessionId/threads/:threadId',
|
|
205
|
-
{
|
|
206
|
-
schema: {
|
|
207
|
-
body: {
|
|
208
|
-
type: 'object',
|
|
209
|
-
required: ['status'],
|
|
210
|
-
properties: {
|
|
211
|
-
status: { type: 'string', enum: ['resolved', 'open'] },
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
},
|
|
216
|
-
async (request, reply) => {
|
|
217
|
-
const { sessionId, threadId } = request.params;
|
|
218
|
-
const { status } = request.body || {};
|
|
219
|
-
|
|
220
|
-
const db = getDb();
|
|
221
|
-
const thread = db
|
|
222
|
-
.prepare('SELECT * FROM discussion_threads WHERE id = ? AND session_id = ?')
|
|
223
|
-
.get(threadId, sessionId) as DiscussionThread | undefined;
|
|
224
|
-
if (!thread) {
|
|
225
|
-
return reply.code(404).send({ error: 'Thread not found' });
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const now = new Date().toISOString();
|
|
229
|
-
const resolvedAt = status === 'resolved' ? now : null;
|
|
230
|
-
db.prepare(
|
|
231
|
-
'UPDATE discussion_threads SET status = ?, resolved_at = ?, updated_at = ? WHERE id = ?',
|
|
232
|
-
).run(status, resolvedAt, now, threadId);
|
|
233
|
-
|
|
234
|
-
broadcast('thread:resolved', { sessionId, threadId, status });
|
|
235
|
-
|
|
236
|
-
return { id: threadId, status, resolved_at: resolvedAt };
|
|
237
|
-
},
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
// DELETE /api/sessions/:sessionId/threads/:threadId — delete thread + its turns + items
|
|
241
|
-
fastify.delete<{ Params: { sessionId: string; threadId: string } }>(
|
|
242
|
-
'/api/sessions/:sessionId/threads/:threadId',
|
|
243
|
-
async (request, reply) => {
|
|
244
|
-
const { sessionId, threadId } = request.params;
|
|
245
|
-
const db = getDb();
|
|
246
|
-
|
|
247
|
-
const thread = db
|
|
248
|
-
.prepare('SELECT id FROM discussion_threads WHERE id = ? AND session_id = ?')
|
|
249
|
-
.get(threadId, sessionId);
|
|
250
|
-
if (!thread) {
|
|
251
|
-
return reply.code(404).send({ error: 'Thread not found' });
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Clean up the thread's branched session entry (Claude provider)
|
|
255
|
-
const session = db.prepare('SELECT runtime_type FROM sessions WHERE id = ?').get(sessionId) as { runtime_type: string } | undefined;
|
|
256
|
-
if (session) {
|
|
257
|
-
const provider = getProvider(session.runtime_type);
|
|
258
|
-
provider.clearThreadBranch?.(sessionId, threadId);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Delete items belonging to turns in this thread
|
|
262
|
-
db.prepare(
|
|
263
|
-
'DELETE FROM items WHERE turn_id IN (SELECT id FROM turns WHERE thread_id = ?)',
|
|
264
|
-
).run(threadId);
|
|
265
|
-
// Delete turns in this thread
|
|
266
|
-
db.prepare('DELETE FROM turns WHERE thread_id = ?').run(threadId);
|
|
267
|
-
// Delete the thread row
|
|
268
|
-
db.prepare('DELETE FROM discussion_threads WHERE id = ?').run(threadId);
|
|
269
|
-
|
|
270
|
-
broadcast('thread:deleted', { sessionId, threadId });
|
|
271
|
-
|
|
272
|
-
return { success: true };
|
|
273
|
-
},
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
// GET /api/sessions/:sessionId/threads — list all threads with their turns
|
|
277
|
-
fastify.get<{ Params: { sessionId: string } }>('/api/sessions/:sessionId/threads', async (request, reply) => {
|
|
278
|
-
const { sessionId } = request.params;
|
|
279
|
-
const db = getDb();
|
|
280
|
-
|
|
281
|
-
const session = db.prepare('SELECT id FROM sessions WHERE id = ?').get(sessionId);
|
|
282
|
-
if (!session) {
|
|
283
|
-
return reply.code(404).send({ error: 'Session not found' });
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const threadRows = db
|
|
287
|
-
.prepare('SELECT * FROM discussion_threads WHERE session_id = ? ORDER BY created_at ASC')
|
|
288
|
-
.all(sessionId) as DiscussionThread[];
|
|
289
|
-
|
|
290
|
-
// Attach turns + items for each thread
|
|
291
|
-
const result: DiscussionThread[] = [];
|
|
292
|
-
for (const tr of threadRows) {
|
|
293
|
-
const turnRows = db
|
|
294
|
-
.prepare('SELECT * FROM turns WHERE thread_id = ? ORDER BY created_at ASC')
|
|
295
|
-
.all(tr.id) as TurnRow[];
|
|
296
|
-
|
|
297
|
-
const itemRows = db
|
|
298
|
-
.prepare(
|
|
299
|
-
'SELECT * FROM items WHERE turn_id IN (SELECT id FROM turns WHERE thread_id = ?) ORDER BY created_at ASC',
|
|
300
|
-
)
|
|
301
|
-
.all(tr.id) as ItemRow[];
|
|
302
|
-
|
|
303
|
-
const itemsByTurn = new Map<string, Turn['items']>();
|
|
304
|
-
for (const row of itemRows) {
|
|
305
|
-
const item = { ...row, data: row.data ? JSON.parse(row.data) : null };
|
|
306
|
-
const list = itemsByTurn.get(item.turn_id) ?? [];
|
|
307
|
-
list.push(item);
|
|
308
|
-
itemsByTurn.set(item.turn_id, list);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
result.push({
|
|
312
|
-
...tr,
|
|
313
|
-
turns: turnRows.map((row) => ({
|
|
314
|
-
...row,
|
|
315
|
-
token_usage: row.token_usage ? JSON.parse(row.token_usage) : null,
|
|
316
|
-
items: itemsByTurn.get(row.id) ?? [],
|
|
317
|
-
})),
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return result;
|
|
322
|
-
});
|
|
323
|
-
}
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import Fastify, { type FastifyInstance } from 'fastify';
|
|
3
|
-
import { initDb, getDb } from '../db/index.ts';
|
|
4
|
-
import { registerTurnRoutes } from './turns.ts';
|
|
5
|
-
|
|
6
|
-
// Mock registry to avoid spawning real provider processes
|
|
7
|
-
const mocks = vi.hoisted(() => ({
|
|
8
|
-
startTurn: vi.fn().mockResolvedValue(undefined),
|
|
9
|
-
interruptTurn: vi.fn().mockResolvedValue(true),
|
|
10
|
-
clearThread: vi.fn(),
|
|
11
|
-
clearAllThreads: vi.fn(),
|
|
12
|
-
getModels: vi.fn().mockResolvedValue([]),
|
|
13
|
-
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
vi.mock('../providers/registry.ts', () => ({
|
|
17
|
-
getProvider: vi.fn(() => ({
|
|
18
|
-
startTurn: mocks.startTurn,
|
|
19
|
-
interruptTurn: mocks.interruptTurn,
|
|
20
|
-
clearThread: mocks.clearThread,
|
|
21
|
-
clearAllThreads: mocks.clearAllThreads,
|
|
22
|
-
getModels: mocks.getModels,
|
|
23
|
-
shutdown: mocks.shutdown,
|
|
24
|
-
})),
|
|
25
|
-
hasProvider: vi.fn(() => true),
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
const NOW = '2025-01-01T00:00:00Z';
|
|
29
|
-
|
|
30
|
-
function insertSession(db: ReturnType<typeof getDb>, id: string, overrides: Record<string, unknown> = {}): void {
|
|
31
|
-
db.prepare(
|
|
32
|
-
`INSERT INTO sessions (id, title, runtime_type, model, model_reasoning_effort, cwd, external_thread_id, status, is_running, lifecycle, has_unreviewed, created_at, updated_at)
|
|
33
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
34
|
-
).run(
|
|
35
|
-
id,
|
|
36
|
-
null,
|
|
37
|
-
'codex',
|
|
38
|
-
null,
|
|
39
|
-
null,
|
|
40
|
-
null,
|
|
41
|
-
(overrides['external_thread_id'] as string | null) ?? null,
|
|
42
|
-
(overrides['status'] as string) ?? 'active',
|
|
43
|
-
(overrides['is_running'] as number) ?? 0,
|
|
44
|
-
'working',
|
|
45
|
-
0,
|
|
46
|
-
NOW,
|
|
47
|
-
NOW,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe('Turn routes', () => {
|
|
52
|
-
let app: FastifyInstance;
|
|
53
|
-
|
|
54
|
-
beforeEach(async () => {
|
|
55
|
-
vi.clearAllMocks();
|
|
56
|
-
initDb(':memory:');
|
|
57
|
-
// Mirror the production Fastify config (index.ts) — bodyLimit must exceed the
|
|
58
|
-
// default 1 MiB so base64 image attachments fit in the JSON body.
|
|
59
|
-
app = Fastify({ bodyLimit: 50 * 1024 * 1024 });
|
|
60
|
-
await registerTurnRoutes(app);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe('POST /api/sessions/:id/turns', () => {
|
|
64
|
-
it('returns 400 when user_prompt is missing', async () => {
|
|
65
|
-
const res = await app.inject({ method: 'POST', url: '/api/sessions/s1/turns', payload: {} });
|
|
66
|
-
expect(res.statusCode).toBe(400);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('returns 400 when user_prompt has wrong type', async () => {
|
|
70
|
-
const res = await app.inject({
|
|
71
|
-
method: 'POST',
|
|
72
|
-
url: '/api/sessions/s1/turns',
|
|
73
|
-
payload: { user_prompt: { nested: true } },
|
|
74
|
-
});
|
|
75
|
-
expect(res.statusCode).toBe(400);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('returns 400 when user_prompt is empty', async () => {
|
|
79
|
-
const res = await app.inject({
|
|
80
|
-
method: 'POST',
|
|
81
|
-
url: '/api/sessions/s1/turns',
|
|
82
|
-
payload: { user_prompt: ' ' },
|
|
83
|
-
});
|
|
84
|
-
expect(res.statusCode).toBe(400);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('returns 404 for missing session', async () => {
|
|
88
|
-
const res = await app.inject({
|
|
89
|
-
method: 'POST',
|
|
90
|
-
url: '/api/sessions/nonexistent/turns',
|
|
91
|
-
payload: { user_prompt: 'hello' },
|
|
92
|
-
});
|
|
93
|
-
expect(res.statusCode).toBe(404);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('returns 409 when session is already running', async () => {
|
|
97
|
-
const db = getDb();
|
|
98
|
-
insertSession(db, 's1', { is_running: 1 });
|
|
99
|
-
const res = await app.inject({
|
|
100
|
-
method: 'POST',
|
|
101
|
-
url: '/api/sessions/s1/turns',
|
|
102
|
-
payload: { user_prompt: 'hello' },
|
|
103
|
-
});
|
|
104
|
-
expect(res.statusCode).toBe(409);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('creates a turn and calls startTurn', async () => {
|
|
108
|
-
const db = getDb();
|
|
109
|
-
insertSession(db, 's1');
|
|
110
|
-
mocks.startTurn.mockResolvedValue(undefined);
|
|
111
|
-
const res = await app.inject({
|
|
112
|
-
method: 'POST',
|
|
113
|
-
url: '/api/sessions/s1/turns',
|
|
114
|
-
payload: { user_prompt: 'hello' },
|
|
115
|
-
});
|
|
116
|
-
expect(res.statusCode).toBe(200);
|
|
117
|
-
const turn = res.json();
|
|
118
|
-
expect(turn.id).toBeDefined();
|
|
119
|
-
expect(turn.user_prompt).toBe('hello');
|
|
120
|
-
expect(turn.status).toBe('pending');
|
|
121
|
-
|
|
122
|
-
expect(mocks.startTurn).toHaveBeenCalledTimes(1);
|
|
123
|
-
const [sessionArg, turnArg] = mocks.startTurn.mock.calls[0];
|
|
124
|
-
expect(sessionArg.id).toBe('s1');
|
|
125
|
-
expect(turnArg.user_prompt).toBe('hello');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('accepts a large base64 image attachment (>1 MiB) — regression for 413 Payload Too Large', async () => {
|
|
129
|
-
const db = getDb();
|
|
130
|
-
insertSession(db, 's1');
|
|
131
|
-
// ~2 MB of base64 — exceeds Fastify's default 1 MiB bodyLimit; the production
|
|
132
|
-
// server (index.ts) raises it to 50 MB so image attachments fit.
|
|
133
|
-
const bigBase64 = 'A'.repeat(2 * 1024 * 1024);
|
|
134
|
-
const res = await app.inject({
|
|
135
|
-
method: 'POST',
|
|
136
|
-
url: '/api/sessions/s1/turns',
|
|
137
|
-
payload: {
|
|
138
|
-
user_prompt: 'look at this',
|
|
139
|
-
attachments: [{ name: 'shot.png', media_type: 'image/png', data: bigBase64 }],
|
|
140
|
-
},
|
|
141
|
-
});
|
|
142
|
-
expect(res.statusCode).toBe(200);
|
|
143
|
-
const turn = res.json();
|
|
144
|
-
expect(turn.attachments).toEqual([{ name: 'shot.png', media_type: 'image/png', data: bigBase64 }]);
|
|
145
|
-
const [, turnArg] = mocks.startTurn.mock.calls[0];
|
|
146
|
-
// startTurn receives the raw DB row — attachments is still a JSON string there.
|
|
147
|
-
expect(typeof turnArg.attachments).toBe('string');
|
|
148
|
-
expect(JSON.parse(turnArg.attachments)).toEqual([{ name: 'shot.png', media_type: 'image/png', data: bigBase64 }]);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('returns 413 on the default 1 MiB bodyLimit — documents why index.ts raises it', async () => {
|
|
152
|
-
const db = getDb();
|
|
153
|
-
insertSession(db, 's1');
|
|
154
|
-
const defaultApp = Fastify(); // default bodyLimit = 1 MiB
|
|
155
|
-
await registerTurnRoutes(defaultApp);
|
|
156
|
-
const bigBase64 = 'A'.repeat(2 * 1024 * 1024);
|
|
157
|
-
const res = await defaultApp.inject({
|
|
158
|
-
method: 'POST',
|
|
159
|
-
url: '/api/sessions/s1/turns',
|
|
160
|
-
payload: {
|
|
161
|
-
user_prompt: 'look at this',
|
|
162
|
-
attachments: [{ name: 'shot.png', media_type: 'image/png', data: bigBase64 }],
|
|
163
|
-
},
|
|
164
|
-
});
|
|
165
|
-
expect(res.statusCode).toBe(413);
|
|
166
|
-
expect(mocks.startTurn).not.toHaveBeenCalled();
|
|
167
|
-
await defaultApp.close();
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
describe('POST /api/sessions/:id/cancel', () => {
|
|
172
|
-
it('returns 404 for missing session', async () => {
|
|
173
|
-
const res = await app.inject({ method: 'POST', url: '/api/sessions/nonexistent/cancel' });
|
|
174
|
-
expect(res.statusCode).toBe(404);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('returns 409 when no running turn', async () => {
|
|
178
|
-
const db = getDb();
|
|
179
|
-
insertSession(db, 's1');
|
|
180
|
-
mocks.interruptTurn.mockResolvedValue(false);
|
|
181
|
-
const res = await app.inject({ method: 'POST', url: '/api/sessions/s1/cancel' });
|
|
182
|
-
expect(res.statusCode).toBe(409);
|
|
183
|
-
expect(res.json().error).toBe('No running turn to cancel');
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('returns success when turn is cancelled', async () => {
|
|
187
|
-
const db = getDb();
|
|
188
|
-
insertSession(db, 's1');
|
|
189
|
-
mocks.interruptTurn.mockResolvedValue(true);
|
|
190
|
-
const res = await app.inject({ method: 'POST', url: '/api/sessions/s1/cancel' });
|
|
191
|
-
expect(res.statusCode).toBe(200);
|
|
192
|
-
expect(res.json()).toEqual({ success: true });
|
|
193
|
-
expect(mocks.interruptTurn).toHaveBeenCalledWith('s1');
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
});
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import type { FastifyInstance } from 'fastify';
|
|
2
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
-
import { getDb } from '../db/index.ts';
|
|
4
|
-
import { getProvider } from '../providers/registry.ts';
|
|
5
|
-
import type { Session, Turn, Attachment } from '../db/schema.ts';
|
|
6
|
-
|
|
7
|
-
export async function registerTurnRoutes(fastify: FastifyInstance): Promise<void> {
|
|
8
|
-
// POST /api/sessions/:id/turns — submit prompt, starts a turn
|
|
9
|
-
fastify.post<{
|
|
10
|
-
Params: { id: string };
|
|
11
|
-
Body: { user_prompt: string; attachments?: Attachment[] };
|
|
12
|
-
}>(
|
|
13
|
-
'/api/sessions/:id/turns',
|
|
14
|
-
{
|
|
15
|
-
schema: {
|
|
16
|
-
body: {
|
|
17
|
-
type: 'object',
|
|
18
|
-
required: ['user_prompt'],
|
|
19
|
-
properties: {
|
|
20
|
-
user_prompt: { type: 'string', minLength: 1 },
|
|
21
|
-
attachments: {
|
|
22
|
-
type: 'array',
|
|
23
|
-
items: {
|
|
24
|
-
type: 'object',
|
|
25
|
-
required: ['name', 'media_type', 'data'],
|
|
26
|
-
properties: {
|
|
27
|
-
name: { type: 'string' },
|
|
28
|
-
media_type: { type: 'string' },
|
|
29
|
-
data: { type: 'string' },
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
async (request, reply) => {
|
|
38
|
-
const { id } = request.params;
|
|
39
|
-
const { user_prompt, attachments } = request.body || {};
|
|
40
|
-
|
|
41
|
-
if (!user_prompt || !user_prompt.trim()) {
|
|
42
|
-
return reply.code(400).send({ error: 'user_prompt is required' });
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const db = getDb();
|
|
46
|
-
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session | undefined;
|
|
47
|
-
if (!session) {
|
|
48
|
-
return reply.code(404).send({ error: 'Session not found' });
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (session.is_running) {
|
|
52
|
-
return reply.code(409).send({ error: 'A turn is already running for this session' });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Create turn in DB — store attachments as JSON if provided
|
|
56
|
-
const turnId = uuidv4();
|
|
57
|
-
const now = new Date().toISOString();
|
|
58
|
-
const attachmentsJson = attachments && attachments.length > 0 ? JSON.stringify(attachments) : null;
|
|
59
|
-
db.prepare(
|
|
60
|
-
`INSERT INTO turns (id, session_id, user_prompt, agent_response, thoughts, token_usage, external_turn_id, status, error, attachments, created_at, updated_at)
|
|
61
|
-
VALUES (?, ?, ?, NULL, NULL, NULL, NULL, 'pending', NULL, ?, ?, ?)`,
|
|
62
|
-
).run(turnId, id, user_prompt, attachmentsJson, now, now);
|
|
63
|
-
|
|
64
|
-
const turn = db.prepare('SELECT * FROM turns WHERE id = ?').get(turnId) as Turn;
|
|
65
|
-
|
|
66
|
-
// Parse attachments from JSON string (DB stores as TEXT) before returning to frontend
|
|
67
|
-
const rawTurn = turn as Turn & { attachments: string | null };
|
|
68
|
-
const parsedTurn: Turn = {
|
|
69
|
-
...turn,
|
|
70
|
-
attachments: rawTurn.attachments ? (JSON.parse(rawTurn.attachments) as Attachment[]) : null,
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// Fire and forget — events stream via WebSocket
|
|
74
|
-
getProvider(session.runtime_type)
|
|
75
|
-
.startTurn(session, rawTurn)
|
|
76
|
-
.catch((err) => {
|
|
77
|
-
console.error(`[provider] Unhandled error for session ${id}:`, err);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
return parsedTurn;
|
|
81
|
-
},
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
// POST /api/sessions/:id/cancel — cancel running turn
|
|
85
|
-
fastify.post<{ Params: { id: string } }>('/api/sessions/:id/cancel', async (request, reply) => {
|
|
86
|
-
const { id } = request.params;
|
|
87
|
-
const db = getDb();
|
|
88
|
-
|
|
89
|
-
// Look up session to get runtime_type for provider dispatch
|
|
90
|
-
const session = db.prepare('SELECT runtime_type FROM sessions WHERE id = ?').get(id) as
|
|
91
|
-
| { runtime_type: string }
|
|
92
|
-
| undefined;
|
|
93
|
-
if (!session) {
|
|
94
|
-
return reply.code(404).send({ error: 'Session not found' });
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const cancelled = await getProvider(session.runtime_type).interruptTurn(id);
|
|
98
|
-
if (!cancelled) {
|
|
99
|
-
return reply.code(409).send({ error: 'No running turn to cancel' });
|
|
100
|
-
}
|
|
101
|
-
return { success: true };
|
|
102
|
-
});
|
|
103
|
-
}
|