@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,776 @@
|
|
|
1
|
+
// /model command - Set the preferred model for this channel or session.
|
|
2
|
+
import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, ChannelType, MessageFlags, } from 'discord.js';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { setChannelModel, setSessionModel, setSessionAgent, getChannelModel, getSessionModel, getSessionAgent, getChannelAgent, getThreadSession, getGlobalModel, setGlobalModel, getVariantCascade, } from '../database.js';
|
|
5
|
+
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
6
|
+
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
7
|
+
import { getDefaultModel } from '../session-handler/model-utils.js';
|
|
8
|
+
import { getRuntime } from '../session-handler/thread-session-runtime.js';
|
|
9
|
+
import { getThinkingValuesForModel } from '../thinking-utils.js';
|
|
10
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
11
|
+
import * as errore from 'errore';
|
|
12
|
+
import { buildPaginatedOptions, parsePaginationValue } from './paginated-select.js';
|
|
13
|
+
const modelLogger = createLogger(LogPrefix.MODEL);
|
|
14
|
+
// Store context by hash to avoid customId length limits (Discord max: 100 chars).
|
|
15
|
+
// Entries are TTL'd to prevent unbounded growth when users open /model and never
|
|
16
|
+
// interact with the select menu.
|
|
17
|
+
const MODEL_CONTEXT_TTL_MS = 10 * 60 * 1000;
|
|
18
|
+
const pendingModelContexts = new Map();
|
|
19
|
+
function setModelContext(contextHash, context) {
|
|
20
|
+
pendingModelContexts.set(contextHash, context);
|
|
21
|
+
setTimeout(() => {
|
|
22
|
+
pendingModelContexts.delete(contextHash);
|
|
23
|
+
}, MODEL_CONTEXT_TTL_MS).unref();
|
|
24
|
+
}
|
|
25
|
+
function parseModelId(modelString) {
|
|
26
|
+
const [providerID, ...modelParts] = modelString.split('/');
|
|
27
|
+
const modelID = modelParts.join('/');
|
|
28
|
+
if (providerID && modelID) {
|
|
29
|
+
return { providerID, modelID };
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
export async function ensureSessionPreferencesSnapshot({ sessionId, channelId, appId, getClient, agentOverride, modelOverride, force, }) {
|
|
34
|
+
const [sessionAgentPreference, sessionModelPreference] = await Promise.all([
|
|
35
|
+
getSessionAgent(sessionId),
|
|
36
|
+
getSessionModel(sessionId),
|
|
37
|
+
]);
|
|
38
|
+
const shouldBootstrapSessionPreferences = force || (!sessionAgentPreference && !sessionModelPreference);
|
|
39
|
+
if (!shouldBootstrapSessionPreferences) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const bootstrappedAgent = agentOverride ||
|
|
43
|
+
sessionAgentPreference ||
|
|
44
|
+
(channelId ? await getChannelAgent(channelId) : undefined);
|
|
45
|
+
if (!sessionAgentPreference && bootstrappedAgent) {
|
|
46
|
+
await setSessionAgent(sessionId, bootstrappedAgent);
|
|
47
|
+
modelLogger.log(`[MODEL] Snapshotted session agent ${bootstrappedAgent} for session ${sessionId}`);
|
|
48
|
+
}
|
|
49
|
+
if (sessionModelPreference) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (modelOverride) {
|
|
53
|
+
const parsedModelOverride = parseModelId(modelOverride);
|
|
54
|
+
if (parsedModelOverride) {
|
|
55
|
+
const bootstrappedVariant = await getVariantCascade({
|
|
56
|
+
sessionId,
|
|
57
|
+
channelId,
|
|
58
|
+
appId,
|
|
59
|
+
});
|
|
60
|
+
await setSessionModel({
|
|
61
|
+
sessionId,
|
|
62
|
+
modelId: modelOverride,
|
|
63
|
+
variant: bootstrappedVariant ?? null,
|
|
64
|
+
});
|
|
65
|
+
modelLogger.log(`[MODEL] Snapshotted explicit session model ${modelOverride} for session ${sessionId}`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
modelLogger.warn(`[MODEL] Ignoring invalid explicit model override "${modelOverride}" for session ${sessionId}`);
|
|
69
|
+
}
|
|
70
|
+
const bootstrappedModel = await getCurrentModelInfo({
|
|
71
|
+
sessionId,
|
|
72
|
+
channelId,
|
|
73
|
+
appId,
|
|
74
|
+
agentPreference: bootstrappedAgent,
|
|
75
|
+
getClient,
|
|
76
|
+
});
|
|
77
|
+
if (bootstrappedModel.type === 'none') {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const bootstrappedVariant = await getVariantCascade({
|
|
81
|
+
sessionId,
|
|
82
|
+
channelId,
|
|
83
|
+
appId,
|
|
84
|
+
});
|
|
85
|
+
await setSessionModel({
|
|
86
|
+
sessionId,
|
|
87
|
+
modelId: bootstrappedModel.model,
|
|
88
|
+
variant: bootstrappedVariant ?? null,
|
|
89
|
+
});
|
|
90
|
+
modelLogger.log(`[MODEL] Snapshotted session model ${bootstrappedModel.model} for session ${sessionId}`);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get the current model info for a channel/session, including where it comes from.
|
|
94
|
+
* Priority: session > agent > channel > global > opencode default
|
|
95
|
+
*/
|
|
96
|
+
export async function getCurrentModelInfo({ sessionId, channelId, appId, agentPreference, getClient, }) {
|
|
97
|
+
if (getClient instanceof Error) {
|
|
98
|
+
return { type: 'none' };
|
|
99
|
+
}
|
|
100
|
+
// 1. Check session model preference
|
|
101
|
+
if (sessionId) {
|
|
102
|
+
const sessionPref = await getSessionModel(sessionId);
|
|
103
|
+
if (sessionPref) {
|
|
104
|
+
const parsed = parseModelId(sessionPref.modelId);
|
|
105
|
+
if (parsed) {
|
|
106
|
+
return { type: 'session', model: sessionPref.modelId, ...parsed };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// 2. Check agent's configured model
|
|
111
|
+
const effectiveAgent = agentPreference ??
|
|
112
|
+
(sessionId
|
|
113
|
+
? (await getSessionAgent(sessionId)) ||
|
|
114
|
+
(channelId ? await getChannelAgent(channelId) : undefined)
|
|
115
|
+
: channelId
|
|
116
|
+
? await getChannelAgent(channelId)
|
|
117
|
+
: undefined);
|
|
118
|
+
if (effectiveAgent) {
|
|
119
|
+
const agentsResponse = await getClient().app.agents({});
|
|
120
|
+
if (agentsResponse.data) {
|
|
121
|
+
const agent = agentsResponse.data.find((a) => a.name === effectiveAgent);
|
|
122
|
+
if (agent?.model) {
|
|
123
|
+
const model = `${agent.model.providerID}/${agent.model.modelID}`;
|
|
124
|
+
return {
|
|
125
|
+
type: 'agent',
|
|
126
|
+
model,
|
|
127
|
+
providerID: agent.model.providerID,
|
|
128
|
+
modelID: agent.model.modelID,
|
|
129
|
+
agentName: effectiveAgent,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// 3. Check channel model preference
|
|
135
|
+
if (channelId) {
|
|
136
|
+
const channelPref = await getChannelModel(channelId);
|
|
137
|
+
if (channelPref) {
|
|
138
|
+
const parsed = parseModelId(channelPref.modelId);
|
|
139
|
+
if (parsed) {
|
|
140
|
+
return { type: 'channel', model: channelPref.modelId, ...parsed };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// 4. Check global model preference
|
|
145
|
+
if (appId) {
|
|
146
|
+
const globalPref = await getGlobalModel(appId);
|
|
147
|
+
if (globalPref) {
|
|
148
|
+
const parsed = parseModelId(globalPref.modelId);
|
|
149
|
+
if (parsed) {
|
|
150
|
+
return { type: 'global', model: globalPref.modelId, ...parsed };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// 5. Get opencode default (config > recent > provider default)
|
|
155
|
+
const defaultModel = await getDefaultModel({ getClient });
|
|
156
|
+
if (defaultModel) {
|
|
157
|
+
const model = `${defaultModel.providerID}/${defaultModel.modelID}`;
|
|
158
|
+
return {
|
|
159
|
+
type: defaultModel.source,
|
|
160
|
+
model,
|
|
161
|
+
providerID: defaultModel.providerID,
|
|
162
|
+
modelID: defaultModel.modelID,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return { type: 'none' };
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Handle the /model slash command.
|
|
169
|
+
* Shows a select menu with available providers.
|
|
170
|
+
*/
|
|
171
|
+
export async function handleModelCommand({ interaction, appId, }) {
|
|
172
|
+
modelLogger.log('[MODEL] handleModelCommand called');
|
|
173
|
+
// Defer reply immediately to avoid 3-second timeout
|
|
174
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
175
|
+
modelLogger.log('[MODEL] Deferred reply');
|
|
176
|
+
const channel = interaction.channel;
|
|
177
|
+
if (!channel) {
|
|
178
|
+
await interaction.editReply({
|
|
179
|
+
content: 'This command can only be used in a channel',
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Determine if we're in a thread or text channel
|
|
184
|
+
const isThread = [
|
|
185
|
+
ChannelType.PublicThread,
|
|
186
|
+
ChannelType.PrivateThread,
|
|
187
|
+
ChannelType.AnnouncementThread,
|
|
188
|
+
].includes(channel.type);
|
|
189
|
+
let projectDirectory;
|
|
190
|
+
let targetChannelId;
|
|
191
|
+
let sessionId;
|
|
192
|
+
if (isThread) {
|
|
193
|
+
const thread = channel;
|
|
194
|
+
// Parallelize: resolve metadata and session ID at the same time
|
|
195
|
+
const [textChannel, threadSessionId] = await Promise.all([
|
|
196
|
+
resolveTextChannel(thread),
|
|
197
|
+
getThreadSession(thread.id),
|
|
198
|
+
]);
|
|
199
|
+
const metadata = await getKimakiMetadata(textChannel);
|
|
200
|
+
projectDirectory = metadata.projectDirectory;
|
|
201
|
+
targetChannelId = textChannel?.id || channel.id;
|
|
202
|
+
sessionId = threadSessionId;
|
|
203
|
+
}
|
|
204
|
+
else if (channel.type === ChannelType.GuildText) {
|
|
205
|
+
const textChannel = channel;
|
|
206
|
+
const metadata = await getKimakiMetadata(textChannel);
|
|
207
|
+
projectDirectory = metadata.projectDirectory;
|
|
208
|
+
targetChannelId = channel.id;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
await interaction.editReply({
|
|
212
|
+
content: 'This command can only be used in text channels or threads',
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (!projectDirectory) {
|
|
217
|
+
await interaction.editReply({
|
|
218
|
+
content: 'This channel is not configured with a project directory',
|
|
219
|
+
});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
224
|
+
if (getClient instanceof Error) {
|
|
225
|
+
await interaction.editReply({ content: getClient.message });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const effectiveAppId = appId;
|
|
229
|
+
if (isThread && sessionId) {
|
|
230
|
+
await ensureSessionPreferencesSnapshot({
|
|
231
|
+
sessionId,
|
|
232
|
+
channelId: targetChannelId,
|
|
233
|
+
appId: effectiveAppId,
|
|
234
|
+
getClient,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// Parallelize: fetch providers, current model info, and variant cascade at the same time.
|
|
238
|
+
// getCurrentModelInfo does DB lookups first (fast) and only hits provider.list as fallback.
|
|
239
|
+
const [providersResponse, currentModelInfo, cascadeVariant] = await Promise.all([
|
|
240
|
+
getClient().provider.list({ directory: projectDirectory }),
|
|
241
|
+
getCurrentModelInfo({
|
|
242
|
+
sessionId,
|
|
243
|
+
channelId: targetChannelId,
|
|
244
|
+
appId: effectiveAppId,
|
|
245
|
+
getClient,
|
|
246
|
+
}),
|
|
247
|
+
getVariantCascade({
|
|
248
|
+
sessionId,
|
|
249
|
+
channelId: targetChannelId,
|
|
250
|
+
appId: effectiveAppId,
|
|
251
|
+
}),
|
|
252
|
+
]);
|
|
253
|
+
if (!providersResponse.data) {
|
|
254
|
+
await interaction.editReply({
|
|
255
|
+
content: 'Failed to fetch providers',
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const { all: allProviders, connected } = providersResponse.data;
|
|
260
|
+
// Filter to only connected providers (have credentials)
|
|
261
|
+
const availableProviders = allProviders.filter((p) => {
|
|
262
|
+
return connected.includes(p.id);
|
|
263
|
+
});
|
|
264
|
+
if (availableProviders.length === 0) {
|
|
265
|
+
await interaction.editReply({
|
|
266
|
+
content: 'No providers with credentials found. Use `/login` to connect a provider and add credentials.',
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const currentModelText = (() => {
|
|
271
|
+
switch (currentModelInfo.type) {
|
|
272
|
+
case 'session':
|
|
273
|
+
return `**Current (this thread):** \`${currentModelInfo.model}\``;
|
|
274
|
+
case 'agent':
|
|
275
|
+
return `**Current (agent "${currentModelInfo.agentName}"):** \`${currentModelInfo.model}\``;
|
|
276
|
+
case 'channel':
|
|
277
|
+
return `**Current (channel override):** \`${currentModelInfo.model}\``;
|
|
278
|
+
case 'global':
|
|
279
|
+
return `**Current (global default):** \`${currentModelInfo.model}\``;
|
|
280
|
+
case 'opencode-config':
|
|
281
|
+
case 'opencode-recent':
|
|
282
|
+
case 'opencode-provider-default':
|
|
283
|
+
return `**Current (opencode default):** \`${currentModelInfo.model}\``;
|
|
284
|
+
case 'none':
|
|
285
|
+
return '**Current:** none';
|
|
286
|
+
}
|
|
287
|
+
})();
|
|
288
|
+
const variantText = (() => {
|
|
289
|
+
if (currentModelInfo.type === 'none' || !cascadeVariant) {
|
|
290
|
+
return '';
|
|
291
|
+
}
|
|
292
|
+
return `\n**Variant:** \`${cascadeVariant}\``;
|
|
293
|
+
})();
|
|
294
|
+
// Store context with a short hash key to avoid customId length limits.
|
|
295
|
+
const providerSelectHeader = `**Set Model Preference**\n${currentModelText}${variantText}\nSelect a provider:`;
|
|
296
|
+
const context = {
|
|
297
|
+
dir: projectDirectory,
|
|
298
|
+
channelId: targetChannelId,
|
|
299
|
+
sessionId: sessionId,
|
|
300
|
+
isThread: isThread,
|
|
301
|
+
thread: isThread ? channel : undefined,
|
|
302
|
+
appId,
|
|
303
|
+
providerSelectHeader,
|
|
304
|
+
};
|
|
305
|
+
const contextHash = crypto.randomBytes(8).toString('hex');
|
|
306
|
+
setModelContext(contextHash, context);
|
|
307
|
+
const allProviderOptions = [...availableProviders]
|
|
308
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
309
|
+
.map((provider) => {
|
|
310
|
+
const modelCount = Object.keys(provider.models || {}).length;
|
|
311
|
+
return {
|
|
312
|
+
label: provider.name.slice(0, 100),
|
|
313
|
+
value: provider.id,
|
|
314
|
+
description: `${modelCount} model${modelCount !== 1 ? 's' : ''} available`.slice(0, 100),
|
|
315
|
+
};
|
|
316
|
+
});
|
|
317
|
+
const { options } = buildPaginatedOptions({
|
|
318
|
+
allOptions: allProviderOptions,
|
|
319
|
+
page: 0,
|
|
320
|
+
});
|
|
321
|
+
const selectMenu = new StringSelectMenuBuilder()
|
|
322
|
+
.setCustomId(`model_provider:${contextHash}`)
|
|
323
|
+
.setPlaceholder('Select a provider')
|
|
324
|
+
.addOptions(options);
|
|
325
|
+
const actionRow = new ActionRowBuilder().addComponents(selectMenu);
|
|
326
|
+
await interaction.editReply({
|
|
327
|
+
content: providerSelectHeader,
|
|
328
|
+
components: [actionRow],
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
modelLogger.error('Error loading providers:', error);
|
|
333
|
+
await interaction.editReply({
|
|
334
|
+
content: `Failed to load providers: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Handle the provider select menu interaction.
|
|
340
|
+
* Shows a second select menu with models for the chosen provider.
|
|
341
|
+
*/
|
|
342
|
+
export async function handleProviderSelectMenu(interaction) {
|
|
343
|
+
const customId = interaction.customId;
|
|
344
|
+
if (!customId.startsWith('model_provider:')) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
// Defer update immediately to avoid timeout
|
|
348
|
+
await interaction.deferUpdate();
|
|
349
|
+
const contextHash = customId.replace('model_provider:', '');
|
|
350
|
+
const context = pendingModelContexts.get(contextHash);
|
|
351
|
+
if (!context) {
|
|
352
|
+
await interaction.editReply({
|
|
353
|
+
content: 'Selection expired. Please run /model again.',
|
|
354
|
+
components: [],
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const selectedProviderId = interaction.values[0];
|
|
359
|
+
if (!selectedProviderId) {
|
|
360
|
+
await interaction.editReply({
|
|
361
|
+
content: 'No provider selected',
|
|
362
|
+
components: [],
|
|
363
|
+
});
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
// Handle pagination nav — re-render the same provider select with new page
|
|
367
|
+
const providerNavPage = parsePaginationValue(selectedProviderId);
|
|
368
|
+
if (providerNavPage !== undefined) {
|
|
369
|
+
context.providerPage = providerNavPage;
|
|
370
|
+
setModelContext(contextHash, context);
|
|
371
|
+
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
372
|
+
if (getClient instanceof Error) {
|
|
373
|
+
await interaction.editReply({ content: getClient.message, components: [] });
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const providersResponse = await getClient().provider.list({ directory: context.dir });
|
|
377
|
+
if (!providersResponse.data) {
|
|
378
|
+
await interaction.editReply({ content: 'Failed to fetch providers', components: [] });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const { all: allProviders, connected } = providersResponse.data;
|
|
382
|
+
const availableProviders = allProviders.filter((p) => connected.includes(p.id));
|
|
383
|
+
const allProviderOptions = [...availableProviders]
|
|
384
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
385
|
+
.map((p) => {
|
|
386
|
+
const modelCount = Object.keys(p.models || {}).length;
|
|
387
|
+
return {
|
|
388
|
+
label: p.name.slice(0, 100),
|
|
389
|
+
value: p.id,
|
|
390
|
+
description: `${modelCount} model${modelCount !== 1 ? 's' : ''} available`.slice(0, 100),
|
|
391
|
+
};
|
|
392
|
+
});
|
|
393
|
+
const { options } = buildPaginatedOptions({ allOptions: allProviderOptions, page: providerNavPage });
|
|
394
|
+
const selectMenu = new StringSelectMenuBuilder()
|
|
395
|
+
.setCustomId(`model_provider:${contextHash}`)
|
|
396
|
+
.setPlaceholder('Select a provider')
|
|
397
|
+
.addOptions(options);
|
|
398
|
+
const actionRow = new ActionRowBuilder().addComponents(selectMenu);
|
|
399
|
+
await interaction.editReply({
|
|
400
|
+
content: context.providerSelectHeader || `**Set Model Preference**\nSelect a provider:`,
|
|
401
|
+
components: [actionRow],
|
|
402
|
+
});
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
407
|
+
if (getClient instanceof Error) {
|
|
408
|
+
await interaction.editReply({
|
|
409
|
+
content: getClient.message,
|
|
410
|
+
components: [],
|
|
411
|
+
});
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const providersResponse = await getClient().provider.list({
|
|
415
|
+
directory: context.dir,
|
|
416
|
+
});
|
|
417
|
+
if (!providersResponse.data) {
|
|
418
|
+
await interaction.editReply({
|
|
419
|
+
content: 'Failed to fetch providers',
|
|
420
|
+
components: [],
|
|
421
|
+
});
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const provider = providersResponse.data.all.find((p) => p.id === selectedProviderId);
|
|
425
|
+
if (!provider) {
|
|
426
|
+
await interaction.editReply({
|
|
427
|
+
content: 'Provider not found',
|
|
428
|
+
components: [],
|
|
429
|
+
});
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const models = Object.entries(provider.models || {})
|
|
433
|
+
.map(([modelId, model]) => ({
|
|
434
|
+
id: modelId,
|
|
435
|
+
name: model.name,
|
|
436
|
+
releaseDate: model.release_date,
|
|
437
|
+
}))
|
|
438
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
439
|
+
if (models.length === 0) {
|
|
440
|
+
await interaction.editReply({
|
|
441
|
+
content: `No models available for ${provider.name}`,
|
|
442
|
+
components: [],
|
|
443
|
+
});
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
// Update context with provider info and reuse the same hash
|
|
447
|
+
context.providerId = selectedProviderId;
|
|
448
|
+
context.providerName = provider.name;
|
|
449
|
+
context.modelPage = 0;
|
|
450
|
+
setModelContext(contextHash, context);
|
|
451
|
+
const allModelOptions = models.map((model) => {
|
|
452
|
+
const dateStr = model.releaseDate
|
|
453
|
+
? new Date(model.releaseDate).toLocaleDateString()
|
|
454
|
+
: 'Unknown date';
|
|
455
|
+
return {
|
|
456
|
+
label: model.name.slice(0, 100),
|
|
457
|
+
value: model.id,
|
|
458
|
+
description: dateStr.slice(0, 100),
|
|
459
|
+
};
|
|
460
|
+
});
|
|
461
|
+
const { options } = buildPaginatedOptions({
|
|
462
|
+
allOptions: allModelOptions,
|
|
463
|
+
page: 0,
|
|
464
|
+
});
|
|
465
|
+
const selectMenu = new StringSelectMenuBuilder()
|
|
466
|
+
.setCustomId(`model_select:${contextHash}`)
|
|
467
|
+
.setPlaceholder('Select a model')
|
|
468
|
+
.addOptions(options);
|
|
469
|
+
const actionRow = new ActionRowBuilder().addComponents(selectMenu);
|
|
470
|
+
await interaction.editReply({
|
|
471
|
+
content: `**Set Model Preference**\nProvider: **${provider.name}**\nSelect a model:`,
|
|
472
|
+
components: [actionRow],
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
modelLogger.error('Error loading models:', error);
|
|
477
|
+
await interaction.editReply({
|
|
478
|
+
content: `Failed to load models: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
479
|
+
components: [],
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Handle the model select menu interaction.
|
|
485
|
+
* Stores the model preference in the database.
|
|
486
|
+
*/
|
|
487
|
+
export async function handleModelSelectMenu(interaction) {
|
|
488
|
+
const customId = interaction.customId;
|
|
489
|
+
if (!customId.startsWith('model_select:')) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
// Defer update immediately
|
|
493
|
+
await interaction.deferUpdate();
|
|
494
|
+
const contextHash = customId.replace('model_select:', '');
|
|
495
|
+
const context = pendingModelContexts.get(contextHash);
|
|
496
|
+
if (!context || !context.providerId || !context.providerName) {
|
|
497
|
+
await interaction.editReply({
|
|
498
|
+
content: 'Selection expired. Please run /model again.',
|
|
499
|
+
components: [],
|
|
500
|
+
});
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const selectedModelId = interaction.values[0];
|
|
504
|
+
if (!selectedModelId) {
|
|
505
|
+
await interaction.editReply({
|
|
506
|
+
content: 'No model selected',
|
|
507
|
+
components: [],
|
|
508
|
+
});
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
// Handle pagination nav — re-render the same model select with new page
|
|
512
|
+
const modelNavPage = parsePaginationValue(selectedModelId);
|
|
513
|
+
if (modelNavPage !== undefined) {
|
|
514
|
+
context.modelPage = modelNavPage;
|
|
515
|
+
setModelContext(contextHash, context);
|
|
516
|
+
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
517
|
+
if (getClient instanceof Error) {
|
|
518
|
+
await interaction.editReply({ content: getClient.message, components: [] });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const providersResponse = await getClient().provider.list({ directory: context.dir });
|
|
522
|
+
const provider = providersResponse.data?.all.find((p) => p.id === context.providerId);
|
|
523
|
+
if (!provider) {
|
|
524
|
+
await interaction.editReply({ content: 'Provider not found', components: [] });
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const allModelOptions = Object.entries(provider.models || {})
|
|
528
|
+
.map(([modelId, model]) => ({
|
|
529
|
+
label: model.name.slice(0, 100),
|
|
530
|
+
value: modelId,
|
|
531
|
+
description: (model.release_date
|
|
532
|
+
? new Date(model.release_date).toLocaleDateString()
|
|
533
|
+
: 'Unknown date').slice(0, 100),
|
|
534
|
+
}))
|
|
535
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
536
|
+
const { options } = buildPaginatedOptions({ allOptions: allModelOptions, page: modelNavPage });
|
|
537
|
+
const selectMenu = new StringSelectMenuBuilder()
|
|
538
|
+
.setCustomId(`model_select:${contextHash}`)
|
|
539
|
+
.setPlaceholder('Select a model')
|
|
540
|
+
.addOptions(options);
|
|
541
|
+
const actionRow = new ActionRowBuilder().addComponents(selectMenu);
|
|
542
|
+
await interaction.editReply({
|
|
543
|
+
content: `**Set Model Preference**\nProvider: **${context.providerName}**\nSelect a model:`,
|
|
544
|
+
components: [actionRow],
|
|
545
|
+
});
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
// Build full model ID: provider_id/model_id
|
|
549
|
+
const fullModelId = `${context.providerId}/${selectedModelId}`;
|
|
550
|
+
try {
|
|
551
|
+
context.selectedModelId = fullModelId;
|
|
552
|
+
setModelContext(contextHash, context);
|
|
553
|
+
// Check if model has variants (thinking levels) - if so, show variant picker first
|
|
554
|
+
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
555
|
+
if (!(getClient instanceof Error)) {
|
|
556
|
+
const providersResponse = await getClient().provider.list({
|
|
557
|
+
directory: context.dir,
|
|
558
|
+
});
|
|
559
|
+
if (providersResponse.data) {
|
|
560
|
+
const variants = getThinkingValuesForModel({
|
|
561
|
+
providers: providersResponse.data.all,
|
|
562
|
+
providerId: context.providerId,
|
|
563
|
+
modelId: selectedModelId,
|
|
564
|
+
});
|
|
565
|
+
if (variants.length > 0) {
|
|
566
|
+
context.availableVariants = variants;
|
|
567
|
+
setModelContext(contextHash, context);
|
|
568
|
+
const variantOptions = [
|
|
569
|
+
{
|
|
570
|
+
label: 'None (default)',
|
|
571
|
+
value: '__none__',
|
|
572
|
+
description: 'Use the model without a specific thinking level',
|
|
573
|
+
},
|
|
574
|
+
...variants.slice(0, 24).map((v) => ({
|
|
575
|
+
label: v.slice(0, 100),
|
|
576
|
+
value: v,
|
|
577
|
+
description: `Use ${v} thinking`.slice(0, 100),
|
|
578
|
+
})),
|
|
579
|
+
];
|
|
580
|
+
const selectMenu = new StringSelectMenuBuilder()
|
|
581
|
+
.setCustomId(`model_variant:${contextHash}`)
|
|
582
|
+
.setPlaceholder('Select a thinking level')
|
|
583
|
+
.addOptions(variantOptions);
|
|
584
|
+
const actionRow = new ActionRowBuilder().addComponents(selectMenu);
|
|
585
|
+
await interaction.editReply({
|
|
586
|
+
content: `**Set Model Preference**\nModel: **${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\`\nSelect a thinking level:`,
|
|
587
|
+
components: [actionRow],
|
|
588
|
+
});
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// No variants available - skip to scope
|
|
594
|
+
context.selectedVariant = null;
|
|
595
|
+
setModelContext(contextHash, context);
|
|
596
|
+
await showScopeMenu({ interaction, contextHash, context });
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
modelLogger.error('Error saving model preference:', error);
|
|
600
|
+
await interaction.editReply({
|
|
601
|
+
content: `Failed to save model preference: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
602
|
+
components: [],
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Handle the variant select menu interaction.
|
|
608
|
+
* Stores the selected variant and shows the scope menu.
|
|
609
|
+
*/
|
|
610
|
+
export async function handleModelVariantSelectMenu(interaction) {
|
|
611
|
+
const customId = interaction.customId;
|
|
612
|
+
if (!customId.startsWith('model_variant:')) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
await interaction.deferUpdate();
|
|
616
|
+
const contextHash = customId.replace('model_variant:', '');
|
|
617
|
+
const context = pendingModelContexts.get(contextHash);
|
|
618
|
+
if (!context || !context.selectedModelId) {
|
|
619
|
+
await interaction.editReply({
|
|
620
|
+
content: 'Selection expired. Please run /model again.',
|
|
621
|
+
components: [],
|
|
622
|
+
});
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const selectedValue = interaction.values[0];
|
|
626
|
+
if (!selectedValue) {
|
|
627
|
+
await interaction.editReply({
|
|
628
|
+
content: 'No variant selected',
|
|
629
|
+
components: [],
|
|
630
|
+
});
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
context.selectedVariant = selectedValue === '__none__' ? null : selectedValue;
|
|
634
|
+
setModelContext(contextHash, context);
|
|
635
|
+
await showScopeMenu({ interaction, contextHash, context });
|
|
636
|
+
}
|
|
637
|
+
async function showScopeMenu({ interaction, contextHash, context, }) {
|
|
638
|
+
const modelId = context.selectedModelId;
|
|
639
|
+
const modelDisplay = modelId.split('/')[1] || modelId;
|
|
640
|
+
const variantSuffix = context.selectedVariant
|
|
641
|
+
? ` (${context.selectedVariant})`
|
|
642
|
+
: '';
|
|
643
|
+
const scopeOptions = [
|
|
644
|
+
...(context.isThread && context.sessionId
|
|
645
|
+
? [
|
|
646
|
+
{
|
|
647
|
+
label: 'This session only',
|
|
648
|
+
value: 'session',
|
|
649
|
+
description: 'Override for this session only',
|
|
650
|
+
},
|
|
651
|
+
]
|
|
652
|
+
: []),
|
|
653
|
+
{
|
|
654
|
+
label: 'This channel only',
|
|
655
|
+
value: 'channel',
|
|
656
|
+
description: 'Override for this channel only',
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
label: 'Global default',
|
|
660
|
+
value: 'global',
|
|
661
|
+
description: 'Set for this channel and as default for all others',
|
|
662
|
+
},
|
|
663
|
+
];
|
|
664
|
+
const selectMenu = new StringSelectMenuBuilder()
|
|
665
|
+
.setCustomId(`model_scope:${contextHash}`)
|
|
666
|
+
.setPlaceholder('Apply to...')
|
|
667
|
+
.addOptions(scopeOptions);
|
|
668
|
+
const actionRow = new ActionRowBuilder().addComponents(selectMenu);
|
|
669
|
+
await interaction.editReply({
|
|
670
|
+
content: `**Set Model Preference**\nModel: **${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`\nApply to:`,
|
|
671
|
+
components: [actionRow],
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Handle the scope select menu interaction.
|
|
676
|
+
* Applies the model to either the channel or globally.
|
|
677
|
+
*/
|
|
678
|
+
export async function handleModelScopeSelectMenu(interaction) {
|
|
679
|
+
const customId = interaction.customId;
|
|
680
|
+
if (!customId.startsWith('model_scope:')) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
// Defer update immediately
|
|
684
|
+
await interaction.deferUpdate();
|
|
685
|
+
const contextHash = customId.replace('model_scope:', '');
|
|
686
|
+
const context = pendingModelContexts.get(contextHash);
|
|
687
|
+
if (!context ||
|
|
688
|
+
!context.providerId ||
|
|
689
|
+
!context.providerName ||
|
|
690
|
+
!context.selectedModelId) {
|
|
691
|
+
await interaction.editReply({
|
|
692
|
+
content: 'Selection expired. Please run /model again.',
|
|
693
|
+
components: [],
|
|
694
|
+
});
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const selectedScope = interaction.values[0];
|
|
698
|
+
if (!selectedScope) {
|
|
699
|
+
await interaction.editReply({
|
|
700
|
+
content: 'No scope selected',
|
|
701
|
+
components: [],
|
|
702
|
+
});
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
const modelId = context.selectedModelId;
|
|
706
|
+
const modelDisplay = modelId.split('/')[1] || modelId;
|
|
707
|
+
const variant = context.selectedVariant ?? null;
|
|
708
|
+
const variantSuffix = variant ? ` (${variant})` : '';
|
|
709
|
+
const agentTip = '\n_Tip: create [agent .md files](https://github.com/remorses/kimaki/blob/main/docs/model-switching.md) in .opencode/agent/ for one-command model switching_';
|
|
710
|
+
try {
|
|
711
|
+
if (selectedScope === 'session') {
|
|
712
|
+
if (!context.sessionId) {
|
|
713
|
+
pendingModelContexts.delete(contextHash);
|
|
714
|
+
await interaction.editReply({
|
|
715
|
+
content: 'No active session in this thread. Please run /model in a thread with a session.',
|
|
716
|
+
components: [],
|
|
717
|
+
});
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
await setSessionModel({ sessionId: context.sessionId, modelId, variant });
|
|
721
|
+
modelLogger.log(`Set model ${modelId}${variantSuffix} for session ${context.sessionId}`);
|
|
722
|
+
let retried = false;
|
|
723
|
+
if (context.thread) {
|
|
724
|
+
const runtime = getRuntime(context.thread.id);
|
|
725
|
+
if (runtime) {
|
|
726
|
+
retried = await runtime.retryLastUserPrompt();
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
const retryNote = retried
|
|
730
|
+
? '\n_Restarting current request with new model..._'
|
|
731
|
+
: '';
|
|
732
|
+
await interaction.editReply({
|
|
733
|
+
content: `Model set for this session:\n**${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`${retryNote}${agentTip}`,
|
|
734
|
+
flags: MessageFlags.SuppressEmbeds,
|
|
735
|
+
components: [],
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
else if (selectedScope === 'global') {
|
|
739
|
+
if (!context.appId) {
|
|
740
|
+
pendingModelContexts.delete(contextHash);
|
|
741
|
+
await interaction.editReply({
|
|
742
|
+
content: 'Cannot set global model: channel is not linked to a bot',
|
|
743
|
+
components: [],
|
|
744
|
+
});
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
await setGlobalModel({ appId: context.appId, modelId, variant });
|
|
748
|
+
await setChannelModel({ channelId: context.channelId, modelId, variant });
|
|
749
|
+
modelLogger.log(`Set global model ${modelId}${variantSuffix} for app ${context.appId} and channel ${context.channelId}`);
|
|
750
|
+
await interaction.editReply({
|
|
751
|
+
content: `Model set for this channel and as global default:\n**${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`\nAll channels will use this model (unless they have their own override).${agentTip}`,
|
|
752
|
+
flags: MessageFlags.SuppressEmbeds,
|
|
753
|
+
components: [],
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
// channel scope
|
|
758
|
+
await setChannelModel({ channelId: context.channelId, modelId, variant });
|
|
759
|
+
modelLogger.log(`Set model ${modelId}${variantSuffix} for channel ${context.channelId}`);
|
|
760
|
+
await interaction.editReply({
|
|
761
|
+
content: `Model preference set for this channel:\n**${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`\nAll new sessions in this channel will use this model.${agentTip}`,
|
|
762
|
+
flags: MessageFlags.SuppressEmbeds,
|
|
763
|
+
components: [],
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
// Clean up the context from memory
|
|
767
|
+
pendingModelContexts.delete(contextHash);
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
modelLogger.error('Error saving model preference:', error);
|
|
771
|
+
await interaction.editReply({
|
|
772
|
+
content: `Failed to save model preference: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
773
|
+
components: [],
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|