@otto-assistant/bridge 0.4.92
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/bin.js +2 -0
- package/dist/agent-model.e2e.test.js +755 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/anthropic-auth-plugin.js +728 -0
- package/dist/anthropic-auth-plugin.test.js +125 -0
- package/dist/anthropic-auth-state.js +231 -0
- package/dist/bin.js +90 -0
- package/dist/channel-management.js +227 -0
- package/dist/cli-parsing.test.js +137 -0
- package/dist/cli-send-thread.e2e.test.js +356 -0
- package/dist/cli.js +3276 -0
- package/dist/commands/abort.js +65 -0
- package/dist/commands/action-buttons.js +245 -0
- package/dist/commands/add-project.js +113 -0
- package/dist/commands/agent.js +335 -0
- package/dist/commands/ask-question.js +274 -0
- package/dist/commands/btw.js +116 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +130 -0
- package/dist/commands/diff.js +63 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork.js +220 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +885 -0
- package/dist/commands/mcp.js +239 -0
- package/dist/commands/memory-snapshot.js +24 -0
- package/dist/commands/mention-mode.js +44 -0
- package/dist/commands/merge-worktree.js +159 -0
- package/dist/commands/model-variant.js +364 -0
- package/dist/commands/model.js +776 -0
- package/dist/commands/new-worktree.js +366 -0
- package/dist/commands/paginated-select.js +57 -0
- package/dist/commands/permissions.js +274 -0
- package/dist/commands/queue.js +206 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +127 -0
- package/dist/commands/resume.js +149 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/screenshare.js +303 -0
- package/dist/commands/screenshare.test.js +20 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +176 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/tasks.js +205 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +305 -0
- package/dist/commands/unset-model.js +138 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +155 -0
- package/dist/commands/verbosity.js +125 -0
- package/dist/commands/worktree-settings.js +43 -0
- package/dist/commands/worktrees.js +410 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +94 -0
- package/dist/context-awareness-plugin.js +363 -0
- package/dist/context-awareness-plugin.test.js +124 -0
- package/dist/critique-utils.js +95 -0
- package/dist/database.js +1310 -0
- package/dist/db.js +251 -0
- package/dist/db.test.js +138 -0
- package/dist/debounce-timeout.js +28 -0
- package/dist/debounced-process-flush.js +77 -0
- package/dist/discord-bot.js +1008 -0
- package/dist/discord-command-registration.js +524 -0
- package/dist/discord-urls.js +81 -0
- package/dist/discord-utils.js +591 -0
- package/dist/discord-utils.test.js +134 -0
- package/dist/errors.js +157 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/event-stream-real-capture.e2e.test.js +533 -0
- package/dist/eventsource-parser.test.js +327 -0
- package/dist/exec-async.js +26 -0
- package/dist/external-opencode-sync.js +480 -0
- package/dist/format-tables.js +302 -0
- package/dist/format-tables.test.js +308 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +113 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
- package/dist/gateway-proxy.e2e.test.js +483 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +37 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +52 -0
- package/dist/generated/internal/class.js +49 -0
- package/dist/generated/internal/prismaNamespace.js +253 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_events.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +122 -0
- package/dist/hrana-server.js +263 -0
- package/dist/hrana-server.test.js +370 -0
- package/dist/html-actions.js +123 -0
- package/dist/html-actions.test.js +70 -0
- package/dist/html-components.js +117 -0
- package/dist/html-components.test.js +34 -0
- package/dist/image-optimizer-plugin.js +153 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +397 -0
- package/dist/ipc-polling.js +252 -0
- package/dist/ipc-tools-plugin.js +193 -0
- package/dist/kimaki-digital-twin.e2e.test.js +161 -0
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
- package/dist/kimaki-opencode-plugin.js +17 -0
- package/dist/kimaki-opencode-plugin.test.js +98 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +165 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +257 -0
- package/dist/message-finish-field.e2e.test.js +165 -0
- package/dist/message-formatting.js +413 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/message-preprocessing.js +330 -0
- package/dist/onboarding-tutorial.js +172 -0
- package/dist/onboarding-welcome.js +37 -0
- package/dist/openai-realtime.js +224 -0
- package/dist/opencode-command-detection.js +65 -0
- package/dist/opencode-command-detection.test.js +240 -0
- package/dist/opencode-command.js +129 -0
- package/dist/opencode-command.test.js +48 -0
- package/dist/opencode-interrupt-plugin.js +361 -0
- package/dist/opencode-interrupt-plugin.test.js +458 -0
- package/dist/opencode.js +861 -0
- package/dist/otto/branding.js +22 -0
- package/dist/otto/index.js +21 -0
- package/dist/parse-permission-rules.test.js +117 -0
- package/dist/patch-text-parser.js +97 -0
- package/dist/plugin-logger.js +59 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/queue-advanced-abort.e2e.test.js +293 -0
- package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
- package/dist/queue-advanced-e2e-setup.js +786 -0
- package/dist/queue-advanced-footer.e2e.test.js +472 -0
- package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
- package/dist/queue-advanced-question.e2e.test.js +261 -0
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
- package/dist/queue-advanced-typing.e2e.test.js +153 -0
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
- package/dist/queue-interrupt-drain.e2e.test.js +135 -0
- package/dist/queue-question-select-drain.e2e.test.js +120 -0
- package/dist/runtime-idle-sweeper.js +52 -0
- package/dist/runtime-lifecycle.e2e.test.js +508 -0
- package/dist/sentry.js +23 -0
- package/dist/session-handler/agent-utils.js +67 -0
- package/dist/session-handler/event-stream-state.js +420 -0
- package/dist/session-handler/event-stream-state.test.js +563 -0
- package/dist/session-handler/model-utils.js +124 -0
- package/dist/session-handler/opencode-session-event-log.js +94 -0
- package/dist/session-handler/thread-runtime-state.js +104 -0
- package/dist/session-handler/thread-session-runtime.js +3258 -0
- package/dist/session-handler.js +9 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/session-title-rename.test.js +80 -0
- package/dist/startup-service.js +153 -0
- package/dist/startup-time.e2e.test.js +296 -0
- package/dist/store.js +17 -0
- package/dist/system-message.js +613 -0
- package/dist/system-message.test.js +602 -0
- package/dist/task-runner.js +295 -0
- package/dist/task-schedule.js +209 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/test-utils.js +299 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +999 -0
- package/dist/tools.js +357 -0
- package/dist/undo-redo.e2e.test.js +161 -0
- package/dist/unnest-code-blocks.js +146 -0
- package/dist/unnest-code-blocks.test.js +673 -0
- package/dist/upgrade.js +114 -0
- package/dist/utils.js +144 -0
- package/dist/voice-attachment.js +34 -0
- package/dist/voice-handler.js +646 -0
- package/dist/voice-message.e2e.test.js +1021 -0
- package/dist/voice.js +447 -0
- package/dist/voice.test.js +235 -0
- package/dist/wait-session.js +94 -0
- package/dist/websockify.js +69 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-lifecycle.e2e.test.js +308 -0
- package/dist/worktree-utils.js +3 -0
- package/dist/worktrees.js +929 -0
- package/dist/worktrees.test.js +189 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +98 -0
- package/schema.prisma +295 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +112 -0
- package/skills/egaki/SKILL.md +100 -0
- package/skills/errore/SKILL.md +647 -0
- package/skills/event-sourcing-state/SKILL.md +252 -0
- package/skills/gitchamber/SKILL.md +93 -0
- package/skills/goke/SKILL.md +644 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/lintcn/SKILL.md +873 -0
- package/skills/new-skill/SKILL.md +211 -0
- package/skills/npm-package/SKILL.md +239 -0
- package/skills/playwriter/SKILL.md +35 -0
- package/skills/proxyman/SKILL.md +215 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/spiceflow/SKILL.md +14 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/usecomputer/SKILL.md +264 -0
- package/skills/x-articles/SKILL.md +554 -0
- package/skills/zele/SKILL.md +112 -0
- package/skills/zustand-centralized-state/SKILL.md +1004 -0
- package/src/agent-model.e2e.test.ts +976 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +283 -0
- package/src/ai-tool.ts +39 -0
- package/src/anthropic-auth-plugin.test.ts +159 -0
- package/src/anthropic-auth-plugin.ts +861 -0
- package/src/anthropic-auth-state.ts +282 -0
- package/src/bin.ts +111 -0
- package/src/channel-management.ts +334 -0
- package/src/cli-parsing.test.ts +195 -0
- package/src/cli-send-thread.e2e.test.ts +464 -0
- package/src/cli.ts +4581 -0
- package/src/commands/abort.ts +89 -0
- package/src/commands/action-buttons.ts +364 -0
- package/src/commands/add-project.ts +149 -0
- package/src/commands/agent.ts +473 -0
- package/src/commands/ask-question.ts +390 -0
- package/src/commands/btw.ts +164 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +190 -0
- package/src/commands/diff.ts +91 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork.ts +321 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +1173 -0
- package/src/commands/mcp.ts +307 -0
- package/src/commands/memory-snapshot.ts +30 -0
- package/src/commands/mention-mode.ts +68 -0
- package/src/commands/merge-worktree.ts +223 -0
- package/src/commands/model-variant.ts +483 -0
- package/src/commands/model.ts +1053 -0
- package/src/commands/new-worktree.ts +510 -0
- package/src/commands/paginated-select.ts +81 -0
- package/src/commands/permissions.ts +397 -0
- package/src/commands/queue.ts +271 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +230 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/screenshare.test.ts +30 -0
- package/src/commands/screenshare.ts +366 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +227 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/tasks.ts +293 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +386 -0
- package/src/commands/unset-model.ts +173 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +198 -0
- package/src/commands/verbosity.ts +173 -0
- package/src/commands/worktree-settings.ts +70 -0
- package/src/commands/worktrees.ts +552 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +111 -0
- package/src/context-awareness-plugin.test.ts +142 -0
- package/src/context-awareness-plugin.ts +510 -0
- package/src/critique-utils.ts +139 -0
- package/src/database.ts +1876 -0
- package/src/db.test.ts +162 -0
- package/src/db.ts +286 -0
- package/src/debounce-timeout.ts +43 -0
- package/src/debounced-process-flush.ts +104 -0
- package/src/discord-bot.ts +1330 -0
- package/src/discord-command-registration.ts +693 -0
- package/src/discord-urls.ts +88 -0
- package/src/discord-utils.test.ts +153 -0
- package/src/discord-utils.ts +800 -0
- package/src/errors.ts +201 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/event-stream-real-capture.e2e.test.ts +692 -0
- package/src/eventsource-parser.test.ts +351 -0
- package/src/exec-async.ts +35 -0
- package/src/external-opencode-sync.ts +685 -0
- package/src/format-tables.test.ts +335 -0
- package/src/format-tables.ts +445 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +172 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
- package/src/gateway-proxy.e2e.test.ts +640 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +114 -0
- package/src/generated/client.ts +138 -0
- package/src/generated/commonInputTypes.ts +736 -0
- package/src/generated/enums.ts +88 -0
- package/src/generated/internal/class.ts +384 -0
- package/src/generated/internal/prismaNamespace.ts +2386 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1656 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +1859 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1228 -0
- package/src/generated/models/channel_worktrees.ts +1300 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_events.ts +1439 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1781 -0
- package/src/generated/models/thread_worktrees.ts +1356 -0
- package/src/generated/models.ts +30 -0
- package/src/heap-monitor.ts +152 -0
- package/src/hrana-server.test.ts +434 -0
- package/src/hrana-server.ts +314 -0
- package/src/html-actions.test.ts +87 -0
- package/src/html-actions.ts +174 -0
- package/src/html-components.test.ts +38 -0
- package/src/html-components.ts +181 -0
- package/src/image-optimizer-plugin.ts +194 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +576 -0
- package/src/ipc-polling.ts +326 -0
- package/src/ipc-tools-plugin.ts +236 -0
- package/src/kimaki-digital-twin.e2e.test.ts +199 -0
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
- package/src/kimaki-opencode-plugin.test.ts +108 -0
- package/src/kimaki-opencode-plugin.ts +18 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +208 -0
- package/src/markdown.test.ts +308 -0
- package/src/markdown.ts +410 -0
- package/src/message-finish-field.e2e.test.ts +192 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +533 -0
- package/src/message-preprocessing.ts +455 -0
- package/src/onboarding-tutorial.ts +176 -0
- package/src/onboarding-welcome.ts +49 -0
- package/src/openai-realtime.ts +358 -0
- package/src/opencode-command-detection.test.ts +307 -0
- package/src/opencode-command-detection.ts +76 -0
- package/src/opencode-command.test.ts +70 -0
- package/src/opencode-command.ts +188 -0
- package/src/opencode-interrupt-plugin.test.ts +677 -0
- package/src/opencode-interrupt-plugin.ts +477 -0
- package/src/opencode.ts +1110 -0
- package/src/otto/branding.ts +23 -0
- package/src/otto/index.ts +22 -0
- package/src/parse-permission-rules.test.ts +127 -0
- package/src/patch-text-parser.ts +107 -0
- package/src/plugin-logger.ts +68 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/queue-advanced-abort.e2e.test.ts +382 -0
- package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
- package/src/queue-advanced-e2e-setup.ts +873 -0
- package/src/queue-advanced-footer.e2e.test.ts +576 -0
- package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
- package/src/queue-advanced-question.e2e.test.ts +316 -0
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
- package/src/queue-advanced-typing.e2e.test.ts +199 -0
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
- package/src/queue-interrupt-drain.e2e.test.ts +166 -0
- package/src/queue-question-select-drain.e2e.test.ts +152 -0
- package/src/runtime-idle-sweeper.ts +76 -0
- package/src/runtime-lifecycle.e2e.test.ts +641 -0
- package/src/schema.sql +173 -0
- package/src/sentry.ts +26 -0
- package/src/session-handler/agent-utils.ts +97 -0
- package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
- package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
- package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
- package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
- package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
- package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
- package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
- package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
- package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
- package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
- package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
- package/src/session-handler/event-stream-state.test.ts +645 -0
- package/src/session-handler/event-stream-state.ts +608 -0
- package/src/session-handler/model-utils.ts +183 -0
- package/src/session-handler/opencode-session-event-log.ts +130 -0
- package/src/session-handler/thread-runtime-state.ts +212 -0
- package/src/session-handler/thread-session-runtime.ts +4281 -0
- package/src/session-handler.ts +15 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/session-title-rename.test.ts +112 -0
- package/src/startup-service.ts +200 -0
- package/src/startup-time.e2e.test.ts +373 -0
- package/src/store.ts +122 -0
- package/src/system-message.test.ts +612 -0
- package/src/system-message.ts +723 -0
- package/src/task-runner.ts +421 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +311 -0
- package/src/test-utils.ts +435 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +1219 -0
- package/src/tools.ts +430 -0
- package/src/undici.d.ts +12 -0
- package/src/undo-redo.e2e.test.ts +209 -0
- package/src/unnest-code-blocks.test.ts +713 -0
- package/src/unnest-code-blocks.ts +185 -0
- package/src/upgrade.ts +127 -0
- package/src/utils.ts +212 -0
- package/src/voice-attachment.ts +51 -0
- package/src/voice-handler.ts +908 -0
- package/src/voice-message.e2e.test.ts +1255 -0
- package/src/voice.test.ts +281 -0
- package/src/voice.ts +627 -0
- package/src/wait-session.ts +147 -0
- package/src/websockify.ts +101 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-lifecycle.e2e.test.ts +391 -0
- package/src/worktree-utils.ts +4 -0
- package/src/worktrees.test.ts +223 -0
- package/src/worktrees.ts +1294 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
// Fixture-driven tests for pure event-stream derivation helpers.
|
|
2
|
+
// Focuses on assistant message completion boundaries instead of session.idle.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
6
|
+
import { getOpencodeEventSessionId, } from './opencode-session-event-log.js';
|
|
7
|
+
import { getAssistantMessageIdsForLatestUserTurn, getCurrentTurnStartTime, getDerivedSubtaskIndex, getLatestAssistantMessageIdForLatestUserTurn, getLatestRunInfo, hasAssistantMessageCompletedBefore, doesLatestUserTurnHaveNaturalCompletion, isAssistantMessageInLatestUserTurn, isAssistantMessageNaturalCompletion, isSessionBusy, } from './event-stream-state.js';
|
|
8
|
+
const fixturesDir = path.join(import.meta.dirname, 'event-stream-fixtures');
|
|
9
|
+
function loadFixture(filename) {
|
|
10
|
+
const content = fs.readFileSync(path.join(fixturesDir, filename), 'utf8');
|
|
11
|
+
return content
|
|
12
|
+
.split('\n')
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.map((line) => {
|
|
15
|
+
const parsed = JSON.parse(line);
|
|
16
|
+
return { event: parsed.event, timestamp: parsed.timestamp };
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function getSessionId(events) {
|
|
20
|
+
for (const entry of events) {
|
|
21
|
+
const sessionId = getOpencodeEventSessionId(entry.event);
|
|
22
|
+
if (sessionId) {
|
|
23
|
+
return sessionId;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
throw new Error('No sessionId found in fixture');
|
|
27
|
+
}
|
|
28
|
+
function getAssistantMessages(events, sessionId) {
|
|
29
|
+
const messagesById = new Map();
|
|
30
|
+
events.forEach((entry) => {
|
|
31
|
+
if (entry.event.type !== 'message.updated') {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const info = entry.event.properties.info;
|
|
35
|
+
if (info.sessionID !== sessionId || info.role !== 'assistant') {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
messagesById.set(info.id, info);
|
|
39
|
+
});
|
|
40
|
+
return [...messagesById.values()];
|
|
41
|
+
}
|
|
42
|
+
function getAssistantMessageById({ events, sessionId, messageId, }) {
|
|
43
|
+
const message = getAssistantMessages(events, sessionId).find((candidate) => {
|
|
44
|
+
return candidate.id === messageId;
|
|
45
|
+
});
|
|
46
|
+
if (!message) {
|
|
47
|
+
throw new Error(`Assistant message ${messageId} not found`);
|
|
48
|
+
}
|
|
49
|
+
return message;
|
|
50
|
+
}
|
|
51
|
+
function findAssistantCompletionEventIndex({ events, sessionId, messageId, }) {
|
|
52
|
+
const index = events.findIndex((entry) => {
|
|
53
|
+
if (entry.event.type !== 'message.updated') {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const info = entry.event.properties.info;
|
|
57
|
+
return info.sessionID === sessionId
|
|
58
|
+
&& info.role === 'assistant'
|
|
59
|
+
&& info.id === messageId
|
|
60
|
+
&& typeof info.time.completed === 'number';
|
|
61
|
+
});
|
|
62
|
+
if (index === -1) {
|
|
63
|
+
throw new Error(`Completed assistant message ${messageId} not found`);
|
|
64
|
+
}
|
|
65
|
+
return index;
|
|
66
|
+
}
|
|
67
|
+
describe('session-normal-completion', () => {
|
|
68
|
+
const events = loadFixture('session-normal-completion.jsonl');
|
|
69
|
+
const sessionId = getSessionId(events);
|
|
70
|
+
const latestAssistantMessageId = getLatestAssistantMessageIdForLatestUserTurn({
|
|
71
|
+
events,
|
|
72
|
+
sessionId,
|
|
73
|
+
});
|
|
74
|
+
test('latest assistant message completes naturally', () => {
|
|
75
|
+
if (!latestAssistantMessageId) {
|
|
76
|
+
throw new Error('Expected latest assistant message');
|
|
77
|
+
}
|
|
78
|
+
const message = getAssistantMessageById({
|
|
79
|
+
events,
|
|
80
|
+
sessionId,
|
|
81
|
+
messageId: latestAssistantMessageId,
|
|
82
|
+
});
|
|
83
|
+
expect(isAssistantMessageNaturalCompletion({ message })).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
test('latest user turn start time comes from the latest user message', () => {
|
|
86
|
+
expect(getCurrentTurnStartTime({ events, sessionId })).toBe(1772636294845);
|
|
87
|
+
});
|
|
88
|
+
test('completion history only appears after the completed update lands', () => {
|
|
89
|
+
if (!latestAssistantMessageId) {
|
|
90
|
+
throw new Error('Expected latest assistant message');
|
|
91
|
+
}
|
|
92
|
+
const completionIndex = findAssistantCompletionEventIndex({
|
|
93
|
+
events,
|
|
94
|
+
sessionId,
|
|
95
|
+
messageId: latestAssistantMessageId,
|
|
96
|
+
});
|
|
97
|
+
expect(hasAssistantMessageCompletedBefore({
|
|
98
|
+
events,
|
|
99
|
+
sessionId,
|
|
100
|
+
messageId: latestAssistantMessageId,
|
|
101
|
+
upToIndex: completionIndex - 1,
|
|
102
|
+
})).toBe(false);
|
|
103
|
+
expect(hasAssistantMessageCompletedBefore({
|
|
104
|
+
events,
|
|
105
|
+
sessionId,
|
|
106
|
+
messageId: latestAssistantMessageId,
|
|
107
|
+
})).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
test('getLatestRunInfo', () => {
|
|
110
|
+
expect(getLatestRunInfo({ events, sessionId })).toEqual({
|
|
111
|
+
model: 'deterministic-v2',
|
|
112
|
+
providerID: 'deterministic-provider',
|
|
113
|
+
agent: 'build',
|
|
114
|
+
tokensUsed: 2,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe('session-explicit-abort', () => {
|
|
119
|
+
const events = loadFixture('session-explicit-abort.jsonl');
|
|
120
|
+
const sessionId = getSessionId(events);
|
|
121
|
+
const assistantMessages = getAssistantMessages(events, sessionId);
|
|
122
|
+
const latestAssistant = assistantMessages[assistantMessages.length - 1];
|
|
123
|
+
test('aborted assistant message is not a natural completion', () => {
|
|
124
|
+
if (!latestAssistant) {
|
|
125
|
+
throw new Error('Expected assistant message in fixture');
|
|
126
|
+
}
|
|
127
|
+
expect(isAssistantMessageNaturalCompletion({ message: latestAssistant })).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe('session-user-interruption', () => {
|
|
131
|
+
const events = loadFixture('session-user-interruption.jsonl');
|
|
132
|
+
const sessionId = getSessionId(events);
|
|
133
|
+
const firstAssistantId = 'msg_cb95be135001I1vqtzLtT4Q1iQ';
|
|
134
|
+
const slowSleepAssistantId = 'msg_cb95be39e001huREyY2wfjgV1M';
|
|
135
|
+
const followupAssistantId = 'msg_cb95beeb8001MuEOER9WprXsPC';
|
|
136
|
+
test('latest user turn only includes the follow-up assistant message', () => {
|
|
137
|
+
expect(isAssistantMessageInLatestUserTurn({
|
|
138
|
+
events,
|
|
139
|
+
sessionId,
|
|
140
|
+
messageId: firstAssistantId,
|
|
141
|
+
})).toBe(false);
|
|
142
|
+
expect(isAssistantMessageInLatestUserTurn({
|
|
143
|
+
events,
|
|
144
|
+
sessionId,
|
|
145
|
+
messageId: slowSleepAssistantId,
|
|
146
|
+
})).toBe(false);
|
|
147
|
+
expect(isAssistantMessageInLatestUserTurn({
|
|
148
|
+
events,
|
|
149
|
+
sessionId,
|
|
150
|
+
messageId: followupAssistantId,
|
|
151
|
+
})).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
test('latest user turn start time follows the follow-up user message', () => {
|
|
154
|
+
expect(getCurrentTurnStartTime({ events, sessionId })).toBe(1772636335777);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('session-two-completions-same-session', () => {
|
|
158
|
+
const events = loadFixture('session-two-completions-same-session.jsonl');
|
|
159
|
+
const sessionId = getSessionId(events);
|
|
160
|
+
const assistantMessages = getAssistantMessages(events, sessionId);
|
|
161
|
+
const firstAssistant = assistantMessages[0];
|
|
162
|
+
const secondAssistant = assistantMessages[1];
|
|
163
|
+
test('latest user turn points at the second completion only', () => {
|
|
164
|
+
if (!firstAssistant || !secondAssistant) {
|
|
165
|
+
throw new Error('Expected two assistant messages in fixture');
|
|
166
|
+
}
|
|
167
|
+
expect(isAssistantMessageInLatestUserTurn({
|
|
168
|
+
events,
|
|
169
|
+
sessionId,
|
|
170
|
+
messageId: firstAssistant.id,
|
|
171
|
+
})).toBe(false);
|
|
172
|
+
expect(isAssistantMessageInLatestUserTurn({
|
|
173
|
+
events,
|
|
174
|
+
sessionId,
|
|
175
|
+
messageId: secondAssistant.id,
|
|
176
|
+
})).toBe(true);
|
|
177
|
+
expect(getLatestAssistantMessageIdForLatestUserTurn({
|
|
178
|
+
events,
|
|
179
|
+
sessionId,
|
|
180
|
+
})).toBe(secondAssistant.id);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
describe('session-concurrent-messages-serialized', () => {
|
|
184
|
+
const events = loadFixture('session-concurrent-messages-serialized.jsonl');
|
|
185
|
+
const sessionId = getSessionId(events);
|
|
186
|
+
const latestAssistantMessageId = getLatestAssistantMessageIdForLatestUserTurn({
|
|
187
|
+
events,
|
|
188
|
+
sessionId,
|
|
189
|
+
});
|
|
190
|
+
test('fixture latest turn is still incomplete even though an older turn completed', () => {
|
|
191
|
+
expect(doesLatestUserTurnHaveNaturalCompletion({
|
|
192
|
+
events,
|
|
193
|
+
sessionId,
|
|
194
|
+
})).toBe(false);
|
|
195
|
+
if (!latestAssistantMessageId) {
|
|
196
|
+
throw new Error('Expected latest assistant message');
|
|
197
|
+
}
|
|
198
|
+
const message = getAssistantMessageById({
|
|
199
|
+
events,
|
|
200
|
+
sessionId,
|
|
201
|
+
messageId: latestAssistantMessageId,
|
|
202
|
+
});
|
|
203
|
+
expect(message.id).toBe(latestAssistantMessageId);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
describe('session-tool-call-noisy-stream', () => {
|
|
207
|
+
const events = loadFixture('session-tool-call-noisy-stream.jsonl');
|
|
208
|
+
const sessionId = getSessionId(events);
|
|
209
|
+
const latestAssistantMessageId = getLatestAssistantMessageIdForLatestUserTurn({
|
|
210
|
+
events,
|
|
211
|
+
sessionId,
|
|
212
|
+
});
|
|
213
|
+
test('fixture ends busy on a tool-call handoff message', () => {
|
|
214
|
+
expect(isSessionBusy({ events, sessionId })).toBe(true);
|
|
215
|
+
if (!latestAssistantMessageId) {
|
|
216
|
+
throw new Error('Expected latest assistant message');
|
|
217
|
+
}
|
|
218
|
+
const message = getAssistantMessageById({
|
|
219
|
+
events,
|
|
220
|
+
sessionId,
|
|
221
|
+
messageId: latestAssistantMessageId,
|
|
222
|
+
});
|
|
223
|
+
expect(isAssistantMessageNaturalCompletion({ message })).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
test('getLatestRunInfo still works through dense tool events', () => {
|
|
226
|
+
expect(getLatestRunInfo({ events, sessionId })).toEqual({
|
|
227
|
+
model: 'deterministic-v2',
|
|
228
|
+
providerID: 'deterministic-provider',
|
|
229
|
+
agent: 'build',
|
|
230
|
+
tokensUsed: 0,
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
describe('session-voice-queued-followup', () => {
|
|
235
|
+
const events = loadFixture('session-voice-queued-followup.jsonl');
|
|
236
|
+
const sessionId = getSessionId(events);
|
|
237
|
+
test('latest user turn start moves to the queued follow-up', () => {
|
|
238
|
+
expect(getCurrentTurnStartTime({ events, sessionId })).toBe(1772636414577);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
describe('synthetic-question-followup', () => {
|
|
242
|
+
const sessionId = 'ses_question';
|
|
243
|
+
const events = [
|
|
244
|
+
{
|
|
245
|
+
timestamp: 1,
|
|
246
|
+
event: {
|
|
247
|
+
type: 'message.updated',
|
|
248
|
+
properties: {
|
|
249
|
+
sessionID: sessionId,
|
|
250
|
+
info: {
|
|
251
|
+
id: 'msg_user_1',
|
|
252
|
+
sessionID: sessionId,
|
|
253
|
+
role: 'user',
|
|
254
|
+
time: { created: 1 },
|
|
255
|
+
agent: 'build',
|
|
256
|
+
model: {
|
|
257
|
+
providerID: 'deterministic-provider',
|
|
258
|
+
modelID: 'deterministic-v2',
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
timestamp: 2,
|
|
266
|
+
event: {
|
|
267
|
+
type: 'message.updated',
|
|
268
|
+
properties: {
|
|
269
|
+
sessionID: sessionId,
|
|
270
|
+
info: {
|
|
271
|
+
id: 'msg_asst_1',
|
|
272
|
+
sessionID: sessionId,
|
|
273
|
+
role: 'assistant',
|
|
274
|
+
time: { created: 2, completed: 3 },
|
|
275
|
+
parentID: 'msg_user_1',
|
|
276
|
+
modelID: 'deterministic-v2',
|
|
277
|
+
providerID: 'deterministic-provider',
|
|
278
|
+
mode: 'build',
|
|
279
|
+
agent: 'build',
|
|
280
|
+
path: { cwd: '/test', root: '/test' },
|
|
281
|
+
cost: 0,
|
|
282
|
+
tokens: {
|
|
283
|
+
input: 1,
|
|
284
|
+
output: 1,
|
|
285
|
+
reasoning: 0,
|
|
286
|
+
cache: { read: 0, write: 0 },
|
|
287
|
+
},
|
|
288
|
+
finish: 'stop',
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
timestamp: 4,
|
|
295
|
+
event: {
|
|
296
|
+
type: 'message.updated',
|
|
297
|
+
properties: {
|
|
298
|
+
sessionID: sessionId,
|
|
299
|
+
info: {
|
|
300
|
+
id: 'msg_user_2',
|
|
301
|
+
sessionID: sessionId,
|
|
302
|
+
role: 'user',
|
|
303
|
+
time: { created: 4 },
|
|
304
|
+
agent: 'build',
|
|
305
|
+
model: {
|
|
306
|
+
providerID: 'deterministic-provider',
|
|
307
|
+
modelID: 'deterministic-v2',
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
];
|
|
314
|
+
test('latest user turn flips immediately after the follow-up user message', () => {
|
|
315
|
+
expect(isAssistantMessageInLatestUserTurn({
|
|
316
|
+
events,
|
|
317
|
+
sessionId,
|
|
318
|
+
messageId: 'msg_asst_1',
|
|
319
|
+
})).toBe(false);
|
|
320
|
+
expect(getCurrentTurnStartTime({ events, sessionId })).toBe(4);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
describe('real-session-task-normal', () => {
|
|
324
|
+
const events = loadFixture('real-session-task-normal.jsonl');
|
|
325
|
+
const sessionId = getSessionId(events);
|
|
326
|
+
const latestAssistantMessageId = getLatestAssistantMessageIdForLatestUserTurn({
|
|
327
|
+
events,
|
|
328
|
+
sessionId,
|
|
329
|
+
});
|
|
330
|
+
test('latest assistant completion is terminal', () => {
|
|
331
|
+
if (!latestAssistantMessageId) {
|
|
332
|
+
throw new Error('Expected latest assistant message');
|
|
333
|
+
}
|
|
334
|
+
const message = getAssistantMessageById({
|
|
335
|
+
events,
|
|
336
|
+
sessionId,
|
|
337
|
+
messageId: latestAssistantMessageId,
|
|
338
|
+
});
|
|
339
|
+
expect(isAssistantMessageNaturalCompletion({ message })).toBe(true);
|
|
340
|
+
});
|
|
341
|
+
test('getLatestRunInfo has model info', () => {
|
|
342
|
+
expect(getLatestRunInfo({ events, sessionId })).toEqual({
|
|
343
|
+
model: 'gemini-2.5-flash',
|
|
344
|
+
providerID: 'cached-google-real-events',
|
|
345
|
+
agent: 'build',
|
|
346
|
+
tokensUsed: 39025,
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
describe('real-session-task-user-interruption', () => {
|
|
351
|
+
const events = loadFixture('real-session-task-user-interruption.jsonl');
|
|
352
|
+
const sessionId = getSessionId(events);
|
|
353
|
+
const childSessionId = 'ses_3464f3a1dffeBBD0d15EqnGjAh';
|
|
354
|
+
const firstAssistantId = 'msg_cb9b0ba96001SpPjgzxWPmRuW9';
|
|
355
|
+
const secondAssistantId = 'msg_cb9b1ae5c001E5G3Ql6aXNpst2';
|
|
356
|
+
test('tool-call handoff assistant is not a natural completion but the resumed reply is', () => {
|
|
357
|
+
const firstAssistant = getAssistantMessageById({
|
|
358
|
+
events,
|
|
359
|
+
sessionId,
|
|
360
|
+
messageId: firstAssistantId,
|
|
361
|
+
});
|
|
362
|
+
const secondAssistant = getAssistantMessageById({
|
|
363
|
+
events,
|
|
364
|
+
sessionId,
|
|
365
|
+
messageId: secondAssistantId,
|
|
366
|
+
});
|
|
367
|
+
// The first message finished with tool-calls — not a natural completion
|
|
368
|
+
// (footer is deferred to session.idle). The second message IS natural.
|
|
369
|
+
expect(isAssistantMessageNaturalCompletion({ message: firstAssistant })).toBe(false);
|
|
370
|
+
expect(isAssistantMessageNaturalCompletion({ message: secondAssistant })).toBe(true);
|
|
371
|
+
});
|
|
372
|
+
test('latest user turn keeps both assistant messages for the same user turn', () => {
|
|
373
|
+
const assistantIds = getAssistantMessageIdsForLatestUserTurn({ events, sessionId });
|
|
374
|
+
expect(assistantIds.has(firstAssistantId)).toBe(true);
|
|
375
|
+
expect(assistantIds.has(secondAssistantId)).toBe(true);
|
|
376
|
+
expect(getLatestAssistantMessageIdForLatestUserTurn({
|
|
377
|
+
events,
|
|
378
|
+
sessionId,
|
|
379
|
+
})).toBe(secondAssistantId);
|
|
380
|
+
});
|
|
381
|
+
test('getDerivedSubtaskIndex starts at 1 for first task of assistant message', () => {
|
|
382
|
+
expect(getDerivedSubtaskIndex({
|
|
383
|
+
events,
|
|
384
|
+
mainSessionId: sessionId,
|
|
385
|
+
candidateSessionId: childSessionId,
|
|
386
|
+
})).toBe(1);
|
|
387
|
+
});
|
|
388
|
+
test('getDerivedSubtaskIndex restarts at 1 for a newer assistant message', () => {
|
|
389
|
+
const firstTaskEvent = events.find((entry) => {
|
|
390
|
+
if (entry.event.type !== 'message.part.updated') {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
const part = entry.event.properties.part;
|
|
394
|
+
if (part.sessionID !== sessionId) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
if (part.type !== 'tool' || part.tool !== 'task') {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
if (part.state.status !== 'running' && part.state.status !== 'completed') {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
return part.state.metadata?.sessionId === childSessionId;
|
|
404
|
+
});
|
|
405
|
+
if (!firstTaskEvent) {
|
|
406
|
+
throw new Error('Expected to find task tool event in fixture');
|
|
407
|
+
}
|
|
408
|
+
const secondChildSessionId = 'ses_synthetic_child_2';
|
|
409
|
+
const thirdChildSessionId = 'ses_synthetic_child_3';
|
|
410
|
+
const syntheticAssistantMessageId = 'msg_synthetic_new_assistant';
|
|
411
|
+
const secondTaskEvent = structuredClone(firstTaskEvent);
|
|
412
|
+
if (secondTaskEvent.event.type !== 'message.part.updated') {
|
|
413
|
+
throw new Error('Expected message.part.updated event');
|
|
414
|
+
}
|
|
415
|
+
const secondTaskPart = secondTaskEvent.event.properties.part;
|
|
416
|
+
if (secondTaskPart.type !== 'tool' || secondTaskPart.tool !== 'task') {
|
|
417
|
+
throw new Error('Expected task tool part');
|
|
418
|
+
}
|
|
419
|
+
if (secondTaskPart.state.status !== 'completed') {
|
|
420
|
+
throw new Error('Expected completed task tool part');
|
|
421
|
+
}
|
|
422
|
+
secondTaskPart.id = `${secondTaskPart.id}-synthetic-2`;
|
|
423
|
+
secondTaskPart.messageID = syntheticAssistantMessageId;
|
|
424
|
+
secondTaskPart.state = {
|
|
425
|
+
...secondTaskPart.state,
|
|
426
|
+
metadata: {
|
|
427
|
+
...(secondTaskPart.state.metadata || {}),
|
|
428
|
+
sessionId: secondChildSessionId,
|
|
429
|
+
},
|
|
430
|
+
output: `task_id: ${secondChildSessionId}`,
|
|
431
|
+
};
|
|
432
|
+
const thirdTaskEvent = structuredClone(secondTaskEvent);
|
|
433
|
+
if (thirdTaskEvent.event.type !== 'message.part.updated') {
|
|
434
|
+
throw new Error('Expected message.part.updated event');
|
|
435
|
+
}
|
|
436
|
+
const thirdTaskPart = thirdTaskEvent.event.properties.part;
|
|
437
|
+
if (thirdTaskPart.type !== 'tool' || thirdTaskPart.tool !== 'task') {
|
|
438
|
+
throw new Error('Expected task tool part');
|
|
439
|
+
}
|
|
440
|
+
if (thirdTaskPart.state.status !== 'completed') {
|
|
441
|
+
throw new Error('Expected completed task tool part');
|
|
442
|
+
}
|
|
443
|
+
thirdTaskPart.id = `${thirdTaskPart.id}-synthetic-3`;
|
|
444
|
+
thirdTaskPart.messageID = syntheticAssistantMessageId;
|
|
445
|
+
thirdTaskPart.state = {
|
|
446
|
+
...thirdTaskPart.state,
|
|
447
|
+
metadata: {
|
|
448
|
+
...(thirdTaskPart.state.metadata || {}),
|
|
449
|
+
sessionId: thirdChildSessionId,
|
|
450
|
+
},
|
|
451
|
+
output: `task_id: ${thirdChildSessionId}`,
|
|
452
|
+
};
|
|
453
|
+
const lastTimestamp = events[events.length - 1]?.timestamp || 0;
|
|
454
|
+
const augmentedEvents = [
|
|
455
|
+
...events,
|
|
456
|
+
{
|
|
457
|
+
timestamp: lastTimestamp + 1,
|
|
458
|
+
event: secondTaskEvent.event,
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
timestamp: lastTimestamp + 2,
|
|
462
|
+
event: thirdTaskEvent.event,
|
|
463
|
+
},
|
|
464
|
+
];
|
|
465
|
+
expect(getDerivedSubtaskIndex({
|
|
466
|
+
events: augmentedEvents,
|
|
467
|
+
mainSessionId: sessionId,
|
|
468
|
+
candidateSessionId: childSessionId,
|
|
469
|
+
})).toBe(1);
|
|
470
|
+
expect(getDerivedSubtaskIndex({
|
|
471
|
+
events: augmentedEvents,
|
|
472
|
+
mainSessionId: sessionId,
|
|
473
|
+
candidateSessionId: secondChildSessionId,
|
|
474
|
+
})).toBe(1);
|
|
475
|
+
expect(getDerivedSubtaskIndex({
|
|
476
|
+
events: augmentedEvents,
|
|
477
|
+
mainSessionId: sessionId,
|
|
478
|
+
candidateSessionId: thirdChildSessionId,
|
|
479
|
+
})).toBe(2);
|
|
480
|
+
});
|
|
481
|
+
test('getDerivedSubtaskIndex returns undefined for unknown session', () => {
|
|
482
|
+
expect(getDerivedSubtaskIndex({
|
|
483
|
+
events,
|
|
484
|
+
mainSessionId: sessionId,
|
|
485
|
+
candidateSessionId: 'ses_nonexistent',
|
|
486
|
+
})).toBe(undefined);
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
describe('real-session-action-buttons', () => {
|
|
490
|
+
const events = loadFixture('real-session-action-buttons.jsonl');
|
|
491
|
+
const sessionId = getSessionId(events);
|
|
492
|
+
const toolCallAssistantId = 'msg_cb9b55c3b001hXC9qxjVxLMypM';
|
|
493
|
+
const finalAssistantId = 'msg_cb9b5ddd1001FALqKNM6xW98u6';
|
|
494
|
+
test('tool-call handoff assistant is not a natural completion but final reply is', () => {
|
|
495
|
+
const toolCallAssistant = getAssistantMessageById({
|
|
496
|
+
events,
|
|
497
|
+
sessionId,
|
|
498
|
+
messageId: toolCallAssistantId,
|
|
499
|
+
});
|
|
500
|
+
const finalAssistant = getAssistantMessageById({
|
|
501
|
+
events,
|
|
502
|
+
sessionId,
|
|
503
|
+
messageId: finalAssistantId,
|
|
504
|
+
});
|
|
505
|
+
// The tool-call message has finish="tool-calls" — not a natural completion
|
|
506
|
+
// (footer is deferred to session.idle). The final text message IS natural.
|
|
507
|
+
expect(isAssistantMessageNaturalCompletion({ message: toolCallAssistant })).toBe(false);
|
|
508
|
+
expect(isAssistantMessageNaturalCompletion({ message: finalAssistant })).toBe(true);
|
|
509
|
+
});
|
|
510
|
+
test('latest user turn keeps both assistant messages for the same user turn', () => {
|
|
511
|
+
const assistantIds = getAssistantMessageIdsForLatestUserTurn({ events, sessionId });
|
|
512
|
+
expect(assistantIds.has(toolCallAssistantId)).toBe(true);
|
|
513
|
+
expect(assistantIds.has(finalAssistantId)).toBe(true);
|
|
514
|
+
expect(getLatestAssistantMessageIdForLatestUserTurn({
|
|
515
|
+
events,
|
|
516
|
+
sessionId,
|
|
517
|
+
})).toBe(finalAssistantId);
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
describe('real-session-permission-external-file', () => {
|
|
521
|
+
const events = loadFixture('real-session-permission-external-file.jsonl');
|
|
522
|
+
const sessionId = getSessionId(events);
|
|
523
|
+
test('permission flow has no terminal assistant completion yet', () => {
|
|
524
|
+
const latestAssistantMessageId = getLatestAssistantMessageIdForLatestUserTurn({
|
|
525
|
+
events,
|
|
526
|
+
sessionId,
|
|
527
|
+
});
|
|
528
|
+
expect(latestAssistantMessageId).toBeDefined();
|
|
529
|
+
if (!latestAssistantMessageId) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const message = getAssistantMessageById({
|
|
533
|
+
events,
|
|
534
|
+
sessionId,
|
|
535
|
+
messageId: latestAssistantMessageId,
|
|
536
|
+
});
|
|
537
|
+
expect(isAssistantMessageNaturalCompletion({ message })).toBe(false);
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
describe('real-session-footer-suppressed-on-pre-idle-interrupt', () => {
|
|
541
|
+
const events = loadFixture('real-session-footer-suppressed-on-pre-idle-interrupt.jsonl');
|
|
542
|
+
const sessionId = getSessionId(events);
|
|
543
|
+
const oldAssistantId = 'msg_cbda8f408001VATHNUi9l05XqA';
|
|
544
|
+
const abortedAssistantId = 'msg_cbda90cef001GOQW8EQxkUz9b5';
|
|
545
|
+
const latestAssistantId = 'msg_cbda91463001DvEB6YMCXayZNj';
|
|
546
|
+
test('latest user turn ignores stale assistant messages from the interrupted turn', () => {
|
|
547
|
+
expect(isAssistantMessageInLatestUserTurn({
|
|
548
|
+
events,
|
|
549
|
+
sessionId,
|
|
550
|
+
messageId: oldAssistantId,
|
|
551
|
+
})).toBe(false);
|
|
552
|
+
expect(isAssistantMessageInLatestUserTurn({
|
|
553
|
+
events,
|
|
554
|
+
sessionId,
|
|
555
|
+
messageId: abortedAssistantId,
|
|
556
|
+
})).toBe(false);
|
|
557
|
+
expect(isAssistantMessageInLatestUserTurn({
|
|
558
|
+
events,
|
|
559
|
+
sessionId,
|
|
560
|
+
messageId: latestAssistantId,
|
|
561
|
+
})).toBe(true);
|
|
562
|
+
});
|
|
563
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Model resolution utilities.
|
|
2
|
+
// getDefaultModel resolves the default model from OpenCode when no user preference is set.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { xdgState } from 'xdg-basedir';
|
|
6
|
+
import * as errore from 'errore';
|
|
7
|
+
import {} from '../opencode.js';
|
|
8
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
9
|
+
const sessionLogger = createLogger(LogPrefix.SESSION);
|
|
10
|
+
/**
|
|
11
|
+
* Read user's recent models from OpenCode TUI's state file.
|
|
12
|
+
* Uses same path as OpenCode: path.join(xdgState, "opencode", "model.json")
|
|
13
|
+
* Returns all recent models so we can iterate until finding a valid one.
|
|
14
|
+
* See: opensrc/repos/github.com/sst/opencode/packages/opencode/src/global/index.ts
|
|
15
|
+
*/
|
|
16
|
+
function getRecentModelsFromTuiState() {
|
|
17
|
+
if (!xdgState) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
// Same path as OpenCode TUI: path.join(Global.Path.state, "model.json")
|
|
21
|
+
const modelJsonPath = path.join(xdgState, 'opencode', 'model.json');
|
|
22
|
+
const result = errore.tryFn(() => {
|
|
23
|
+
const content = fs.readFileSync(modelJsonPath, 'utf-8');
|
|
24
|
+
const data = JSON.parse(content);
|
|
25
|
+
return data.recent ?? [];
|
|
26
|
+
});
|
|
27
|
+
if (result instanceof Error) {
|
|
28
|
+
// File doesn't exist or is invalid - this is normal for fresh installs
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse a model string in format "provider/model" into providerID and modelID.
|
|
35
|
+
*/
|
|
36
|
+
function parseModelString(model) {
|
|
37
|
+
const [providerID, ...modelParts] = model.split('/');
|
|
38
|
+
const modelID = modelParts.join('/');
|
|
39
|
+
if (!providerID || !modelID) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return { providerID, modelID };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Validate that a model is available (provider connected + model exists).
|
|
46
|
+
*/
|
|
47
|
+
function isModelValid(model, connected, providers) {
|
|
48
|
+
const isConnected = connected.includes(model.providerID);
|
|
49
|
+
const provider = providers.find((p) => {
|
|
50
|
+
return p.id === model.providerID;
|
|
51
|
+
});
|
|
52
|
+
const modelExists = provider?.models && model.modelID in provider.models;
|
|
53
|
+
return isConnected && !!modelExists;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get the default model from OpenCode when no user preference is set.
|
|
57
|
+
* Priority (matches OpenCode TUI behavior):
|
|
58
|
+
* 1. OpenCode config.model setting
|
|
59
|
+
* 2. User's recent models from TUI state (~/.local/state/opencode/model.json)
|
|
60
|
+
* 3. First connected provider's default model from API
|
|
61
|
+
* Returns the model and its source.
|
|
62
|
+
*/
|
|
63
|
+
export async function getDefaultModel({ getClient, }) {
|
|
64
|
+
if (getClient instanceof Error) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
// Fetch connected providers to validate any model we return
|
|
68
|
+
const providersResponse = await errore.tryAsync(() => {
|
|
69
|
+
return getClient().provider.list({});
|
|
70
|
+
});
|
|
71
|
+
if (providersResponse instanceof Error) {
|
|
72
|
+
sessionLogger.log(`[MODEL] Failed to fetch providers for default model:`, providersResponse.message);
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
if (!providersResponse.data) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const { connected, default: defaults, all: providers, } = providersResponse.data;
|
|
79
|
+
if (connected.length === 0) {
|
|
80
|
+
sessionLogger.log(`[MODEL] No connected providers found`);
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
// 1. Check OpenCode config.model setting (highest priority after user preference)
|
|
84
|
+
const configResponse = await errore.tryAsync(() => {
|
|
85
|
+
return getClient().config.get({});
|
|
86
|
+
});
|
|
87
|
+
if (!(configResponse instanceof Error) && configResponse.data?.model) {
|
|
88
|
+
const configModel = parseModelString(configResponse.data.model);
|
|
89
|
+
if (configModel && isModelValid(configModel, connected, providers)) {
|
|
90
|
+
sessionLogger.log(`[MODEL] Using config model: ${configModel.providerID}/${configModel.modelID}`);
|
|
91
|
+
return { ...configModel, source: 'opencode-config' };
|
|
92
|
+
}
|
|
93
|
+
if (configModel) {
|
|
94
|
+
sessionLogger.log(`[MODEL] Config model ${configResponse.data.model} not available, checking recent`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// 2. Try to use user's recent models from TUI state (iterate until finding valid one)
|
|
98
|
+
const recentModels = getRecentModelsFromTuiState();
|
|
99
|
+
for (const recentModel of recentModels) {
|
|
100
|
+
if (isModelValid(recentModel, connected, providers)) {
|
|
101
|
+
sessionLogger.log(`[MODEL] Using recent TUI model: ${recentModel.providerID}/${recentModel.modelID}`);
|
|
102
|
+
return { ...recentModel, source: 'opencode-recent' };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (recentModels.length > 0) {
|
|
106
|
+
sessionLogger.log(`[MODEL] No valid recent TUI models found`);
|
|
107
|
+
}
|
|
108
|
+
// 3. Fall back to first connected provider's default model
|
|
109
|
+
const firstConnected = connected[0];
|
|
110
|
+
if (!firstConnected) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
const defaultModelId = defaults[firstConnected];
|
|
114
|
+
if (!defaultModelId) {
|
|
115
|
+
sessionLogger.log(`[MODEL] No default model for provider ${firstConnected}`);
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
sessionLogger.log(`[MODEL] Using provider default: ${firstConnected}/${defaultModelId}`);
|
|
119
|
+
return {
|
|
120
|
+
providerID: firstConnected,
|
|
121
|
+
modelID: defaultModelId,
|
|
122
|
+
source: 'opencode-provider-default',
|
|
123
|
+
};
|
|
124
|
+
}
|