mixdog 0.9.1 → 0.9.3
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/package.json +9 -1
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +310 -67
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +20 -9
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +17 -9
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/rules-builder.cjs +32 -54
- package/src/mixdog-session-runtime.mjs +1040 -2036
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +17 -7
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +8 -12
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +13 -2
- package/src/rules/shared/01-tool.md +23 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
- package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +437 -1439
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/config.mjs +54 -2
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +280 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +465 -345
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +121 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/lib/session-ingest.mjs +107 -0
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +10 -7
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +156 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +302 -117
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +10 -292
- package/src/standalone/explore-tool.mjs +12 -5
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +25 -3
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2883 -778
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +355 -22
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +183 -101
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +161 -23
- package/src/tui/components/tool-output-format.test.mjs +87 -0
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +6731 -2333
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +569 -948
- package/src/tui/index.jsx +117 -7
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +469 -0
- package/src/tui/markdown/format-token.mjs +128 -76
- package/src/tui/markdown/format-token.test.mjs +61 -19
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +36 -9
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -657
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +80 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +75 -27
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +46 -12
- package/src/workflows/sequential/WORKFLOW.md +51 -0
- package/src/workflows/solo/WORKFLOW.md +12 -1
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +100 -12
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
package/src/tui/index.jsx
CHANGED
|
@@ -6,19 +6,24 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import React from 'react';
|
|
8
8
|
import { render } from 'ink';
|
|
9
|
-
import { createWriteStream, mkdirSync } from 'node:fs';
|
|
9
|
+
import { closeSync, constants as fsConstants, createWriteStream, mkdirSync, openSync, readSync } from 'node:fs';
|
|
10
10
|
import { tmpdir } from 'node:os';
|
|
11
11
|
import { dirname, join } from 'node:path';
|
|
12
12
|
import { performance } from 'node:perf_hooks';
|
|
13
13
|
import { App } from './App.jsx';
|
|
14
14
|
import { createEngineSession } from './engine.mjs';
|
|
15
15
|
import { installProcessSignalCleanup } from '../runtime/shared/process-shutdown.mjs';
|
|
16
|
+
import { touchUiHeartbeat } from '../runtime/channels/lib/runtime-paths.mjs';
|
|
16
17
|
import { emitTerminalBackground, loadThemeSettingFromConfig, theme } from './theme.mjs';
|
|
18
|
+
import { POP_KITTY, DISABLE_MODIFY_OTHER_KEYS } from './keyboard-protocol.mjs';
|
|
17
19
|
|
|
18
20
|
const TERMINAL_MODE_RESET = '\x1b[?1006l\x1b[?1005l\x1b[?1015l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?2004l\x1b[?25h';
|
|
19
21
|
const TERMINAL_OSC_RESET_BG = '\x1b]111\x07';
|
|
20
22
|
const TERMINAL_MODE_RESET_HIDDEN_CURSOR = TERMINAL_MODE_RESET.replace('\x1b[?25h', '\x1b[?25l');
|
|
21
23
|
const MOUSE_TRACKING_ON = '\x1b[?1000h\x1b[?1002h\x1b[?1006h';
|
|
24
|
+
// Keyboard-protocol teardown. App.jsx enables kitty + modifyOtherKeys
|
|
25
|
+
// synchronously at raw-mode-on (no query); here we just pop/disable them on
|
|
26
|
+
// exit. POP_KITTY / DISABLE_MODIFY_OTHER_KEYS come from keyboard-protocol.mjs.
|
|
22
27
|
const BOOT_PROFILE_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_BOOT_PROFILE || ''));
|
|
23
28
|
const BOOT_PROFILE_START = globalThis.__mixdogBootProfileStart || (globalThis.__mixdogBootProfileStart = performance.now());
|
|
24
29
|
const EXIT_WAIT_TIMEOUT_MS = positiveIntEnv('MIXDOG_TUI_EXIT_WAIT_MS', 2500);
|
|
@@ -136,6 +141,47 @@ function resolveTuiStderrLogPath() {
|
|
|
136
141
|
|| join(process.env.MIXDOG_RUNTIME_ROOT || join(tmpdir(), 'mixdog'), 'mixdog-tui.stderr.log');
|
|
137
142
|
}
|
|
138
143
|
|
|
144
|
+
/** Drain stdin so queued key/mouse bytes do not leak into the shell after exit. */
|
|
145
|
+
function drainStdin(stdin = process.stdin) {
|
|
146
|
+
if (!stdin.isTTY) return;
|
|
147
|
+
try {
|
|
148
|
+
while (stdin.read() !== null) {
|
|
149
|
+
/* discard */
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
/* stream may be destroyed */
|
|
153
|
+
}
|
|
154
|
+
if (process.platform === 'win32') return;
|
|
155
|
+
const tty = stdin;
|
|
156
|
+
const wasRaw = tty.isRaw === true;
|
|
157
|
+
let fd = -1;
|
|
158
|
+
try {
|
|
159
|
+
if (!wasRaw) tty.setRawMode?.(true);
|
|
160
|
+
fd = openSync('/dev/tty', fsConstants.O_RDONLY | fsConstants.O_NONBLOCK);
|
|
161
|
+
const buf = Buffer.alloc(1024);
|
|
162
|
+
for (let i = 0; i < 64; i++) {
|
|
163
|
+
if (readSync(fd, buf, 0, buf.length, null) <= 0) break;
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
/* EAGAIN, ENXIO, ENOENT, EBADF, EIO */
|
|
167
|
+
} finally {
|
|
168
|
+
if (fd >= 0) {
|
|
169
|
+
try {
|
|
170
|
+
closeSync(fd);
|
|
171
|
+
} catch {
|
|
172
|
+
/* ignore */
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!wasRaw) {
|
|
176
|
+
try {
|
|
177
|
+
tty.setRawMode?.(false);
|
|
178
|
+
} catch {
|
|
179
|
+
/* TTY may be gone */
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
139
185
|
function installTuiStderrGuard() {
|
|
140
186
|
if (process.env.MIXDOG_TUI_ALLOW_STDERR === '1') return () => {};
|
|
141
187
|
const originalWrite = process.stderr.write.bind(process.stderr);
|
|
@@ -177,9 +223,9 @@ function installTuiStderrGuard() {
|
|
|
177
223
|
};
|
|
178
224
|
}
|
|
179
225
|
|
|
180
|
-
export async function runTui({ provider, model, toolMode } = {}) {
|
|
226
|
+
export async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {}) {
|
|
181
227
|
const startedAt = performance.now();
|
|
182
|
-
bootProfile('run:start', { provider, model, toolMode });
|
|
228
|
+
bootProfile('run:start', { provider, model, toolMode, remote });
|
|
183
229
|
// The React/ink TUI needs a raw-mode-capable TTY (interactive input). In a
|
|
184
230
|
// pipe/redirect/CI, ink's input hooks throw — bail with a clear hint instead.
|
|
185
231
|
if (!process.stdin.isTTY) {
|
|
@@ -191,13 +237,19 @@ export async function runTui({ provider, model, toolMode } = {}) {
|
|
|
191
237
|
}
|
|
192
238
|
|
|
193
239
|
const restoreStderr = installTuiStderrGuard();
|
|
194
|
-
const restorePrimedInput = () =>
|
|
240
|
+
const restorePrimedInput = () => drainStdin(process.stdin);
|
|
195
241
|
let restored = false;
|
|
196
242
|
const restoreTerminal = () => {
|
|
197
243
|
if (restored) return;
|
|
198
244
|
restored = true;
|
|
199
245
|
restorePrimedInput();
|
|
200
|
-
try {
|
|
246
|
+
try {
|
|
247
|
+
process.stdout.write(
|
|
248
|
+
// Pop kitty + disable modifyOtherKeys BEFORE leaving the alt screen.
|
|
249
|
+
// Both are no-ops if nothing was enabled, so this is always safe.
|
|
250
|
+
`${TERMINAL_MODE_RESET}\x1b[0 q${POP_KITTY}${DISABLE_MODIFY_OTHER_KEYS}\x1b[?1049l${TERMINAL_MODE_RESET}${TERMINAL_OSC_RESET_BG}`,
|
|
251
|
+
);
|
|
252
|
+
} catch { /* ignore */ }
|
|
201
253
|
};
|
|
202
254
|
|
|
203
255
|
// Enter the alternate screen buffer before session/runtime boot so no stale
|
|
@@ -211,6 +263,10 @@ export async function runTui({ provider, model, toolMode } = {}) {
|
|
|
211
263
|
// fat block. PromptInput parks this hardware cursor at the insertion point via
|
|
212
264
|
// useCursor; the terminal also anchors IME composition to it.
|
|
213
265
|
process.stdout.write('\x1b[5 q'); // blinking bar
|
|
266
|
+
// NOTE: extended-keys enabling (kitty + modifyOtherKeys) is done by App.jsx's
|
|
267
|
+
// mount effect, SYNCHRONOUSLY at ink's raw-mode-on — no query, no round-trip —
|
|
268
|
+
// so the first Ctrl+Enter is already covered. Only teardown lives here (see
|
|
269
|
+
// restoreTerminal above).
|
|
214
270
|
|
|
215
271
|
process.on('exit', restoreTerminal);
|
|
216
272
|
|
|
@@ -223,7 +279,7 @@ export async function runTui({ provider, model, toolMode } = {}) {
|
|
|
223
279
|
|
|
224
280
|
let store;
|
|
225
281
|
try {
|
|
226
|
-
store = await createEngineSession({ provider, model, toolMode });
|
|
282
|
+
store = await createEngineSession({ provider, model, toolMode, remote });
|
|
227
283
|
bootProfile('store:ready', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
228
284
|
} catch (error) {
|
|
229
285
|
restoreTerminal();
|
|
@@ -260,11 +316,58 @@ export async function runTui({ provider, model, toolMode } = {}) {
|
|
|
260
316
|
},
|
|
261
317
|
});
|
|
262
318
|
|
|
319
|
+
// Zombie-Lead repro (2026-07-02): the render loop can wedge (all owning
|
|
320
|
+
// PIDs still alive) leaving active-instance.json's pid-only staleness
|
|
321
|
+
// check unable to detect it. Heartbeat every 30s so runtime-paths.mjs can
|
|
322
|
+
// fall back to a ui_heartbeat_at staleness check when pids look fine.
|
|
323
|
+
// Best-effort: a failed/late write just means the next tick catches up,
|
|
324
|
+
// and the timer is unref'd so it never keeps the process alive on its own.
|
|
325
|
+
const uiHeartbeatTimer = setInterval(() => {
|
|
326
|
+
try { touchUiHeartbeat(); } catch { /* ignore */ }
|
|
327
|
+
}, 30_000);
|
|
328
|
+
if (typeof uiHeartbeatTimer.unref === 'function') uiHeartbeatTimer.unref();
|
|
329
|
+
try { touchUiHeartbeat(); } catch { /* ignore */ }
|
|
330
|
+
|
|
331
|
+
// Zombie-Lead repro (2026-07-02): stdio can die (TTY hangup, EPIPE on a
|
|
332
|
+
// detached/piped stdout) without the process ever receiving SIGHUP/SIGTERM,
|
|
333
|
+
// leaving a zombie renderer looping forever with nothing left to draw to.
|
|
334
|
+
// Treat a dead stdio surface as fatal and route it through the same
|
|
335
|
+
// signalCleanup teardown used for signals.
|
|
336
|
+
// 'error' whitelist: only codes that mean the stream is truly gone.
|
|
337
|
+
// Transient EAGAIN (backpressure) or terminal-resize noise must NOT be
|
|
338
|
+
// treated as fatal here — only EPIPE / ERR_STREAM_DESTROYED / EIO.
|
|
339
|
+
const STDIO_DEATH_FATAL_CODES = new Set(['EPIPE', 'ERR_STREAM_DESTROYED', 'EIO']);
|
|
340
|
+
const stdioDeathListeners = [];
|
|
341
|
+
const registerStdioDeath = (stream, event, { requireCode = false } = {}) => {
|
|
342
|
+
if (!stream || typeof stream.on !== 'function') return;
|
|
343
|
+
const handler = (err) => {
|
|
344
|
+
if (requireCode && !(err && STDIO_DEATH_FATAL_CODES.has(err.code))) return;
|
|
345
|
+
void signalCleanup.run('stdio-dead', {
|
|
346
|
+
code: 1,
|
|
347
|
+
shouldExit: true,
|
|
348
|
+
error: err || new Error(`stdio ${event} (source: ${stream === process.stdin ? 'stdin' : stream === process.stdout ? 'stdout' : 'stderr'})`),
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
stream.on(event, handler);
|
|
352
|
+
stdioDeathListeners.push([stream, event, handler]);
|
|
353
|
+
};
|
|
354
|
+
// stdin end/close/error: TTY/pipe on the input side is gone — nothing left
|
|
355
|
+
// to read from, no point keeping the renderer alive.
|
|
356
|
+
registerStdioDeath(process.stdin, 'end');
|
|
357
|
+
registerStdioDeath(process.stdin, 'close');
|
|
358
|
+
registerStdioDeath(process.stdin, 'error', { requireCode: true });
|
|
359
|
+
// stdout/stderr: 'close' always means the fd is gone (fatal); 'error' is
|
|
360
|
+
// filtered to the whitelist above so resize/EAGAIN noise doesn't kill us.
|
|
361
|
+
registerStdioDeath(process.stdout, 'error', { requireCode: true });
|
|
362
|
+
registerStdioDeath(process.stdout, 'close');
|
|
363
|
+
registerStdioDeath(process.stderr, 'error', { requireCode: true });
|
|
364
|
+
registerStdioDeath(process.stderr, 'close');
|
|
365
|
+
|
|
263
366
|
// exitOnCtrlC:false — App handles Ctrl+C as an interrupt/line-clear so Ink
|
|
264
367
|
// does not exit abruptly. Explicit exits go through /exit or /quit so teardown
|
|
265
368
|
// still restores the cursor, mouse mode, and alternate screen cleanly.
|
|
266
369
|
try {
|
|
267
|
-
const instance = render(<App store={store} />, { exitOnCtrlC: false, maxFps: 120, onRender: makeRenderProfiler() });
|
|
370
|
+
const instance = render(<App store={store} forceOnboarding={forceOnboarding === true} />, { exitOnCtrlC: false, maxFps: 120, onRender: makeRenderProfiler() });
|
|
268
371
|
bootProfile('render:mounted', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
269
372
|
const { waitUntilExit } = instance;
|
|
270
373
|
// [mixdog fork] Hand the ink renderer's drag-selection setter to the store so
|
|
@@ -281,8 +384,15 @@ export async function runTui({ provider, model, toolMode } = {}) {
|
|
|
281
384
|
if (mouseTracking && typeof instance.getWordRectAt === 'function') {
|
|
282
385
|
store.getWordRectAt = instance.getWordRectAt;
|
|
283
386
|
}
|
|
387
|
+
if (mouseTracking && typeof instance.getLineRectAt === 'function') {
|
|
388
|
+
store.getLineRectAt = instance.getLineRectAt;
|
|
389
|
+
}
|
|
284
390
|
await waitUntilExit();
|
|
285
391
|
} finally {
|
|
392
|
+
clearInterval(uiHeartbeatTimer);
|
|
393
|
+
for (const [stream, event, handler] of stdioDeathListeners.splice(0)) {
|
|
394
|
+
try { stream.off(event, handler); } catch { /* ignore */ }
|
|
395
|
+
}
|
|
286
396
|
signalCleanup.uninstall();
|
|
287
397
|
try {
|
|
288
398
|
await disposeStoreOnce('cli-react-exit');
|
|
@@ -122,14 +122,52 @@ export function lineEnd(text, offset) {
|
|
|
122
122
|
return end === -1 ? value.length : end;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
// Return the [start,end) run of same-class characters (word-like vs
|
|
126
|
+
// non-word-like) surrounding `offset`, used by double-click word selection.
|
|
127
|
+
// Mirrors typical desktop double-click behavior: clicking inside a run of
|
|
128
|
+
// letters/digits selects that whole run; clicking on a run of punctuation or
|
|
129
|
+
// whitespace selects that run instead (still deterministic, never empty).
|
|
130
|
+
export function wordRangeAt(text, offset) {
|
|
131
|
+
const value = String(text || '');
|
|
132
|
+
const units = graphemeUnits(value);
|
|
133
|
+
if (units.length === 0) return { start: 0, end: 0 };
|
|
134
|
+
const off = clampOffset(value, offset);
|
|
135
|
+
let idx = units.findIndex((u) => off >= u.start && off < u.end);
|
|
136
|
+
if (idx === -1) {
|
|
137
|
+
idx = units.length - 1;
|
|
138
|
+
for (let i = 0; i < units.length; i += 1) {
|
|
139
|
+
if (units[i].start >= off) { idx = i > 0 ? i - 1 : 0; break; }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const targetIsWord = wordLike(units[idx].segment);
|
|
143
|
+
let start = idx;
|
|
144
|
+
while (start > 0 && wordLike(units[start - 1].segment) === targetIsWord) start -= 1;
|
|
145
|
+
let end = idx;
|
|
146
|
+
while (end < units.length - 1 && wordLike(units[end + 1].segment) === targetIsWord) end += 1;
|
|
147
|
+
return { start: units[start].start, end: units[end].end };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// `hasTrailingContent` (default derived from the full text) decides whether a
|
|
151
|
+
// caret that lands FLUSH on the last column (col === w) rolls to the next row.
|
|
152
|
+
// It must reflect whether ANY rendered cell follows this offset in the FULL
|
|
153
|
+
// text — not just the sliced prefix — otherwise a caret at a mid-text soft-wrap
|
|
154
|
+
// boundary reports {row N, col w} while ink already rendered the next glyph on
|
|
155
|
+
// row N+1, dropping the caret outside the box. PromptInput also appends a
|
|
156
|
+
// trailing space cell when the cursor sits at end-of-input, so "end of text"
|
|
157
|
+
// still has a following cell there; callers pass hasTrailingContent=true for
|
|
158
|
+
// that case.
|
|
159
|
+
export function caretPosition(text, offset, width, hasTrailingContent = undefined) {
|
|
126
160
|
const value = String(text || '');
|
|
127
161
|
const before = value.slice(0, offset);
|
|
128
162
|
const w = safeWidth(width);
|
|
163
|
+
const followsInFullText = hasTrailingContent === undefined
|
|
164
|
+
? offset < value.length
|
|
165
|
+
: hasTrailingContent === true;
|
|
129
166
|
let row = 0;
|
|
130
167
|
let col = 0;
|
|
131
|
-
|
|
132
|
-
for (
|
|
168
|
+
const segments = [...graphemeSegmenter.segment(before)];
|
|
169
|
+
for (let i = 0; i < segments.length; i += 1) {
|
|
170
|
+
const { segment } = segments[i];
|
|
133
171
|
if (segment === '\n') {
|
|
134
172
|
row += 1;
|
|
135
173
|
col = 0;
|
|
@@ -139,15 +177,24 @@ export function caretPosition(text, offset, width) {
|
|
|
139
177
|
const segmentWidth = stringWidth(segment);
|
|
140
178
|
if (segmentWidth === 0) continue;
|
|
141
179
|
|
|
180
|
+
// Mirror ink's wrap-ansi wrapWord (wrap="hard", wordWrap:false): a glyph
|
|
181
|
+
// that would overflow the row is pushed WHOLE to the next row (col never
|
|
182
|
+
// exceeds w for wide chars — no half-column left behind), and a glyph that
|
|
183
|
+
// lands flush on the last column starts a new row ONLY when more rendered
|
|
184
|
+
// content follows. Whether content follows is decided from the FULL text
|
|
185
|
+
// (followsInFullText), not the sliced prefix, so a caret at a mid-text wrap
|
|
186
|
+
// boundary rolls to row N+1 exactly as ink renders it. The old
|
|
187
|
+
// `col >= w → row += floor(col/w)` over-counted a row for width-2 glyphs at
|
|
188
|
+
// odd widths (col could reach w+1), landing the cursor one row too low.
|
|
142
189
|
if (col > 0 && col + segmentWidth > w) {
|
|
143
190
|
row += 1;
|
|
144
191
|
col = 0;
|
|
145
192
|
}
|
|
146
|
-
|
|
147
193
|
col += segmentWidth;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
194
|
+
const moreFollows = i < segments.length - 1 || followsInFullText;
|
|
195
|
+
if (col === w && moreFollows) {
|
|
196
|
+
row += 1;
|
|
197
|
+
col = 0;
|
|
151
198
|
}
|
|
152
199
|
}
|
|
153
200
|
|
|
@@ -185,13 +232,16 @@ function boundaryPositions(text, width) {
|
|
|
185
232
|
for (const unit of graphemeUnits(value)) offsets.push(unit.end);
|
|
186
233
|
|
|
187
234
|
const seen = new Set();
|
|
188
|
-
|
|
235
|
+
const positions = offsets
|
|
189
236
|
.filter((offset) => {
|
|
190
237
|
if (seen.has(offset)) return false;
|
|
191
238
|
seen.add(offset);
|
|
192
239
|
return true;
|
|
193
240
|
})
|
|
241
|
+
// Decide the flush-boundary advance from the FULL text so a soft-wrap
|
|
242
|
+
// offset reports {row N+1, col 0} (matching ink) instead of {row N, col w}.
|
|
194
243
|
.map((offset) => ({ offset, ...caretPosition(value, offset, width) }));
|
|
244
|
+
return positions;
|
|
195
245
|
}
|
|
196
246
|
|
|
197
247
|
export function verticalOffset(text, offset, width, direction, preferredColumn = null) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
moveCursor,
|
|
5
|
+
previousWordOffset,
|
|
6
|
+
nextWordOffset,
|
|
7
|
+
selectionRange,
|
|
8
|
+
verticalOffset,
|
|
9
|
+
wordRangeAt,
|
|
10
|
+
lineStart,
|
|
11
|
+
lineEnd,
|
|
12
|
+
} from './input-editing.mjs';
|
|
13
|
+
|
|
14
|
+
// Whole-word extend (Ctrl+Shift+Left/Right) relies on moveCursor(extend:true)
|
|
15
|
+
// starting/keeping an anchor while jumping by word.
|
|
16
|
+
test('ctrl+shift+right extends selection by whole word', () => {
|
|
17
|
+
const draft = { value: 'hello world foo', cursor: 0, selectionAnchor: null };
|
|
18
|
+
const next = moveCursor(draft, nextWordOffset(draft.value, draft.cursor), { extend: true });
|
|
19
|
+
assert.equal(next.cursor, 'hello'.length);
|
|
20
|
+
assert.equal(next.selectionAnchor, 0);
|
|
21
|
+
assert.deepEqual(selectionRange(next), { start: 0, end: 5 });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('ctrl+shift+left extends selection backward by whole word', () => {
|
|
25
|
+
const value = 'hello world';
|
|
26
|
+
const draft = { value, cursor: value.length, selectionAnchor: null };
|
|
27
|
+
const next = moveCursor(draft, previousWordOffset(value, value.length), { extend: true });
|
|
28
|
+
assert.equal(next.selectionAnchor, value.length);
|
|
29
|
+
assert.deepEqual(selectionRange(next), { start: 'hello '.length, end: value.length });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Shift+Up on the first line yields no vertical move (targetRow < 0) → callers
|
|
33
|
+
// then extend to document start (offset 0).
|
|
34
|
+
test('verticalOffset returns same cursor on first line (up)', () => {
|
|
35
|
+
const value = 'line1\nline2';
|
|
36
|
+
const moved = verticalOffset(value, 2, 80, -1, null);
|
|
37
|
+
assert.equal(moved.cursor, 2, 'no vertical move available above first line');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('shift+up to document start extends selection to offset 0', () => {
|
|
41
|
+
const value = 'abc';
|
|
42
|
+
const draft = { value, cursor: 2, selectionAnchor: null };
|
|
43
|
+
const next = moveCursor(draft, 0, { extend: true });
|
|
44
|
+
assert.deepEqual(selectionRange(next), { start: 0, end: 2 });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('shift+down to document end extends selection to value.length', () => {
|
|
48
|
+
const value = 'abc';
|
|
49
|
+
const draft = { value, cursor: 1, selectionAnchor: null };
|
|
50
|
+
const next = moveCursor(draft, value.length, { extend: true });
|
|
51
|
+
assert.deepEqual(selectionRange(next), { start: 1, end: 3 });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Double-click word select: clicking anywhere inside a word run selects the
|
|
55
|
+
// whole run, and a click on punctuation/whitespace selects that run instead.
|
|
56
|
+
test('wordRangeAt selects the word run under the offset', () => {
|
|
57
|
+
const value = 'hello world foo';
|
|
58
|
+
assert.deepEqual(wordRangeAt(value, 2), { start: 0, end: 5 });
|
|
59
|
+
assert.deepEqual(wordRangeAt(value, 6), { start: 6, end: 11 });
|
|
60
|
+
assert.deepEqual(wordRangeAt(value, 0), { start: 0, end: 5 });
|
|
61
|
+
assert.deepEqual(wordRangeAt(value, value.length), { start: 12, end: 15 });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('wordRangeAt selects a punctuation/whitespace run distinctly from words', () => {
|
|
65
|
+
const value = 'foo---bar';
|
|
66
|
+
assert.deepEqual(wordRangeAt(value, 4), { start: 3, end: 6 });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Triple-click line select uses lineStart/lineEnd directly.
|
|
70
|
+
test('lineStart/lineEnd bound the current line for triple-click select', () => {
|
|
71
|
+
const value = 'line1\nline2\nline3';
|
|
72
|
+
const offset = value.indexOf('line2') + 2;
|
|
73
|
+
assert.equal(lineStart(value, offset), 6);
|
|
74
|
+
assert.equal(lineEnd(value, offset), 11);
|
|
75
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/tui/keyboard-protocol.mjs — terminal extended-keys capability gate.
|
|
3
|
+
*
|
|
4
|
+
* We do NOT query the terminal. Instead, we enable the
|
|
5
|
+
* kitty keyboard protocol AND xterm modifyOtherKeys SYNCHRONOUSLY at the moment
|
|
6
|
+
* raw mode turns on (see App.jsx's mount effect), gated by the allowlist below.
|
|
7
|
+
* Writing both enables unconditionally is safe — a terminal honors whichever it
|
|
8
|
+
* implements (Windows Terminal 1.24 has no kitty support but DOES honor
|
|
9
|
+
* modifyOtherKeys), and doing it before the first keypair is read removes the
|
|
10
|
+
* round-trip race that made the first Ctrl+Enter submit instead of inserting a
|
|
11
|
+
* newline.
|
|
12
|
+
*
|
|
13
|
+
* Enable / disable byte sequences (used by App.jsx enable + index.jsx teardown):
|
|
14
|
+
* ENABLE_KITTY_KEYBOARD \x1b[>1u push kitty flags=1 (disambiguate)
|
|
15
|
+
* ENABLE_MODIFY_OTHER_KEYS \x1b[>4;2m xterm modifyOtherKeys level 2
|
|
16
|
+
* POP_KITTY \x1b[<u pop the kitty stack entry
|
|
17
|
+
* DISABLE_MODIFY_OTHER_KEYS \x1b[>4;0m modifyOtherKeys off
|
|
18
|
+
*/
|
|
19
|
+
export const ENABLE_KITTY_KEYBOARD = '\x1b[>1u';
|
|
20
|
+
export const ENABLE_MODIFY_OTHER_KEYS = '\x1b[>4;2m';
|
|
21
|
+
export const POP_KITTY = '\x1b[<u';
|
|
22
|
+
export const DISABLE_MODIFY_OTHER_KEYS = '\x1b[>4;0m';
|
|
23
|
+
|
|
24
|
+
// Allowlist of terminals known to honor kitty and/or xterm modifyOtherKeys.
|
|
25
|
+
// VS Code's xterm.js
|
|
26
|
+
// integrated terminal mishandles these sequences, so it is excluded first.
|
|
27
|
+
// Honors the MIXDOG_TUI_EXTENDED_KEYS opt-out (=0) / override (=1).
|
|
28
|
+
export function supportsExtendedKeys() {
|
|
29
|
+
const raw = String(process.env.MIXDOG_TUI_EXTENDED_KEYS ?? '').trim();
|
|
30
|
+
if (/^(0|false|no|off)$/i.test(raw)) return false;
|
|
31
|
+
if (/^(1|true|yes|on)$/i.test(raw)) return true;
|
|
32
|
+
if (process.env.TERM_PROGRAM === 'vscode') return false;
|
|
33
|
+
if (process.env.WT_SESSION) return true; // Windows Terminal
|
|
34
|
+
if (process.env.TERM?.includes('kitty')) return true;
|
|
35
|
+
if (process.env.KITTY_WINDOW_ID) return true;
|
|
36
|
+
if (process.env.TERM_PROGRAM === 'WezTerm') return true;
|
|
37
|
+
if (process.env.TERM_PROGRAM === 'ghostty') return true;
|
|
38
|
+
if (process.env.TERM === 'xterm-ghostty') return true;
|
|
39
|
+
if (process.env.TMUX) return true;
|
|
40
|
+
if (process.env.TERM_PROGRAM === 'iTerm.app') return true;
|
|
41
|
+
return false;
|
|
42
|
+
}
|