@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,945 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: termcast
|
|
3
|
+
description: Build TUIs with a Raycast-like React API using termcast. Implements @raycast/api components (List, Detail, Form, Action) rendered to the terminal via opentui.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# termcast — Build TUIs with a Raycast-like React API
|
|
7
|
+
|
|
8
|
+
termcast is a framework for building terminal user interfaces using React. It implements the Raycast extension API (`@raycast/api`) but renders to the terminal via opentui. If you know Raycast, you know termcast.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
bun install -g termcast
|
|
12
|
+
termcast new my-extension # scaffold
|
|
13
|
+
cd my-extension && termcast dev # hot-reload dev mode
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
IMPORTANT: before starting every task ALWAYS read opentui docs:
|
|
17
|
+
```bash
|
|
18
|
+
curl -s https://raw.githubusercontent.com/sst/opentui/refs/heads/main/packages/react/README.md
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Imports
|
|
22
|
+
|
|
23
|
+
For **new projects**, import from `termcast` and `@termcast/utils`:
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { List, Detail, Action, ActionPanel, showToast, Toast, Icon, Color } from 'termcast'
|
|
27
|
+
import { useCachedPromise, useCachedState } from '@termcast/utils'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`@raycast/api` imports still work (for porting existing extensions) but `termcast` is preferred for new code.
|
|
31
|
+
|
|
32
|
+
## Project Structure
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
my-extension/
|
|
36
|
+
package.json # must have "commands" array
|
|
37
|
+
src/
|
|
38
|
+
index.tsx # default command entry point
|
|
39
|
+
other-command.tsx # additional commands
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**package.json** must declare commands:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"name": "my-extension",
|
|
47
|
+
"commands": [
|
|
48
|
+
{
|
|
49
|
+
"name": "index",
|
|
50
|
+
"title": "Browse Items",
|
|
51
|
+
"description": "Main command",
|
|
52
|
+
"mode": "view"
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"termcast": "latest",
|
|
57
|
+
"@termcast/utils": "latest"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Each command file exports a default React component:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
export default function Command() {
|
|
66
|
+
return <List>...</List>
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For standalone scripts (examples, prototyping), use `renderWithProviders`:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
import { renderWithProviders } from 'termcast'
|
|
74
|
+
|
|
75
|
+
await renderWithProviders(<MyComponent />, {
|
|
76
|
+
extensionName: 'my-app', // required for LocalStorage/Cache to work
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 1. List — The Core Component
|
|
83
|
+
|
|
84
|
+
The simplest termcast app is a searchable list:
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import { List } from 'termcast'
|
|
88
|
+
|
|
89
|
+
export default function Command() {
|
|
90
|
+
return (
|
|
91
|
+
<List searchBarPlaceholder="Search items...">
|
|
92
|
+
<List.Item title="First Item" subtitle="A subtitle" />
|
|
93
|
+
<List.Item title="Second Item" accessories={[{ text: 'Badge' }]} />
|
|
94
|
+
<List.Item
|
|
95
|
+
title="Third Item"
|
|
96
|
+
accessories={[
|
|
97
|
+
{ tag: { value: 'Important', color: Color.Red } },
|
|
98
|
+
{ date: new Date() },
|
|
99
|
+
]}
|
|
100
|
+
/>
|
|
101
|
+
</List>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Key props on `List`:
|
|
107
|
+
- `navigationTitle` — title in the top bar
|
|
108
|
+
- `searchBarPlaceholder` — placeholder text in search
|
|
109
|
+
- `isLoading` — shows a loading indicator
|
|
110
|
+
- `isShowingDetail` — enables the side detail panel
|
|
111
|
+
- `spacingMode` — `'default'` (single-line) or `'relaxed'` (two-line items)
|
|
112
|
+
- `onSelectionChange` — callback when selection moves
|
|
113
|
+
- `onSearchTextChange` — callback when search text changes
|
|
114
|
+
- `throttle` — throttle search change events
|
|
115
|
+
|
|
116
|
+
Key props on `List.Item`:
|
|
117
|
+
- `title`, `subtitle` — main text
|
|
118
|
+
- `icon` — emoji string or `{ source: Icon.Star, tintColor: Color.Orange }`
|
|
119
|
+
- `accessories` — array of `{ text?, tag?, date?, icon? }`
|
|
120
|
+
- `keywords` — extra search terms
|
|
121
|
+
- `id` — stable identifier for selection tracking
|
|
122
|
+
- `detail` — side panel content (when `isShowingDetail` is true)
|
|
123
|
+
- `actions` — ActionPanel for this item
|
|
124
|
+
|
|
125
|
+
## 2. Actions
|
|
126
|
+
|
|
127
|
+
Actions are what users can do. The first action triggers on Enter. All actions show in the action panel (ctrl+k).
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
import { List, Action, ActionPanel, showToast, Toast, Icon } from 'termcast'
|
|
131
|
+
|
|
132
|
+
<List.Item
|
|
133
|
+
title="My Item"
|
|
134
|
+
actions={
|
|
135
|
+
<ActionPanel>
|
|
136
|
+
<Action
|
|
137
|
+
title="Open"
|
|
138
|
+
icon={Icon.Eye}
|
|
139
|
+
onAction={() => { /* primary action on Enter */ }}
|
|
140
|
+
/>
|
|
141
|
+
<Action
|
|
142
|
+
title="Refresh"
|
|
143
|
+
icon={Icon.ArrowClockwise}
|
|
144
|
+
shortcut={{ modifiers: ['ctrl'], key: 'r' }}
|
|
145
|
+
onAction={() => { /* triggered by ctrl+r directly */ }}
|
|
146
|
+
/>
|
|
147
|
+
<Action.CopyToClipboard title="Copy Name" content="My Item" />
|
|
148
|
+
</ActionPanel>
|
|
149
|
+
}
|
|
150
|
+
/>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Action sections
|
|
154
|
+
|
|
155
|
+
Group related actions:
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
<ActionPanel>
|
|
159
|
+
<ActionPanel.Section title="Primary">
|
|
160
|
+
<Action title="Open" onAction={() => {}} />
|
|
161
|
+
</ActionPanel.Section>
|
|
162
|
+
<ActionPanel.Section title="Copy">
|
|
163
|
+
<Action.CopyToClipboard title="Copy ID" content={item.id} />
|
|
164
|
+
<Action.CopyToClipboard title="Copy Title" content={item.title} />
|
|
165
|
+
</ActionPanel.Section>
|
|
166
|
+
</ActionPanel>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Built-in action types
|
|
170
|
+
|
|
171
|
+
- `Action` — generic action with `onAction`
|
|
172
|
+
- `Action.Push` — push a new view onto the navigation stack
|
|
173
|
+
- `Action.CopyToClipboard` — copy text to clipboard
|
|
174
|
+
- `Action.SubmitForm` — submit a form (used inside Form)
|
|
175
|
+
|
|
176
|
+
### Keyboard shortcuts
|
|
177
|
+
|
|
178
|
+
Shortcuts use `ctrl` or `alt` modifiers with letter keys. `cmd` (hyper) does **not** work in terminals — the parent terminal app intercepts it.
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
shortcut={{ modifiers: ['ctrl'], key: 'r' }} // ctrl+r
|
|
182
|
+
shortcut={{ modifiers: ['ctrl', 'shift'], key: 'r' }} // ctrl+shift+r
|
|
183
|
+
shortcut={{ modifiers: ['alt'], key: 'd' }} // alt+d
|
|
184
|
+
// Also available: Keyboard.Shortcut.Common.Refresh, etc.
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Note**: `ctrl+digit` shortcuts don't work reliably. Always use letters.
|
|
188
|
+
|
|
189
|
+
## 3. Navigation
|
|
190
|
+
|
|
191
|
+
Push and pop views onto a navigation stack. Esc goes back.
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
import { useNavigation, Detail, Action, ActionPanel } from 'termcast'
|
|
195
|
+
|
|
196
|
+
function ItemDetail({ item }: { item: Item }) {
|
|
197
|
+
const { pop } = useNavigation()
|
|
198
|
+
return (
|
|
199
|
+
<Detail
|
|
200
|
+
navigationTitle={item.title}
|
|
201
|
+
markdown={`# ${item.title}\n\n${item.description}`}
|
|
202
|
+
actions={
|
|
203
|
+
<ActionPanel>
|
|
204
|
+
<Action title="Go Back" onAction={() => { pop() }} />
|
|
205
|
+
</ActionPanel>
|
|
206
|
+
}
|
|
207
|
+
/>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// In a list item:
|
|
212
|
+
function MyList() {
|
|
213
|
+
const { push } = useNavigation()
|
|
214
|
+
return (
|
|
215
|
+
<List>
|
|
216
|
+
<List.Item
|
|
217
|
+
title="Item A"
|
|
218
|
+
actions={
|
|
219
|
+
<ActionPanel>
|
|
220
|
+
<Action
|
|
221
|
+
title="View Detail"
|
|
222
|
+
onAction={() => { push(<ItemDetail item={itemA} />) }}
|
|
223
|
+
/>
|
|
224
|
+
{/* Or use Action.Push for declarative navigation */}
|
|
225
|
+
<Action.Push
|
|
226
|
+
title="View Detail"
|
|
227
|
+
target={<ItemDetail item={itemA} />}
|
|
228
|
+
/>
|
|
229
|
+
</ActionPanel>
|
|
230
|
+
}
|
|
231
|
+
/>
|
|
232
|
+
</List>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Important**: props passed via `push()` are captured at push time and won't sync with parent state changes. If the child needs reactive parent state, use zustand or pass a zustand store via props.
|
|
238
|
+
|
|
239
|
+
## 4. Detail View
|
|
240
|
+
|
|
241
|
+
Full-screen markdown view with optional metadata sidebar:
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
import { Detail, Color } from 'termcast'
|
|
245
|
+
|
|
246
|
+
<Detail
|
|
247
|
+
navigationTitle="Server Status"
|
|
248
|
+
markdown={`# Server Status\n\nAll systems operational.\n\n| Service | Status |\n|---------|--------|\n| API | Running |\n| DB | Running |`}
|
|
249
|
+
metadata={
|
|
250
|
+
<Detail.Metadata>
|
|
251
|
+
<Detail.Metadata.Label title="Status" text={{ value: "Active", color: Color.Green }} />
|
|
252
|
+
<Detail.Metadata.Label title="Uptime" text="14d 3h" />
|
|
253
|
+
<Detail.Metadata.Separator />
|
|
254
|
+
<Detail.Metadata.Link
|
|
255
|
+
title="Dashboard"
|
|
256
|
+
target="https://example.com"
|
|
257
|
+
text="example.com"
|
|
258
|
+
/>
|
|
259
|
+
<Detail.Metadata.Separator />
|
|
260
|
+
<Detail.Metadata.TagList title="Tags">
|
|
261
|
+
<Detail.Metadata.TagList.Item text="production" color={Color.Green} />
|
|
262
|
+
<Detail.Metadata.TagList.Item text="critical" color={Color.Red} />
|
|
263
|
+
</Detail.Metadata.TagList>
|
|
264
|
+
</Detail.Metadata>
|
|
265
|
+
}
|
|
266
|
+
actions={
|
|
267
|
+
<ActionPanel>
|
|
268
|
+
<Action title="Refresh" onAction={() => {}} />
|
|
269
|
+
</ActionPanel>
|
|
270
|
+
}
|
|
271
|
+
/>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Metadata components
|
|
275
|
+
|
|
276
|
+
- `Label` — key-value row. `text` can be a string or `{ value, color }`
|
|
277
|
+
- `Separator` — horizontal divider
|
|
278
|
+
- `Link` — clickable link (OSC 8 hyperlinks in supported terminals)
|
|
279
|
+
- `TagList` — row of colored tags via `TagList.Item`
|
|
280
|
+
|
|
281
|
+
## 5. List with Side Detail Panel
|
|
282
|
+
|
|
283
|
+
Show a detail panel alongside the list. The detail updates as the user navigates items:
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
<List isShowingDetail={true} navigationTitle="Pokemon List">
|
|
287
|
+
{pokemons.map((pokemon) => (
|
|
288
|
+
<List.Item
|
|
289
|
+
key={pokemon.id}
|
|
290
|
+
title={pokemon.name}
|
|
291
|
+
subtitle={`#${pokemon.id}`}
|
|
292
|
+
detail={
|
|
293
|
+
<List.Item.Detail
|
|
294
|
+
markdown={`# ${pokemon.name}\n\nTypes: ${pokemon.types.join(', ')}`}
|
|
295
|
+
metadata={
|
|
296
|
+
<List.Item.Detail.Metadata>
|
|
297
|
+
<List.Item.Detail.Metadata.Label title="Height" text={`${pokemon.height}m`} />
|
|
298
|
+
<List.Item.Detail.Metadata.Label title="Weight" text={`${pokemon.weight}kg`} />
|
|
299
|
+
<List.Item.Detail.Metadata.Separator />
|
|
300
|
+
<List.Item.Detail.Metadata.TagList title="Types">
|
|
301
|
+
{pokemon.types.map((t) => (
|
|
302
|
+
<List.Item.Detail.Metadata.TagList.Item key={t} text={t} />
|
|
303
|
+
))}
|
|
304
|
+
</List.Item.Detail.Metadata.TagList>
|
|
305
|
+
</List.Item.Detail.Metadata>
|
|
306
|
+
}
|
|
307
|
+
/>
|
|
308
|
+
}
|
|
309
|
+
actions={
|
|
310
|
+
<ActionPanel>
|
|
311
|
+
<Action title="Toggle Detail" onAction={() => { setShowingDetail(!showingDetail) }} />
|
|
312
|
+
</ActionPanel>
|
|
313
|
+
}
|
|
314
|
+
/>
|
|
315
|
+
))}
|
|
316
|
+
</List>
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## 6. Sections and Dropdowns
|
|
320
|
+
|
|
321
|
+
### Sections
|
|
322
|
+
|
|
323
|
+
Group items with headers:
|
|
324
|
+
|
|
325
|
+
```tsx
|
|
326
|
+
<List>
|
|
327
|
+
<List.Section title="Fruits">
|
|
328
|
+
<List.Item title="Apple" />
|
|
329
|
+
<List.Item title="Banana" />
|
|
330
|
+
</List.Section>
|
|
331
|
+
<List.Section title="Vegetables">
|
|
332
|
+
<List.Item title="Carrot" />
|
|
333
|
+
</List.Section>
|
|
334
|
+
</List>
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Empty sections are automatically hidden.
|
|
338
|
+
|
|
339
|
+
### Dropdown filter
|
|
340
|
+
|
|
341
|
+
Add a dropdown next to the search bar:
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
<List
|
|
345
|
+
searchBarAccessory={
|
|
346
|
+
<List.Dropdown tooltip="Category" onChange={setCategory}>
|
|
347
|
+
<List.Dropdown.Item title="All" value="all" />
|
|
348
|
+
<List.Dropdown.Section title="Types">
|
|
349
|
+
<List.Dropdown.Item title="Beer" value="beer" />
|
|
350
|
+
<List.Dropdown.Item title="Wine" value="wine" />
|
|
351
|
+
</List.Dropdown.Section>
|
|
352
|
+
</List.Dropdown>
|
|
353
|
+
}
|
|
354
|
+
>
|
|
355
|
+
{filteredItems.map((item) => (
|
|
356
|
+
<List.Item key={item.id} title={item.name} />
|
|
357
|
+
))}
|
|
358
|
+
</List>
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## 7. Forms
|
|
362
|
+
|
|
363
|
+
Collect user input. Navigate fields with Tab/arrows. Submit with ctrl+enter or via action panel.
|
|
364
|
+
|
|
365
|
+
```tsx
|
|
366
|
+
import { Form, Action, ActionPanel, showToast, Toast } from 'termcast'
|
|
367
|
+
|
|
368
|
+
function CreateItem() {
|
|
369
|
+
return (
|
|
370
|
+
<Form
|
|
371
|
+
navigationTitle="New Item"
|
|
372
|
+
actions={
|
|
373
|
+
<ActionPanel>
|
|
374
|
+
<Action.SubmitForm
|
|
375
|
+
title="Create"
|
|
376
|
+
onSubmit={async (values) => {
|
|
377
|
+
await showToast({ style: Toast.Style.Success, title: 'Created!' })
|
|
378
|
+
}}
|
|
379
|
+
/>
|
|
380
|
+
</ActionPanel>
|
|
381
|
+
}
|
|
382
|
+
>
|
|
383
|
+
<Form.TextField id="name" title="Name" placeholder="Item name" />
|
|
384
|
+
<Form.TextArea id="description" title="Description" placeholder="Describe..." />
|
|
385
|
+
<Form.Dropdown id="priority" title="Priority">
|
|
386
|
+
<Form.Dropdown.Item value="high" title="High" />
|
|
387
|
+
<Form.Dropdown.Item value="medium" title="Medium" />
|
|
388
|
+
<Form.Dropdown.Item value="low" title="Low" />
|
|
389
|
+
</Form.Dropdown>
|
|
390
|
+
<Form.Checkbox id="urgent" title="Flags" label="Mark as urgent" />
|
|
391
|
+
<Form.DatePicker id="dueDate" title="Due Date" type={Form.DatePicker.Type.Date} />
|
|
392
|
+
<Form.Separator />
|
|
393
|
+
<Form.Description title="Help" text="Tab to move between fields. ctrl+enter to submit." />
|
|
394
|
+
</Form>
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Form field types: `TextField`, `PasswordField`, `TextArea`, `Checkbox`, `Dropdown`, `DatePicker`, `TagPicker`, `FilePicker`, `Separator`, `Description`.
|
|
400
|
+
|
|
401
|
+
## 8. Toasts
|
|
402
|
+
|
|
403
|
+
Show feedback to the user:
|
|
404
|
+
|
|
405
|
+
```tsx
|
|
406
|
+
import { showToast, Toast, showFailureToast } from 'termcast'
|
|
407
|
+
|
|
408
|
+
// Success
|
|
409
|
+
await showToast({ style: Toast.Style.Success, title: 'Saved', message: 'Item updated' })
|
|
410
|
+
|
|
411
|
+
// Failure
|
|
412
|
+
await showToast({ style: Toast.Style.Failure, title: 'Error', message: 'Connection failed' })
|
|
413
|
+
|
|
414
|
+
// From a caught error (shows title + error message)
|
|
415
|
+
await showFailureToast(error, { title: 'Failed to fetch' })
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## Data Fetching
|
|
421
|
+
|
|
422
|
+
### useCachedPromise
|
|
423
|
+
|
|
424
|
+
The primary hook for async data. Handles loading state, caching, revalidation, and pagination.
|
|
425
|
+
|
|
426
|
+
```tsx
|
|
427
|
+
import { useCachedPromise } from '@termcast/utils'
|
|
428
|
+
|
|
429
|
+
function MyList() {
|
|
430
|
+
const { data, isLoading, revalidate } = useCachedPromise(
|
|
431
|
+
async (query: string) => {
|
|
432
|
+
const response = await fetch(`/api/search?q=${query}`)
|
|
433
|
+
return response.json()
|
|
434
|
+
},
|
|
435
|
+
[searchText], // re-fetches when these change
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
return (
|
|
439
|
+
<List isLoading={isLoading}>
|
|
440
|
+
{data?.map((item) => (
|
|
441
|
+
<List.Item key={item.id} title={item.name} />
|
|
442
|
+
))}
|
|
443
|
+
</List>
|
|
444
|
+
)
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Pagination
|
|
449
|
+
|
|
450
|
+
For infinite scroll lists:
|
|
451
|
+
|
|
452
|
+
```tsx
|
|
453
|
+
const { data, isLoading, pagination } = useCachedPromise(
|
|
454
|
+
(query: string) => {
|
|
455
|
+
return async ({ cursor }: { page: number; cursor?: string }) => {
|
|
456
|
+
const result = await fetchItems({ query, pageToken: cursor })
|
|
457
|
+
return {
|
|
458
|
+
data: result.items,
|
|
459
|
+
hasMore: !!result.nextPageToken,
|
|
460
|
+
cursor: result.nextPageToken,
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
[searchText],
|
|
465
|
+
{ keepPreviousData: true },
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
return (
|
|
469
|
+
<List isLoading={isLoading} pagination={pagination}>
|
|
470
|
+
{data?.map((item) => <List.Item key={item.id} title={item.name} />)}
|
|
471
|
+
</List>
|
|
472
|
+
)
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### useCachedState
|
|
476
|
+
|
|
477
|
+
Persistent UI state that survives across sessions (stored in SQLite):
|
|
478
|
+
|
|
479
|
+
```tsx
|
|
480
|
+
import { useCachedState } from '@termcast/utils'
|
|
481
|
+
|
|
482
|
+
const [selectedAccount, setSelectedAccount] = useCachedState(
|
|
483
|
+
'selectedAccount', // key
|
|
484
|
+
'all', // default value
|
|
485
|
+
{ cacheNamespace: 'my-extension' },
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
const [isShowingDetail, setIsShowingDetail] = useCachedState(
|
|
489
|
+
'isShowingDetail',
|
|
490
|
+
true,
|
|
491
|
+
{ cacheNamespace: 'my-extension' },
|
|
492
|
+
)
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Revalidation pattern
|
|
496
|
+
|
|
497
|
+
After mutations, call `revalidate()` to refresh the data:
|
|
498
|
+
|
|
499
|
+
```tsx
|
|
500
|
+
const { data, revalidate } = useCachedPromise(fetchItems, [])
|
|
501
|
+
|
|
502
|
+
const handleDelete = async (id: string) => {
|
|
503
|
+
await deleteItem(id)
|
|
504
|
+
await showToast({ style: Toast.Style.Success, title: 'Deleted' })
|
|
505
|
+
revalidate() // refresh the list
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## Termcast-Exclusive Components
|
|
512
|
+
|
|
513
|
+
These components are unique to termcast — not available in Raycast. They can be placed inside `Detail.Metadata`, `List.Item.Detail.Metadata`, or used standalone in a Detail view.
|
|
514
|
+
|
|
515
|
+
### Graph (line chart with braille rendering)
|
|
516
|
+
|
|
517
|
+
```tsx
|
|
518
|
+
import { Graph, Color, Detail } from 'termcast'
|
|
519
|
+
|
|
520
|
+
<Detail
|
|
521
|
+
markdown="# Stock Price"
|
|
522
|
+
metadata={
|
|
523
|
+
<Graph height={15} xLabels={['Jan', 'Apr', 'Jul', 'Oct']} yTicks={6}>
|
|
524
|
+
<Graph.Line data={[150, 162, 175, 190, 201]} color={Color.Orange} title="AAPL" />
|
|
525
|
+
<Graph.Line data={[120, 135, 140, 155, 160]} color={Color.Blue} title="MSFT" />
|
|
526
|
+
</Graph>
|
|
527
|
+
}
|
|
528
|
+
/>
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
Variants: `'area'` (default), `'filled'`, `'striped'`. Set via the `variant` prop on Graph.
|
|
532
|
+
|
|
533
|
+
### BarGraph (vertical stacked bars)
|
|
534
|
+
|
|
535
|
+
```tsx
|
|
536
|
+
import { BarGraph } from 'termcast'
|
|
537
|
+
|
|
538
|
+
<BarGraph height={10} labels={['Mon', 'Tue', 'Wed', 'Thu', 'Fri']}>
|
|
539
|
+
<BarGraph.Series data={[40, 30, 25, 15, 50]} title="Direct" />
|
|
540
|
+
<BarGraph.Series data={[30, 35, 15, 20, 35]} title="Organic" />
|
|
541
|
+
<BarGraph.Series data={[20, 25, 10, 10, 25]} title="Referral" />
|
|
542
|
+
</BarGraph>
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### BarChart (horizontal stacked bars)
|
|
546
|
+
|
|
547
|
+
```tsx
|
|
548
|
+
import { BarChart } from 'termcast'
|
|
549
|
+
|
|
550
|
+
<BarChart
|
|
551
|
+
segments={[
|
|
552
|
+
{ title: 'Used', value: 75 },
|
|
553
|
+
{ title: 'Free', value: 25 },
|
|
554
|
+
]}
|
|
555
|
+
/>
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### CalendarHeatmap
|
|
559
|
+
|
|
560
|
+
GitHub-style contribution grid:
|
|
561
|
+
|
|
562
|
+
```tsx
|
|
563
|
+
import { CalendarHeatmap, Color } from 'termcast'
|
|
564
|
+
import type { CalendarHeatmapData } from 'termcast'
|
|
565
|
+
|
|
566
|
+
const data: CalendarHeatmapData[] = days.map((date) => ({
|
|
567
|
+
date: new Date(date),
|
|
568
|
+
value: Math.floor(Math.random() * 8),
|
|
569
|
+
}))
|
|
570
|
+
|
|
571
|
+
<CalendarHeatmap data={data} color={Color.Green} />
|
|
572
|
+
<CalendarHeatmap data={data} color={Color.Blue} emptyColor={Color.Purple} />
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Table
|
|
576
|
+
|
|
577
|
+
Borderless table with header background and alternating row stripes:
|
|
578
|
+
|
|
579
|
+
```tsx
|
|
580
|
+
import { Table } from 'termcast'
|
|
581
|
+
|
|
582
|
+
<Table
|
|
583
|
+
headers={['Region', 'Latency', 'Status']}
|
|
584
|
+
rows={[
|
|
585
|
+
['us-east-1', '**12ms**', 'OK'],
|
|
586
|
+
['eu-west-1', '*45ms*', 'OK'],
|
|
587
|
+
['ap-south-1', '`89ms`', 'Degraded'],
|
|
588
|
+
]}
|
|
589
|
+
/>
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
Cells support inline markdown: `**bold**`, `*italic*`, `` `code` ``, `~~strikethrough~~`, `[links](url)`.
|
|
593
|
+
|
|
594
|
+
### ProgressBar
|
|
595
|
+
|
|
596
|
+
Usage/progress display:
|
|
597
|
+
|
|
598
|
+
```tsx
|
|
599
|
+
import { ProgressBar } from 'termcast'
|
|
600
|
+
|
|
601
|
+
<ProgressBar title="Current session" value={37} percentageSuffix="used" label="Resets 9pm" />
|
|
602
|
+
<ProgressBar title="Weekly quota" value={82} percentageSuffix="used" label="Resets Mar 1" />
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### Row (side-by-side layout)
|
|
606
|
+
|
|
607
|
+
Place any components side by side:
|
|
608
|
+
|
|
609
|
+
```tsx
|
|
610
|
+
import { Row, Graph, BarGraph, Table, Color } from 'termcast'
|
|
611
|
+
|
|
612
|
+
<Row>
|
|
613
|
+
<Graph height={10} xLabels={['Mon', 'Fri']}>
|
|
614
|
+
<Graph.Line data={cpuData} color={Color.Orange} title="CPU" />
|
|
615
|
+
</Graph>
|
|
616
|
+
<Graph height={10} xLabels={['Mon', 'Fri']}>
|
|
617
|
+
<Graph.Line data={memData} color={Color.Blue} title="Memory" />
|
|
618
|
+
</Graph>
|
|
619
|
+
</Row>
|
|
620
|
+
|
|
621
|
+
<Row>
|
|
622
|
+
<Table headers={['Region', 'Latency']} rows={[['us-east', '12ms']]} />
|
|
623
|
+
<Table headers={['Endpoint', 'RPS']} rows={[['/api/auth', '1200']]} />
|
|
624
|
+
</Row>
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### Markdown (standalone block in metadata)
|
|
628
|
+
|
|
629
|
+
Render markdown anywhere inside metadata:
|
|
630
|
+
|
|
631
|
+
```tsx
|
|
632
|
+
import { Markdown, CalendarHeatmap, Color, Detail } from 'termcast'
|
|
633
|
+
|
|
634
|
+
<Detail.Metadata>
|
|
635
|
+
<Markdown content="**Long history** — 5 years of daily data in purple." />
|
|
636
|
+
<CalendarHeatmap data={longData} color={Color.Purple} />
|
|
637
|
+
<Markdown content="**Recent** — last 150 days in red." />
|
|
638
|
+
<CalendarHeatmap data={recentData} color={Color.Red} />
|
|
639
|
+
</Detail.Metadata>
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### Combining components in metadata
|
|
643
|
+
|
|
644
|
+
All termcast-exclusive components compose freely inside metadata:
|
|
645
|
+
|
|
646
|
+
```tsx
|
|
647
|
+
<Detail
|
|
648
|
+
markdown="# Dashboard"
|
|
649
|
+
metadata={
|
|
650
|
+
<Detail.Metadata>
|
|
651
|
+
<Detail.Metadata.Label title="Status" text={{ value: "Active", color: Color.Green }} />
|
|
652
|
+
<Detail.Metadata.Separator />
|
|
653
|
+
<Graph height={12} xLabels={['6h', '12h', '18h', '24h']}>
|
|
654
|
+
<Graph.Line data={requestsPerHour} color={Color.Orange} title="RPS" />
|
|
655
|
+
</Graph>
|
|
656
|
+
<Row>
|
|
657
|
+
<BarGraph height={8} labels={['Mon', 'Tue', 'Wed']}>
|
|
658
|
+
<BarGraph.Series data={[100, 150, 120]} title="2xx" />
|
|
659
|
+
<BarGraph.Series data={[5, 8, 3]} title="5xx" />
|
|
660
|
+
</BarGraph>
|
|
661
|
+
<Table
|
|
662
|
+
headers={['Endpoint', 'p99']}
|
|
663
|
+
rows={[['/api/auth', '45ms'], ['/api/data', '120ms']]}
|
|
664
|
+
/>
|
|
665
|
+
</Row>
|
|
666
|
+
<ProgressBar title="Rate limit" value={62} percentageSuffix="used" />
|
|
667
|
+
<CalendarHeatmap data={uptimeData} color={Color.Green} />
|
|
668
|
+
<Detail.Metadata.TagList title="Regions">
|
|
669
|
+
<Detail.Metadata.TagList.Item text="us-east" color={Color.Blue} />
|
|
670
|
+
<Detail.Metadata.TagList.Item text="eu-west" color={Color.Green} />
|
|
671
|
+
</Detail.Metadata.TagList>
|
|
672
|
+
</Detail.Metadata>
|
|
673
|
+
}
|
|
674
|
+
/>
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
679
|
+
## Real-World Patterns
|
|
680
|
+
|
|
681
|
+
These patterns are drawn from a production termcast extension (a Gmail TUI wrapping an existing CLI tool).
|
|
682
|
+
|
|
683
|
+
### Gluing a CLI tool with a TUI
|
|
684
|
+
|
|
685
|
+
The pattern: import your existing business logic, wrap it with termcast components.
|
|
686
|
+
|
|
687
|
+
```
|
|
688
|
+
┌─────────────────────────────────────────────┐
|
|
689
|
+
│ mail-tui.tsx (termcast UI) │
|
|
690
|
+
│ - List, Detail, Form, ActionPanel │
|
|
691
|
+
│ - useCachedPromise for data fetching │
|
|
692
|
+
│ - useCachedState for persistent prefs │
|
|
693
|
+
├─────────────────────────────────────────────┤
|
|
694
|
+
│ auth.ts / gmail-client.ts (business logic) │
|
|
695
|
+
│ - OAuth, API calls, data models │
|
|
696
|
+
│ - Pure TypeScript, no React dependencies │
|
|
697
|
+
└─────────────────────────────────────────────┘
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
The TUI file only handles rendering. All API calls, auth, and data processing live in separate files that work independently of the UI.
|
|
701
|
+
|
|
702
|
+
### Multi-account dropdown
|
|
703
|
+
|
|
704
|
+
```tsx
|
|
705
|
+
function AccountDropdown({ accounts, value, onChange }: {
|
|
706
|
+
accounts: { email: string }[]
|
|
707
|
+
value: string
|
|
708
|
+
onChange: (value: string) => void
|
|
709
|
+
}) {
|
|
710
|
+
return (
|
|
711
|
+
<List.Dropdown tooltip="Account" value={value} onChange={onChange}>
|
|
712
|
+
<List.Dropdown.Item title="All Accounts" value="all" icon={Icon.Globe} />
|
|
713
|
+
<List.Dropdown.Section title="Accounts">
|
|
714
|
+
{accounts.map((a) => (
|
|
715
|
+
<List.Dropdown.Item key={a.email} title={a.email} value={a.email} />
|
|
716
|
+
))}
|
|
717
|
+
</List.Dropdown.Section>
|
|
718
|
+
</List.Dropdown>
|
|
719
|
+
)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Usage:
|
|
723
|
+
<List searchBarAccessory={
|
|
724
|
+
<AccountDropdown accounts={accounts} value={selected} onChange={setSelected} />
|
|
725
|
+
}>
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
### Date-based section grouping
|
|
729
|
+
|
|
730
|
+
```tsx
|
|
731
|
+
function dateSection(dateStr: string): string {
|
|
732
|
+
const date = new Date(dateStr)
|
|
733
|
+
const now = new Date()
|
|
734
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
|
735
|
+
const yesterday = new Date(today.getTime() - 86400000)
|
|
736
|
+
|
|
737
|
+
if (date >= today) return 'Today'
|
|
738
|
+
if (date >= yesterday) return 'Yesterday'
|
|
739
|
+
return 'Older'
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const sections = useMemo(() => {
|
|
743
|
+
const groups = new Map<string, Item[]>()
|
|
744
|
+
for (const item of items) {
|
|
745
|
+
const section = dateSection(item.date)
|
|
746
|
+
const list = groups.get(section) ?? []
|
|
747
|
+
list.push(item)
|
|
748
|
+
groups.set(section, list)
|
|
749
|
+
}
|
|
750
|
+
return [...groups.entries()].map(([name, items]) => ({ name, items }))
|
|
751
|
+
}, [items])
|
|
752
|
+
|
|
753
|
+
return (
|
|
754
|
+
<List>
|
|
755
|
+
{sections.map((section) => (
|
|
756
|
+
<List.Section key={section.name} title={section.name}>
|
|
757
|
+
{section.items.map((item) => (
|
|
758
|
+
<List.Item key={item.id} title={item.title} />
|
|
759
|
+
))}
|
|
760
|
+
</List.Section>
|
|
761
|
+
))}
|
|
762
|
+
</List>
|
|
763
|
+
)
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
### Mutations with loading state
|
|
767
|
+
|
|
768
|
+
```tsx
|
|
769
|
+
const [activeMutations, setActiveMutations] = useState(0)
|
|
770
|
+
const isMutating = activeMutations > 0
|
|
771
|
+
|
|
772
|
+
const withMutation = async <T,>(fn: () => Promise<T>): Promise<T> => {
|
|
773
|
+
setActiveMutations((n) => n + 1)
|
|
774
|
+
try { return await fn() }
|
|
775
|
+
finally { setActiveMutations((n) => n - 1) }
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Usage in an action:
|
|
779
|
+
<Action
|
|
780
|
+
title="Archive"
|
|
781
|
+
onAction={() => withMutation(async () => {
|
|
782
|
+
await archiveItem(item.id)
|
|
783
|
+
await showToast({ style: Toast.Style.Success, title: 'Archived' })
|
|
784
|
+
revalidate()
|
|
785
|
+
})}
|
|
786
|
+
/>
|
|
787
|
+
|
|
788
|
+
<List isLoading={isLoading || isMutating}>
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
### Compose forms via Action.Push
|
|
792
|
+
|
|
793
|
+
```tsx
|
|
794
|
+
<ActionPanel.Section title="Reply & Forward">
|
|
795
|
+
<Action.Push
|
|
796
|
+
title="Reply"
|
|
797
|
+
icon={Icon.Reply}
|
|
798
|
+
shortcut={{ modifiers: ['ctrl'], key: 'r' }}
|
|
799
|
+
target={
|
|
800
|
+
<ComposeForm
|
|
801
|
+
mode={{ type: 'reply', threadId: thread.id }}
|
|
802
|
+
onSent={revalidate}
|
|
803
|
+
/>
|
|
804
|
+
}
|
|
805
|
+
/>
|
|
806
|
+
<Action.Push
|
|
807
|
+
title="Forward"
|
|
808
|
+
icon={Icon.Forward}
|
|
809
|
+
shortcut={{ modifiers: ['ctrl'], key: 'f' }}
|
|
810
|
+
target={
|
|
811
|
+
<ComposeForm
|
|
812
|
+
mode={{ type: 'forward', threadId: thread.id }}
|
|
813
|
+
onSent={revalidate}
|
|
814
|
+
/>
|
|
815
|
+
}
|
|
816
|
+
/>
|
|
817
|
+
</ActionPanel.Section>
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
---
|
|
821
|
+
|
|
822
|
+
## Porting from Raycast
|
|
823
|
+
|
|
824
|
+
If you're converting an existing Raycast extension:
|
|
825
|
+
|
|
826
|
+
1. **Change imports**: `@raycast/api` -> `termcast`, `@raycast/utils` -> `@termcast/utils`
|
|
827
|
+
2. **Keyboard modifiers**: `cmd` doesn't work in terminals. Replace with `ctrl` or `alt`
|
|
828
|
+
3. **Enter key**: named `return` in opentui key events
|
|
829
|
+
4. **Images**: no pixel rendering in terminals. Emoji and text fallbacks are used
|
|
830
|
+
5. **Everything else** works the same: List, Detail, Form, Action, Toast, Navigation, LocalStorage, Cache, Clipboard, OAuth
|
|
831
|
+
|
|
832
|
+
The compound component patterns are identical:
|
|
833
|
+
- `List.Item`, `List.Section`, `List.Dropdown`, `List.Dropdown.Item`
|
|
834
|
+
- `Detail.Metadata`, `Detail.Metadata.Label`, `Detail.Metadata.TagList`
|
|
835
|
+
- `Form.TextField`, `Form.Dropdown`, `Form.Dropdown.Item`
|
|
836
|
+
- `ActionPanel.Section`
|
|
837
|
+
|
|
838
|
+
---
|
|
839
|
+
|
|
840
|
+
## Gotchas
|
|
841
|
+
|
|
842
|
+
- **Use `logger.log`** instead of `console.log` — logs go to `app.log` in the extension directory
|
|
843
|
+
- **Never use `setTimeout`** for scheduling React state updates
|
|
844
|
+
- **Never pass functions** to `useEffect` dependencies — causes infinite loops
|
|
845
|
+
- **Minimize `useState`** — compute derived state inline when possible
|
|
846
|
+
- **Always use `.tsx` extension** for files with JSX
|
|
847
|
+
- **`useEffect` is discouraged** — colocate logic in event handlers when possible
|
|
848
|
+
- **Never use `as any`** — find proper types, import them, or use `@ts-expect-error` with explanation
|
|
849
|
+
- **Shortcuts**: use `ctrl`/`alt` + **letter** keys only (not digits)
|
|
850
|
+
- **`showFailureToast(error, { title })`** is the standard way to handle errors in actions
|
|
851
|
+
- **`revalidate()`** after every mutation to refresh data
|
|
852
|
+
|
|
853
|
+
## Running and Testing Extensions
|
|
854
|
+
|
|
855
|
+
### Running with `termcast dev`
|
|
856
|
+
|
|
857
|
+
The primary way to develop and try out an extension:
|
|
858
|
+
|
|
859
|
+
```bash
|
|
860
|
+
cd my-extension
|
|
861
|
+
termcast dev
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
This launches the TUI with hot-reload. File changes rebuild and refresh automatically. This is the fast iteration loop for development.
|
|
865
|
+
|
|
866
|
+
### Interactive experimentation with tuistory CLI
|
|
867
|
+
|
|
868
|
+
tuistory is a CLI tool for driving terminal applications from the shell — like Playwright but for TUIs. Use it to launch your extension, interact with it, and take snapshots without manual intervention.
|
|
869
|
+
|
|
870
|
+
**Always run `tuistory --help` first** to see the latest commands and options.
|
|
871
|
+
|
|
872
|
+
```bash
|
|
873
|
+
# Launch the extension in a managed terminal session
|
|
874
|
+
tuistory launch "termcast dev" -s my-ext --cols 120 --rows 36
|
|
875
|
+
|
|
876
|
+
# See current terminal state
|
|
877
|
+
tuistory -s my-ext snapshot --trim
|
|
878
|
+
|
|
879
|
+
# Interact
|
|
880
|
+
tuistory -s my-ext type "search query"
|
|
881
|
+
tuistory -s my-ext press enter
|
|
882
|
+
tuistory -s my-ext press ctrl k # open action panel
|
|
883
|
+
tuistory -s my-ext press tab # next form field
|
|
884
|
+
tuistory -s my-ext press esc # go back
|
|
885
|
+
|
|
886
|
+
# Take a screenshot as image
|
|
887
|
+
tuistory -s my-ext screenshot -o ./tmp/screenshot.jpg --pixel-ratio 2
|
|
888
|
+
|
|
889
|
+
# Observe after each action
|
|
890
|
+
tuistory -s my-ext snapshot --trim
|
|
891
|
+
|
|
892
|
+
# Cleanup
|
|
893
|
+
tuistory -s my-ext close
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
### Automated tests with vitest + tuistory JS API
|
|
897
|
+
|
|
898
|
+
tuistory provides a Playwright-style JS API for writing automated TUI tests. The workflow is **observe-act-observe**: take a snapshot, interact, take another snapshot.
|
|
899
|
+
|
|
900
|
+
```ts
|
|
901
|
+
import { test, expect } from 'vitest'
|
|
902
|
+
import { launchTerminal } from 'tuistory'
|
|
903
|
+
|
|
904
|
+
test('extension shows items and navigates to detail', async () => {
|
|
905
|
+
const session = await launchTerminal({
|
|
906
|
+
command: 'termcast',
|
|
907
|
+
args: ['dev'],
|
|
908
|
+
cols: 120,
|
|
909
|
+
rows: 36,
|
|
910
|
+
cwd: '/path/to/my-extension',
|
|
911
|
+
})
|
|
912
|
+
|
|
913
|
+
// Wait for the list to render
|
|
914
|
+
await session.waitForText('Search', { timeout: 10000 })
|
|
915
|
+
|
|
916
|
+
// Observe initial state
|
|
917
|
+
const initial = await session.text({ trimEnd: true })
|
|
918
|
+
expect(initial).toMatchInlineSnapshot()
|
|
919
|
+
|
|
920
|
+
// Type a search query
|
|
921
|
+
await session.type('project')
|
|
922
|
+
const filtered = await session.text({ trimEnd: true })
|
|
923
|
+
expect(filtered).toMatchInlineSnapshot()
|
|
924
|
+
|
|
925
|
+
// Press Enter to trigger primary action
|
|
926
|
+
await session.press('enter')
|
|
927
|
+
await session.waitForText('Detail', { timeout: 5000 })
|
|
928
|
+
const detail = await session.text({ trimEnd: true })
|
|
929
|
+
expect(detail).toMatchInlineSnapshot()
|
|
930
|
+
|
|
931
|
+
// Go back
|
|
932
|
+
await session.press('esc')
|
|
933
|
+
|
|
934
|
+
session.close()
|
|
935
|
+
}, 30000)
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
Run with:
|
|
939
|
+
|
|
940
|
+
```bash
|
|
941
|
+
vitest --run -u # fill in snapshots
|
|
942
|
+
vitest --run # verify snapshots match
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
Always leave `toMatchInlineSnapshot()` empty the first time, run with `-u` to fill them, then read back the test file to verify the captured output is correct.
|