@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
package/dist/tools.js
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// Voice assistant tool definitions for the GenAI worker.
|
|
2
|
+
// Provides tools for managing OpenCode sessions (create, submit, abort),
|
|
3
|
+
// listing chats, searching files, and reading session messages.
|
|
4
|
+
import { tool } from './ai-tool.js';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import net from 'node:net';
|
|
8
|
+
import {} from '@opencode-ai/sdk/v2';
|
|
9
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
10
|
+
import * as errore from 'errore';
|
|
11
|
+
const toolsLogger = createLogger(LogPrefix.TOOLS);
|
|
12
|
+
import { ShareMarkdown } from './markdown.js';
|
|
13
|
+
import { formatDistanceToNow } from './utils.js';
|
|
14
|
+
import pc from 'picocolors';
|
|
15
|
+
import { initializeOpencodeForDirectory, getOpencodeSystemMessage, } from './discord-bot.js';
|
|
16
|
+
export async function getTools({ onMessageCompleted, directory, }) {
|
|
17
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
18
|
+
if (getClient instanceof Error) {
|
|
19
|
+
throw new Error(getClient.message);
|
|
20
|
+
}
|
|
21
|
+
const client = getClient();
|
|
22
|
+
const markdownRenderer = new ShareMarkdown(client);
|
|
23
|
+
const providersResponse = await client.config.providers();
|
|
24
|
+
const providers = providersResponse.data?.providers || [];
|
|
25
|
+
// Helper: get last assistant model for a session (non-summary)
|
|
26
|
+
const getSessionModel = async (sessionId) => {
|
|
27
|
+
const res = await getClient().session.messages({ sessionID: sessionId });
|
|
28
|
+
const data = res.data;
|
|
29
|
+
if (!data || data.length === 0)
|
|
30
|
+
return undefined;
|
|
31
|
+
for (let i = data.length - 1; i >= 0; i--) {
|
|
32
|
+
const info = data?.[i]?.info;
|
|
33
|
+
if (info?.role === 'assistant') {
|
|
34
|
+
const ai = info;
|
|
35
|
+
if (!ai.summary && ai.providerID && ai.modelID) {
|
|
36
|
+
return { providerID: ai.providerID, modelID: ai.modelID };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
};
|
|
42
|
+
const tools = {
|
|
43
|
+
submitMessage: tool({
|
|
44
|
+
description: 'Submit a message to an existing chat session. Does not wait for the message to complete',
|
|
45
|
+
inputSchema: z.object({
|
|
46
|
+
sessionId: z.string().describe('The session ID to send message to'),
|
|
47
|
+
message: z.string().describe('The message text to send'),
|
|
48
|
+
}),
|
|
49
|
+
execute: async ({ sessionId, message }) => {
|
|
50
|
+
const sessionModel = await getSessionModel(sessionId);
|
|
51
|
+
// do not await
|
|
52
|
+
getClient()
|
|
53
|
+
.session.promptAsync({
|
|
54
|
+
sessionID: sessionId,
|
|
55
|
+
parts: [{ type: 'text', text: message }],
|
|
56
|
+
model: sessionModel,
|
|
57
|
+
system: getOpencodeSystemMessage({ sessionId }),
|
|
58
|
+
})
|
|
59
|
+
.then(async (response) => {
|
|
60
|
+
const markdownResult = await markdownRenderer.generate({
|
|
61
|
+
sessionID: sessionId,
|
|
62
|
+
lastAssistantOnly: true,
|
|
63
|
+
});
|
|
64
|
+
onMessageCompleted?.({
|
|
65
|
+
sessionId,
|
|
66
|
+
messageId: '',
|
|
67
|
+
markdown: errore.unwrapOr(markdownResult, ''),
|
|
68
|
+
});
|
|
69
|
+
})
|
|
70
|
+
.catch((error) => {
|
|
71
|
+
onMessageCompleted?.({
|
|
72
|
+
sessionId,
|
|
73
|
+
messageId: '',
|
|
74
|
+
error,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
success: true,
|
|
79
|
+
sessionId,
|
|
80
|
+
directive: 'Tell user that message has been sent successfully',
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
createNewChat: tool({
|
|
85
|
+
description: 'Start a new chat session with an initial message. Does not wait for the message to complete',
|
|
86
|
+
inputSchema: z.object({
|
|
87
|
+
message: z
|
|
88
|
+
.string()
|
|
89
|
+
.describe('The initial message to start the chat with'),
|
|
90
|
+
title: z.string().optional().describe('Optional title for the session'),
|
|
91
|
+
model: z
|
|
92
|
+
.object({
|
|
93
|
+
providerId: z
|
|
94
|
+
.string()
|
|
95
|
+
.describe('The provider ID (e.g., "anthropic", "openai")'),
|
|
96
|
+
modelId: z
|
|
97
|
+
.string()
|
|
98
|
+
.describe('The model ID (e.g., "claude-opus-4-20250514", "gpt-5")'),
|
|
99
|
+
})
|
|
100
|
+
.optional()
|
|
101
|
+
.describe('Optional model to use for this session'),
|
|
102
|
+
}),
|
|
103
|
+
execute: async ({ message, title }) => {
|
|
104
|
+
if (!message.trim()) {
|
|
105
|
+
throw new Error(`message must be a non empty string`);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const session = await getClient().session.create({
|
|
109
|
+
...(title ? { title } : {}),
|
|
110
|
+
});
|
|
111
|
+
if (!session.data) {
|
|
112
|
+
throw new Error('Failed to create session');
|
|
113
|
+
}
|
|
114
|
+
// do not await
|
|
115
|
+
getClient()
|
|
116
|
+
.session.promptAsync({
|
|
117
|
+
sessionID: session.data.id,
|
|
118
|
+
parts: [{ type: 'text', text: message }],
|
|
119
|
+
system: getOpencodeSystemMessage({ sessionId: session.data.id }),
|
|
120
|
+
})
|
|
121
|
+
.then(async (response) => {
|
|
122
|
+
const markdownResult = await markdownRenderer.generate({
|
|
123
|
+
sessionID: session.data.id,
|
|
124
|
+
lastAssistantOnly: true,
|
|
125
|
+
});
|
|
126
|
+
onMessageCompleted?.({
|
|
127
|
+
sessionId: session.data.id,
|
|
128
|
+
messageId: '',
|
|
129
|
+
markdown: errore.unwrapOr(markdownResult, ''),
|
|
130
|
+
});
|
|
131
|
+
})
|
|
132
|
+
.catch((error) => {
|
|
133
|
+
onMessageCompleted?.({
|
|
134
|
+
sessionId: session.data.id,
|
|
135
|
+
messageId: '',
|
|
136
|
+
error,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
success: true,
|
|
141
|
+
sessionId: session.data.id,
|
|
142
|
+
title: session.data.title,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
error: error instanceof Error
|
|
149
|
+
? error.message
|
|
150
|
+
: 'Failed to create chat session',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
155
|
+
listChats: tool({
|
|
156
|
+
description: 'Get a list of available chat sessions sorted by most recent',
|
|
157
|
+
inputSchema: z.object({}),
|
|
158
|
+
execute: async () => {
|
|
159
|
+
toolsLogger.log(`Listing opencode sessions`);
|
|
160
|
+
const sessions = await getClient().session.list();
|
|
161
|
+
if (!sessions.data) {
|
|
162
|
+
return { success: false, error: 'No sessions found' };
|
|
163
|
+
}
|
|
164
|
+
const sortedSessions = [...sessions.data]
|
|
165
|
+
.sort((a, b) => {
|
|
166
|
+
return b.time.updated - a.time.updated;
|
|
167
|
+
})
|
|
168
|
+
.slice(0, 20);
|
|
169
|
+
const sessionList = sortedSessions.map(async (session) => {
|
|
170
|
+
const finishedAt = session.time.updated;
|
|
171
|
+
const status = await (async () => {
|
|
172
|
+
if (session.revert)
|
|
173
|
+
return 'error';
|
|
174
|
+
const messagesResponse = await getClient().session.messages({
|
|
175
|
+
sessionID: session.id,
|
|
176
|
+
});
|
|
177
|
+
const messages = messagesResponse.data || [];
|
|
178
|
+
const lastMessage = messages[messages.length - 1];
|
|
179
|
+
if (lastMessage?.info.role === 'assistant' &&
|
|
180
|
+
!lastMessage.info.time.completed) {
|
|
181
|
+
return 'in_progress';
|
|
182
|
+
}
|
|
183
|
+
return 'finished';
|
|
184
|
+
})();
|
|
185
|
+
return {
|
|
186
|
+
id: session.id,
|
|
187
|
+
folder: session.directory,
|
|
188
|
+
status,
|
|
189
|
+
finishedAt: formatDistanceToNow(new Date(finishedAt)),
|
|
190
|
+
title: session.title,
|
|
191
|
+
prompt: session.title,
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
const resolvedList = await Promise.all(sessionList);
|
|
195
|
+
return {
|
|
196
|
+
success: true,
|
|
197
|
+
sessions: resolvedList,
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
}),
|
|
201
|
+
searchFiles: tool({
|
|
202
|
+
description: 'Search for files in a folder',
|
|
203
|
+
inputSchema: z.object({
|
|
204
|
+
folder: z
|
|
205
|
+
.string()
|
|
206
|
+
.optional()
|
|
207
|
+
.describe('The folder path to search in, optional. only use if user specifically asks for it'),
|
|
208
|
+
query: z.string().describe('The search query for files'),
|
|
209
|
+
}),
|
|
210
|
+
execute: async ({ folder, query }) => {
|
|
211
|
+
const results = await getClient().find.files({
|
|
212
|
+
query,
|
|
213
|
+
directory: folder,
|
|
214
|
+
});
|
|
215
|
+
return {
|
|
216
|
+
success: true,
|
|
217
|
+
files: results.data || [],
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
}),
|
|
221
|
+
readSessionMessages: tool({
|
|
222
|
+
description: 'Read messages from a chat session',
|
|
223
|
+
inputSchema: z.object({
|
|
224
|
+
sessionId: z.string().describe('The session ID to read messages from'),
|
|
225
|
+
lastAssistantOnly: z
|
|
226
|
+
.boolean()
|
|
227
|
+
.optional()
|
|
228
|
+
.describe('Only read the last assistant message'),
|
|
229
|
+
}),
|
|
230
|
+
execute: async ({ sessionId, lastAssistantOnly = false }) => {
|
|
231
|
+
if (lastAssistantOnly) {
|
|
232
|
+
const messages = await getClient().session.messages({
|
|
233
|
+
sessionID: sessionId,
|
|
234
|
+
});
|
|
235
|
+
if (!messages.data) {
|
|
236
|
+
return { success: false, error: 'No messages found' };
|
|
237
|
+
}
|
|
238
|
+
const assistantMessages = messages.data.filter((m) => m.info.role === 'assistant');
|
|
239
|
+
if (assistantMessages.length === 0) {
|
|
240
|
+
return {
|
|
241
|
+
success: false,
|
|
242
|
+
error: 'No assistant messages found',
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const lastMessage = assistantMessages[assistantMessages.length - 1];
|
|
246
|
+
const status = 'completed' in lastMessage.info.time &&
|
|
247
|
+
lastMessage.info.time.completed
|
|
248
|
+
? 'completed'
|
|
249
|
+
: 'in_progress';
|
|
250
|
+
const markdownResult = await markdownRenderer.generate({
|
|
251
|
+
sessionID: sessionId,
|
|
252
|
+
lastAssistantOnly: true,
|
|
253
|
+
});
|
|
254
|
+
if (markdownResult instanceof Error) {
|
|
255
|
+
throw new Error(markdownResult.message);
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
success: true,
|
|
259
|
+
markdown: markdownResult,
|
|
260
|
+
status,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
const markdownResult = await markdownRenderer.generate({
|
|
265
|
+
sessionID: sessionId,
|
|
266
|
+
});
|
|
267
|
+
if (markdownResult instanceof Error) {
|
|
268
|
+
throw new Error(markdownResult.message);
|
|
269
|
+
}
|
|
270
|
+
const messages = await getClient().session.messages({
|
|
271
|
+
sessionID: sessionId,
|
|
272
|
+
});
|
|
273
|
+
const lastMessage = messages.data?.[messages.data.length - 1];
|
|
274
|
+
const status = lastMessage?.info.role === 'assistant' &&
|
|
275
|
+
lastMessage?.info.time &&
|
|
276
|
+
'completed' in lastMessage.info.time &&
|
|
277
|
+
!lastMessage.info.time.completed
|
|
278
|
+
? 'in_progress'
|
|
279
|
+
: 'completed';
|
|
280
|
+
return {
|
|
281
|
+
success: true,
|
|
282
|
+
markdown: markdownResult,
|
|
283
|
+
status,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
}),
|
|
288
|
+
abortChat: tool({
|
|
289
|
+
description: 'Abort/stop an in-progress chat session',
|
|
290
|
+
inputSchema: z.object({
|
|
291
|
+
sessionId: z.string().describe('The session ID to abort'),
|
|
292
|
+
}),
|
|
293
|
+
execute: async ({ sessionId }) => {
|
|
294
|
+
try {
|
|
295
|
+
toolsLogger.log(`[ABORT] reason=voice-tool sessionId=${sessionId} - user requested abort via voice assistant tool`);
|
|
296
|
+
const result = await getClient().session.abort({
|
|
297
|
+
sessionID: sessionId,
|
|
298
|
+
});
|
|
299
|
+
if (!result.data) {
|
|
300
|
+
return {
|
|
301
|
+
success: false,
|
|
302
|
+
error: 'Failed to abort session',
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
success: true,
|
|
307
|
+
sessionId,
|
|
308
|
+
message: 'Session aborted successfully',
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
return {
|
|
313
|
+
success: false,
|
|
314
|
+
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
}),
|
|
319
|
+
getModels: tool({
|
|
320
|
+
description: 'Get all available AI models from all providers',
|
|
321
|
+
inputSchema: z.object({}),
|
|
322
|
+
execute: async () => {
|
|
323
|
+
try {
|
|
324
|
+
const providersResponse = await getClient().config.providers();
|
|
325
|
+
const providers = providersResponse.data?.providers || [];
|
|
326
|
+
const models = [];
|
|
327
|
+
providers.forEach((provider) => {
|
|
328
|
+
if (provider.models && typeof provider.models === 'object') {
|
|
329
|
+
Object.entries(provider.models).forEach(([modelId, model]) => {
|
|
330
|
+
models.push({
|
|
331
|
+
providerId: provider.id,
|
|
332
|
+
modelId: modelId,
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
return {
|
|
338
|
+
success: true,
|
|
339
|
+
models,
|
|
340
|
+
totalCount: models.length,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
return {
|
|
345
|
+
success: false,
|
|
346
|
+
error: error instanceof Error ? error.message : 'Failed to fetch models',
|
|
347
|
+
models: [],
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
}),
|
|
352
|
+
};
|
|
353
|
+
return {
|
|
354
|
+
tools,
|
|
355
|
+
providers,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// E2e test for /undo command.
|
|
2
|
+
// Validates that:
|
|
3
|
+
// 1. After /undo, session.revert state is set (files reverted, revert boundary marked)
|
|
4
|
+
// 2. Messages are NOT deleted yet (they stay until next prompt cleans them up)
|
|
5
|
+
// 3. On the next user message, reverted messages are cleaned up by OpenCode's
|
|
6
|
+
// SessionRevert.cleanup() and the model only sees pre-revert messages
|
|
7
|
+
//
|
|
8
|
+
// This matches the OpenCode TUI behavior (use-session-commands.tsx):
|
|
9
|
+
// - Pass the user message ID (not assistant ID)
|
|
10
|
+
// - Don't delete messages — just mark session as reverted
|
|
11
|
+
// - Cleanup happens automatically on next promptAsync()
|
|
12
|
+
//
|
|
13
|
+
// Uses opencode-deterministic-provider (no real LLM calls).
|
|
14
|
+
// Poll timeouts: 4s max, 100ms interval.
|
|
15
|
+
import { describe, test, expect } from 'vitest';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
|
|
19
|
+
import { waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
|
|
20
|
+
import { getThreadSession } from './database.js';
|
|
21
|
+
import { initializeOpencodeForDirectory } from './opencode.js';
|
|
22
|
+
const TEXT_CHANNEL_ID = '200000000000001200';
|
|
23
|
+
const e2eTest = describe;
|
|
24
|
+
e2eTest('/undo sets revert state and cleans up on next prompt', () => {
|
|
25
|
+
const ctx = setupQueueAdvancedSuite({
|
|
26
|
+
channelId: TEXT_CHANNEL_ID,
|
|
27
|
+
channelName: 'qa-undo-e2e',
|
|
28
|
+
dirName: 'qa-undo-e2e',
|
|
29
|
+
username: 'undo-tester',
|
|
30
|
+
});
|
|
31
|
+
test('undo sets revert state, next message cleans up reverted messages', async () => {
|
|
32
|
+
const markerPath = path.join(ctx.directories.projectDirectory, 'tmp', 'undo-marker.txt');
|
|
33
|
+
// 1. Send a message and wait for complete session (footer)
|
|
34
|
+
await ctx.discord
|
|
35
|
+
.channel(TEXT_CHANNEL_ID)
|
|
36
|
+
.user(TEST_USER_ID)
|
|
37
|
+
.sendMessage({
|
|
38
|
+
content: 'UNDO_FILE_MARKER',
|
|
39
|
+
});
|
|
40
|
+
const thread = await ctx.discord
|
|
41
|
+
.channel(TEXT_CHANNEL_ID)
|
|
42
|
+
.waitForThread({
|
|
43
|
+
timeout: 8_000,
|
|
44
|
+
predicate: (t) => {
|
|
45
|
+
return t.name === 'UNDO_FILE_MARKER';
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
const th = ctx.discord.thread(thread.id);
|
|
49
|
+
await th.waitForBotReply({ timeout: 8_000 });
|
|
50
|
+
await waitForFooterMessage({
|
|
51
|
+
discord: ctx.discord,
|
|
52
|
+
threadId: thread.id,
|
|
53
|
+
timeout: 8_000,
|
|
54
|
+
});
|
|
55
|
+
// 2. Get session ID and verify it has messages
|
|
56
|
+
const sessionId = await getThreadSession(thread.id);
|
|
57
|
+
expect(sessionId).toBeTruthy();
|
|
58
|
+
const getClient = await initializeOpencodeForDirectory(ctx.directories.projectDirectory);
|
|
59
|
+
if (getClient instanceof Error) {
|
|
60
|
+
throw getClient;
|
|
61
|
+
}
|
|
62
|
+
const beforeMessages = await getClient().session.messages({
|
|
63
|
+
sessionID: sessionId,
|
|
64
|
+
directory: ctx.directories.projectDirectory,
|
|
65
|
+
});
|
|
66
|
+
const beforeCount = (beforeMessages.data || []).length;
|
|
67
|
+
expect(beforeCount).toBeGreaterThan(0);
|
|
68
|
+
const beforeUserMessages = (beforeMessages.data || []).filter((m) => {
|
|
69
|
+
return m.info.role === 'user';
|
|
70
|
+
});
|
|
71
|
+
const beforeAssistantMessages = (beforeMessages.data || []).filter((m) => {
|
|
72
|
+
return m.info.role === 'assistant';
|
|
73
|
+
});
|
|
74
|
+
expect(beforeUserMessages.length).toBeGreaterThan(0);
|
|
75
|
+
expect(beforeAssistantMessages.length).toBeGreaterThan(0);
|
|
76
|
+
expect(fs.existsSync(markerPath)).toBe(true);
|
|
77
|
+
// Verify no revert state yet
|
|
78
|
+
const beforeSession = await getClient().session.get({
|
|
79
|
+
sessionID: sessionId,
|
|
80
|
+
});
|
|
81
|
+
expect(beforeSession.data?.revert).toBeFalsy();
|
|
82
|
+
// 3. Run /undo command
|
|
83
|
+
const { id: undoInteractionId } = await th
|
|
84
|
+
.user(TEST_USER_ID)
|
|
85
|
+
.runSlashCommand({ name: 'undo' });
|
|
86
|
+
const undoAck = await th.waitForInteractionAck({
|
|
87
|
+
interactionId: undoInteractionId,
|
|
88
|
+
timeout: 4_000,
|
|
89
|
+
});
|
|
90
|
+
expect(undoAck).toBeDefined();
|
|
91
|
+
await waitForBotMessageContaining({
|
|
92
|
+
discord: ctx.discord,
|
|
93
|
+
threadId: thread.id,
|
|
94
|
+
text: 'Undone - reverted last assistant message',
|
|
95
|
+
timeout: 8_000,
|
|
96
|
+
});
|
|
97
|
+
// 4. Verify session now has revert state set
|
|
98
|
+
const afterSession = await getClient().session.get({
|
|
99
|
+
sessionID: sessionId,
|
|
100
|
+
});
|
|
101
|
+
expect(afterSession.data?.revert).toBeTruthy();
|
|
102
|
+
expect(afterSession.data?.revert?.messageID).toBeTruthy();
|
|
103
|
+
// Messages should still exist (not deleted — cleanup happens on next prompt)
|
|
104
|
+
const afterMessages = await getClient().session.messages({
|
|
105
|
+
sessionID: sessionId,
|
|
106
|
+
directory: ctx.directories.projectDirectory,
|
|
107
|
+
});
|
|
108
|
+
expect((afterMessages.data || []).length).toBe(beforeCount);
|
|
109
|
+
// 5. Send a new message — this triggers SessionRevert.cleanup()
|
|
110
|
+
// which removes reverted messages before processing the new prompt
|
|
111
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
112
|
+
content: 'Reply with exactly: after-undo-message',
|
|
113
|
+
});
|
|
114
|
+
await waitForFooterMessage({
|
|
115
|
+
discord: ctx.discord,
|
|
116
|
+
threadId: thread.id,
|
|
117
|
+
timeout: 8_000,
|
|
118
|
+
afterMessageIncludes: 'after-undo-message',
|
|
119
|
+
});
|
|
120
|
+
// 6. Verify reverted messages were cleaned up
|
|
121
|
+
const finalMessages = await getClient().session.messages({
|
|
122
|
+
sessionID: sessionId,
|
|
123
|
+
directory: ctx.directories.projectDirectory,
|
|
124
|
+
});
|
|
125
|
+
const finalAssistantMessages = (finalMessages.data || []).filter((m) => {
|
|
126
|
+
return m.info.role === 'assistant';
|
|
127
|
+
});
|
|
128
|
+
// The original assistant message should have been cleaned up,
|
|
129
|
+
// only the new one (from after-undo-message) should remain
|
|
130
|
+
const originalAssistantStillExists = finalAssistantMessages.some((m) => {
|
|
131
|
+
return m.parts.some((p) => {
|
|
132
|
+
return p.type === 'text' && 'text' in p && p.text === 'ok';
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
// The first "ok" response was reverted and should be cleaned up.
|
|
136
|
+
// The new response for "after-undo-message" should produce a fresh "ok".
|
|
137
|
+
// We verify the total count dropped: the original user+assistant pair
|
|
138
|
+
// was removed, and replaced by just the new user+assistant pair.
|
|
139
|
+
expect(finalAssistantMessages.length).toBeLessThanOrEqual(beforeAssistantMessages.length);
|
|
140
|
+
// Revert state should be cleared after cleanup
|
|
141
|
+
const finalSession = await getClient().session.get({
|
|
142
|
+
sessionID: sessionId,
|
|
143
|
+
});
|
|
144
|
+
expect(finalSession.data?.revert).toBeFalsy();
|
|
145
|
+
// 7. Snapshot the Discord thread
|
|
146
|
+
expect(await th.text()).toMatchInlineSnapshot(`
|
|
147
|
+
"--- from: user (undo-tester)
|
|
148
|
+
UNDO_FILE_MARKER
|
|
149
|
+
--- from: assistant (TestBot)
|
|
150
|
+
⬥ creating undo file
|
|
151
|
+
⬥ undo file created
|
|
152
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
153
|
+
Undone - reverted last assistant message
|
|
154
|
+
--- from: user (undo-tester)
|
|
155
|
+
Reply with exactly: after-undo-message
|
|
156
|
+
--- from: assistant (TestBot)
|
|
157
|
+
⬥ ok
|
|
158
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
159
|
+
`);
|
|
160
|
+
}, 20_000);
|
|
161
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Unnest code blocks from list items for Discord.
|
|
2
|
+
// Discord doesn't render code blocks inside lists, so this hoists them
|
|
3
|
+
// to root level while preserving list structure.
|
|
4
|
+
import { Lexer } from 'marked';
|
|
5
|
+
export function unnestCodeBlocksFromLists(markdown) {
|
|
6
|
+
const lexer = new Lexer();
|
|
7
|
+
const tokens = lexer.lex(markdown);
|
|
8
|
+
const result = [];
|
|
9
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
10
|
+
const token = tokens[i];
|
|
11
|
+
const next = tokens[i + 1];
|
|
12
|
+
const chunk = (() => {
|
|
13
|
+
if (token.type === 'list') {
|
|
14
|
+
const segments = processListToken(token);
|
|
15
|
+
return renderSegments(segments);
|
|
16
|
+
}
|
|
17
|
+
return token.raw;
|
|
18
|
+
})();
|
|
19
|
+
if (!chunk) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const nextRaw = next?.raw ?? '';
|
|
23
|
+
const needsNewline = nextRaw &&
|
|
24
|
+
!chunk.endsWith('\n') &&
|
|
25
|
+
typeof nextRaw === 'string' &&
|
|
26
|
+
!nextRaw.startsWith('\n');
|
|
27
|
+
result.push(needsNewline ? chunk + '\n' : chunk);
|
|
28
|
+
}
|
|
29
|
+
return result.join('');
|
|
30
|
+
}
|
|
31
|
+
function processListToken(list) {
|
|
32
|
+
const segments = [];
|
|
33
|
+
const start = typeof list.start === 'number' ? list.start : parseInt(list.start, 10) || 1;
|
|
34
|
+
const prefix = list.ordered ? (i) => `${start + i}. ` : () => '- ';
|
|
35
|
+
for (let i = 0; i < list.items.length; i++) {
|
|
36
|
+
const item = list.items[i];
|
|
37
|
+
const itemSegments = processListItem(item, prefix(i));
|
|
38
|
+
segments.push(...itemSegments);
|
|
39
|
+
}
|
|
40
|
+
return segments;
|
|
41
|
+
}
|
|
42
|
+
function processListItem(item, prefix) {
|
|
43
|
+
const segments = [];
|
|
44
|
+
let currentText = [];
|
|
45
|
+
// Track if we've seen a code block - text after code uses continuation prefix
|
|
46
|
+
let seenCodeBlock = false;
|
|
47
|
+
const taskMarker = item.task ? (item.checked ? '[x] ' : '[ ] ') : '';
|
|
48
|
+
let wroteFirstListItem = false;
|
|
49
|
+
const flushText = () => {
|
|
50
|
+
const rawText = currentText.join('');
|
|
51
|
+
const text = rawText.trimEnd();
|
|
52
|
+
if (text.trim()) {
|
|
53
|
+
// After a code block, use '-' as continuation prefix to avoid repeating numbers
|
|
54
|
+
const effectivePrefix = seenCodeBlock ? '- ' : prefix;
|
|
55
|
+
const marker = !wroteFirstListItem ? taskMarker : '';
|
|
56
|
+
const normalizedText = normalizeListItemText({
|
|
57
|
+
text,
|
|
58
|
+
isTaskItem: item.task,
|
|
59
|
+
});
|
|
60
|
+
segments.push({
|
|
61
|
+
type: 'list-item',
|
|
62
|
+
prefix: effectivePrefix,
|
|
63
|
+
content: marker + normalizedText,
|
|
64
|
+
});
|
|
65
|
+
wroteFirstListItem = true;
|
|
66
|
+
}
|
|
67
|
+
currentText = [];
|
|
68
|
+
};
|
|
69
|
+
for (const token of item.tokens) {
|
|
70
|
+
if (token.type === 'code') {
|
|
71
|
+
flushText();
|
|
72
|
+
const codeToken = token;
|
|
73
|
+
const lang = codeToken.lang || '';
|
|
74
|
+
segments.push({
|
|
75
|
+
type: 'code',
|
|
76
|
+
content: '```' + lang + '\n' + codeToken.text + '\n```\n',
|
|
77
|
+
});
|
|
78
|
+
seenCodeBlock = true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (token.type === 'list') {
|
|
82
|
+
flushText();
|
|
83
|
+
// Recursively process nested list - segments bubble up
|
|
84
|
+
const nestedSegments = processListToken(token);
|
|
85
|
+
segments.push(...nestedSegments);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
currentText.push(extractText(token));
|
|
89
|
+
}
|
|
90
|
+
flushText();
|
|
91
|
+
// If no segments were created (empty item), return empty
|
|
92
|
+
if (segments.length === 0) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
// If item had no code blocks (all segments are list-items from this level),
|
|
96
|
+
// return original raw to preserve formatting
|
|
97
|
+
const hasCode = segments.some((s) => s.type === 'code');
|
|
98
|
+
if (!hasCode) {
|
|
99
|
+
return [{ type: 'list-item', prefix: '', content: item.raw }];
|
|
100
|
+
}
|
|
101
|
+
return segments;
|
|
102
|
+
}
|
|
103
|
+
function extractText(token) {
|
|
104
|
+
// Prefer raw to preserve newlines and markdown markers.
|
|
105
|
+
if ('raw' in token && typeof token.raw === 'string') {
|
|
106
|
+
return token.raw;
|
|
107
|
+
}
|
|
108
|
+
if (token.type === 'text') {
|
|
109
|
+
return token.text;
|
|
110
|
+
}
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
function normalizeListItemText({ text, isTaskItem, }) {
|
|
114
|
+
const withoutIndent = text.replace(/^\s+/, '');
|
|
115
|
+
if (!isTaskItem) {
|
|
116
|
+
return withoutIndent;
|
|
117
|
+
}
|
|
118
|
+
return withoutIndent.replace(/^\[(?: |x|X)\]\s+/, '');
|
|
119
|
+
}
|
|
120
|
+
function renderSegments(segments) {
|
|
121
|
+
const result = [];
|
|
122
|
+
for (let i = 0; i < segments.length; i++) {
|
|
123
|
+
const segment = segments[i];
|
|
124
|
+
const prev = segments[i - 1];
|
|
125
|
+
if (segment.type === 'code') {
|
|
126
|
+
// Add newline before code if previous was a list item
|
|
127
|
+
if (prev && prev.type === 'list-item') {
|
|
128
|
+
result.push('\n');
|
|
129
|
+
}
|
|
130
|
+
result.push(segment.content);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// list-item
|
|
134
|
+
if (segment.prefix) {
|
|
135
|
+
result.push(segment.prefix + segment.content + '\n');
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Raw content (no prefix means it's original raw)
|
|
139
|
+
// Ensure raw ends with newline for proper separation from next segment
|
|
140
|
+
const raw = segment.content.trimEnd();
|
|
141
|
+
result.push(raw + '\n');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return result.join('').trimEnd();
|
|
146
|
+
}
|