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
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import React, { useEffect, useLayoutEffect, useState, useRef } from 'react';
|
|
19
19
|
import { Box, Text, useInput, usePaste, useStdin } from 'ink';
|
|
20
20
|
import stringWidth from 'string-width';
|
|
21
|
-
import { theme } from '../theme.mjs';
|
|
21
|
+
import { theme, surfaceBackground } from '../theme.mjs';
|
|
22
22
|
import {
|
|
23
23
|
caretPosition,
|
|
24
24
|
clearSelection,
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
replaceSelection,
|
|
39
39
|
selectionRange,
|
|
40
40
|
verticalOffset,
|
|
41
|
+
wordRangeAt,
|
|
41
42
|
} from '../input-editing.mjs';
|
|
42
43
|
|
|
43
44
|
function hintStyle(tone) {
|
|
@@ -52,6 +53,26 @@ function hintStyle(tone) {
|
|
|
52
53
|
// paddingX of 1, so the typing start can sit directly against that padding
|
|
53
54
|
// without an extra guard column.
|
|
54
55
|
const IME_LEFT_GUARD_COLUMNS = 0;
|
|
56
|
+
|
|
57
|
+
// Monotone voice indicator glyphs — right end of the prompt box. All frames
|
|
58
|
+
// are 1-cell-wide (no emoji, no string-width special-casing needed). Modes:
|
|
59
|
+
// 'off' — voice disabled and no install in flight: renders nothing,
|
|
60
|
+
// zero reserved columns.
|
|
61
|
+
// 'installing' — voice-runtime download in progress (progressHint active):
|
|
62
|
+
// braille spinner only.
|
|
63
|
+
// 'idle' — voice enabled, recorder idle: a slow "breathing" pulse
|
|
64
|
+
// (◎ ◍ ◉ ◍) so the indicator isn't a dead static glyph.
|
|
65
|
+
// 'recording' — steady ◉ + a braille spinner riding next to it (2 cells).
|
|
66
|
+
// 'transcribing' — braille spinner only (recording just stopped, awaiting
|
|
67
|
+
// the whisper-server round-trip).
|
|
68
|
+
// A single ~120ms interval drives BOTH the fast braille cycle and the slower
|
|
69
|
+
// idle pulse (idle advances one frame every VOICE_IDLE_FRAME_TICKS ticks) —
|
|
70
|
+
// one timer instead of two, and it only exists while the indicator is
|
|
71
|
+
// visible (mode !== 'off'); it's torn down otherwise, never a permanent timer.
|
|
72
|
+
const VOICE_BRAILLE_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧'];
|
|
73
|
+
const VOICE_IDLE_FRAMES = ['◎', '◍', '◉', '◍'];
|
|
74
|
+
const VOICE_TICK_MS = 120;
|
|
75
|
+
const VOICE_IDLE_FRAME_TICKS = 5; // ~600ms per idle frame (5 * 120ms)
|
|
55
76
|
// Coalesce prompt mouse-drag extend commits (SGR motion can fire faster than ink
|
|
56
77
|
// needs to immediate-render). Matches transcript selection paint cadence.
|
|
57
78
|
const MOUSE_EXTEND_COALESCE_MS = 24;
|
|
@@ -96,14 +117,36 @@ function draftStateEqual(a, b) {
|
|
|
96
117
|
);
|
|
97
118
|
}
|
|
98
119
|
|
|
99
|
-
|
|
120
|
+
// Recognize a MODIFIED Enter delivered via the kitty keyboard protocol
|
|
121
|
+
// (\x1b[13;<mod>u) or modifyOtherKeys (\x1b[27;<mod>;13~). The xterm modifier
|
|
122
|
+
// param is (1 + bitmask) where the bitmask bits are shift=1, alt=2, ctrl=4. We
|
|
123
|
+
// treat Ctrl+Enter AND Shift+Enter (the two common "insert newline" chords) as a
|
|
124
|
+
// newline, so we match when the shift OR ctrl bit is set. Ctrl+J is handled
|
|
125
|
+
// separately as the protocol-independent fallback.
|
|
126
|
+
const MODIFIED_ENTER_SHIFT_OR_CTRL = 1 | 4;
|
|
127
|
+
function isModifiedEnterSequence(input) {
|
|
100
128
|
const text = String(input ?? '');
|
|
101
129
|
const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
|
|
102
130
|
if (!body) return false;
|
|
103
131
|
const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
|
|
104
|
-
if (kitty) return ((Number(kitty[1]) - 1) &
|
|
132
|
+
if (kitty) return ((Number(kitty[1]) - 1) & MODIFIED_ENTER_SHIFT_OR_CTRL) !== 0;
|
|
105
133
|
const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
|
|
106
|
-
return Boolean(modifyOtherKeys && (((Number(modifyOtherKeys[1]) - 1) &
|
|
134
|
+
return Boolean(modifyOtherKeys && (((Number(modifyOtherKeys[1]) - 1) & MODIFIED_ENTER_SHIFT_OR_CTRL) !== 0));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Recognize ANY modified Enter (any modifier bitmask, e.g. Alt+Enter \x1b[13;3u
|
|
138
|
+
// / \x1b[27;3;13~). Used to CONSUME modified-Enter sequences we don't map to a
|
|
139
|
+
// newline (Alt-only, etc.) so they aren't typed into the prompt as raw CSI text
|
|
140
|
+
// under modifyOtherKeys. Plain Enter (mod param = 1, bitmask 0) is intentionally
|
|
141
|
+
// NOT matched, so it still submits.
|
|
142
|
+
function isAnyModifiedEnterSequence(input) {
|
|
143
|
+
const text = String(input ?? '');
|
|
144
|
+
const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
|
|
145
|
+
if (!body) return false;
|
|
146
|
+
const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
|
|
147
|
+
if (kitty) return (Number(kitty[1]) - 1) !== 0;
|
|
148
|
+
const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
|
|
149
|
+
return Boolean(modifyOtherKeys && ((Number(modifyOtherKeys[1]) - 1) !== 0));
|
|
107
150
|
}
|
|
108
151
|
|
|
109
152
|
export function PromptInput({
|
|
@@ -127,10 +170,13 @@ export function PromptInput({
|
|
|
127
170
|
onRestoreQueued,
|
|
128
171
|
onHistoryNavigate,
|
|
129
172
|
onPasteText,
|
|
173
|
+
onVoiceToggle,
|
|
174
|
+
voiceIndicatorMode = 'off',
|
|
130
175
|
selectionRef,
|
|
131
176
|
valueRef,
|
|
132
177
|
boxRectRef,
|
|
133
178
|
mouseSelectionRef,
|
|
179
|
+
suppressShiftNavRef,
|
|
134
180
|
}) {
|
|
135
181
|
const [draft, setDraft] = useState(() => {
|
|
136
182
|
const value = String(initialValue || '');
|
|
@@ -148,8 +194,31 @@ export function PromptInput({
|
|
|
148
194
|
const boxRef = useRef(null);
|
|
149
195
|
const cursorEnabledRef = useRef(false); // latest enabled state, read by the anchor fn at render time
|
|
150
196
|
const contentWidthRef = useRef(80);
|
|
197
|
+
// Cells reserved on the right for the voice indicator glyph(s) (0 when
|
|
198
|
+
// hidden, 1 idle/spinner-only, 2 recording's "◉"+braille). Read by the
|
|
199
|
+
// cursor-anchor closure below (installed ONCE, so it must read live state
|
|
200
|
+
// via a ref rather than close over a render-local variable) to keep the
|
|
201
|
+
// caret/wrap math in sync with the actual reserved column, same pattern as
|
|
202
|
+
// IME_LEFT_GUARD_COLUMNS on the left edge.
|
|
203
|
+
const voiceIndicatorWidthRef = useRef(0);
|
|
151
204
|
const preferredColumnRef = useRef(null);
|
|
152
205
|
const mouseExtendCoalesceRef = useRef({ pendingNext: null, timer: null, t: 0 });
|
|
206
|
+
// Voice indicator animation tick. Runs a single ~120ms interval ONLY while
|
|
207
|
+
// voiceIndicatorMode !== 'off' (installing/idle/recording/transcribing) —
|
|
208
|
+
// torn down the instant the mode goes 'off', so no permanent timer exists
|
|
209
|
+
// when voice is disabled and no install is in flight.
|
|
210
|
+
const [voiceTick, setVoiceTick] = useState(0);
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (voiceIndicatorMode === 'off') return undefined;
|
|
213
|
+
const timer = setInterval(() => setVoiceTick((t) => (t + 1) % 1000000), VOICE_TICK_MS);
|
|
214
|
+
timer.unref?.();
|
|
215
|
+
return () => clearInterval(timer);
|
|
216
|
+
}, [voiceIndicatorMode]);
|
|
217
|
+
// Undo/redo snapshot stack. Each entry is a { value, cursor, selectionAnchor }
|
|
218
|
+
// draft snapshot. Continuous typing coalesces (see UNDO_COALESCE_MS) so a run
|
|
219
|
+
// of characters collapses into one undo step; cursor-only moves are NOT
|
|
220
|
+
// snapshotted. Reset on submit / draftOverride.
|
|
221
|
+
const undoRef = useRef({ past: [], future: [], lastPushAt: 0, lastValue: null });
|
|
153
222
|
const { value, cursor } = draft;
|
|
154
223
|
draftRef.current = draft;
|
|
155
224
|
if (selectionRef) {
|
|
@@ -181,6 +250,7 @@ export function PromptInput({
|
|
|
181
250
|
const sameDraft = draftStateEqual(draftRef.current, next);
|
|
182
251
|
if (!options.keepPreferredColumn) preferredColumnRef.current = null;
|
|
183
252
|
if (sameDraft) return;
|
|
253
|
+
if (!options.skipHistory) recordUndoSnapshot(draftRef.current, next, options);
|
|
184
254
|
draftRef.current = next;
|
|
185
255
|
setDraft(next);
|
|
186
256
|
queueMicrotask(flushImmediate);
|
|
@@ -225,10 +295,19 @@ export function PromptInput({
|
|
|
225
295
|
const d = draftRef.current;
|
|
226
296
|
const w = yogaNode?.getComputedWidth?.() ?? 0;
|
|
227
297
|
const guardColumns = w > IME_LEFT_GUARD_COLUMNS ? IME_LEFT_GUARD_COLUMNS : 0;
|
|
228
|
-
|
|
298
|
+
// Reserve the voice-indicator's own cells (+1 gap column when visible)
|
|
299
|
+
// out of the SAME width the caret/wrap math uses, mirroring the left
|
|
300
|
+
// guard above — otherwise a visible indicator would silently overlap
|
|
301
|
+
// the caret/typed text at the right edge instead of pushing content in.
|
|
302
|
+
const voiceReserve = voiceIndicatorWidthRef.current > 0 ? voiceIndicatorWidthRef.current + 1 : 0;
|
|
303
|
+
const contentWidth = Math.max(1, (w ? w - guardColumns - voiceReserve : contentWidthRef.current) || 80);
|
|
229
304
|
contentWidthRef.current = contentWidth;
|
|
230
305
|
const caret = w > 0
|
|
231
|
-
|
|
306
|
+
// PromptInput renders a trailing space cell when the cursor is at
|
|
307
|
+
// end-of-input, so a caret flush on the last column there still has a
|
|
308
|
+
// following cell — pass hasTrailingContent=true so it rolls to row N+1
|
|
309
|
+
// exactly as ink wraps the trailing space.
|
|
310
|
+
? caretPosition(d.value, d.cursor, contentWidth, d.cursor >= d.value.length ? true : undefined)
|
|
232
311
|
: { row: 0, col: stringWidth(d.value.slice(0, d.cursor)) };
|
|
233
312
|
return w > 0
|
|
234
313
|
? { ...caret, col: caret.col + guardColumns }
|
|
@@ -241,6 +320,70 @@ export function PromptInput({
|
|
|
241
320
|
commitDraft(fn(draftRef.current), options);
|
|
242
321
|
};
|
|
243
322
|
|
|
323
|
+
// --- undo/redo -----------------------------------------------------------
|
|
324
|
+
// Continuous-typing coalesce window: successive value-changing edits within
|
|
325
|
+
// this window collapse into a single undo step.
|
|
326
|
+
const UNDO_COALESCE_MS = 500;
|
|
327
|
+
const UNDO_MAX = 100;
|
|
328
|
+
|
|
329
|
+
const snapshotOf = (d) => ({ value: d.value, cursor: d.cursor, selectionAnchor: d.selectionAnchor ?? null });
|
|
330
|
+
|
|
331
|
+
const resetUndo = () => {
|
|
332
|
+
undoRef.current = { past: [], future: [], lastPushAt: 0, lastValue: null };
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// Record a snapshot of the PREVIOUS state before applying `next`. Cursor-only
|
|
336
|
+
// moves (value unchanged) are never snapshotted. Consecutive value edits
|
|
337
|
+
// within UNDO_COALESCE_MS coalesce (we keep only the first snapshot of the
|
|
338
|
+
// run, so a single undo reverts the whole burst).
|
|
339
|
+
const recordUndoSnapshot = (prev, next, options = {}) => {
|
|
340
|
+
const stack = undoRef.current;
|
|
341
|
+
if (prev.value === next.value) {
|
|
342
|
+
// Cursor/selection-only move: don't snapshot, but BREAK the coalesce run
|
|
343
|
+
// so a following edit starts a fresh undo step (typing→move→typing must
|
|
344
|
+
// not collapse into one undo).
|
|
345
|
+
stack.lastPushAt = 0;
|
|
346
|
+
stack.lastValue = next.value;
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const now = Date.now();
|
|
350
|
+
const coalesce = !options.undoBreak
|
|
351
|
+
&& stack.past.length > 0
|
|
352
|
+
&& (now - stack.lastPushAt) < UNDO_COALESCE_MS
|
|
353
|
+
&& stack.lastValue === prev.value;
|
|
354
|
+
if (!coalesce) {
|
|
355
|
+
stack.past.push(snapshotOf(prev));
|
|
356
|
+
if (stack.past.length > UNDO_MAX) stack.past.shift();
|
|
357
|
+
}
|
|
358
|
+
stack.lastPushAt = now;
|
|
359
|
+
stack.lastValue = next.value;
|
|
360
|
+
stack.future = [];
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const performUndo = () => {
|
|
364
|
+
const stack = undoRef.current;
|
|
365
|
+
if (stack.past.length === 0) return false;
|
|
366
|
+
const prev = stack.past.pop();
|
|
367
|
+
stack.future.push(snapshotOf(draftRef.current));
|
|
368
|
+
stack.lastPushAt = 0;
|
|
369
|
+
stack.lastValue = prev.value;
|
|
370
|
+
commitDraft(prev, { skipHistory: true });
|
|
371
|
+
return true;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const performRedo = () => {
|
|
375
|
+
const stack = undoRef.current;
|
|
376
|
+
if (stack.future.length === 0) return false;
|
|
377
|
+
const next = stack.future.pop();
|
|
378
|
+
stack.past.push(snapshotOf(draftRef.current));
|
|
379
|
+
if (stack.past.length > UNDO_MAX) stack.past.shift();
|
|
380
|
+
stack.lastPushAt = 0;
|
|
381
|
+
stack.lastValue = next.value;
|
|
382
|
+
commitDraft(next, { skipHistory: true });
|
|
383
|
+
return true;
|
|
384
|
+
};
|
|
385
|
+
// -------------------------------------------------------------------------
|
|
386
|
+
|
|
244
387
|
const cancelMouseExtendCoalesce = () => {
|
|
245
388
|
const state = mouseExtendCoalesceRef.current;
|
|
246
389
|
if (state.timer) {
|
|
@@ -309,6 +452,28 @@ export function PromptInput({
|
|
|
309
452
|
const anchor = Number.isFinite(d.selectionAnchor) ? d.selectionAnchor : d.cursor;
|
|
310
453
|
queueMouseExtendCommit({ ...d, cursor: off, selectionAnchor: anchor }, immediate);
|
|
311
454
|
},
|
|
455
|
+
hasSelection: () => selectionRange(draftRef.current) != null,
|
|
456
|
+
// Double-click word select: pick the word/punctuation run under the
|
|
457
|
+
// clicked offset and set selectionAnchor/cursor to its bounds so it
|
|
458
|
+
// paints via the normal selection highlight. Triple-click line select
|
|
459
|
+
// uses lineStart/lineEnd instead (see selectLineAt).
|
|
460
|
+
selectWordAt: (offset) => {
|
|
461
|
+
cancelMouseExtendCoalesce();
|
|
462
|
+
const value = draftRef.current.value;
|
|
463
|
+
const off = Math.max(0, Math.min(value.length, Math.floor(Number(offset) || 0)));
|
|
464
|
+
const { start, end } = wordRangeAt(value, off);
|
|
465
|
+
commitDraft({ ...draftRef.current, cursor: end, selectionAnchor: start });
|
|
466
|
+
mouseExtendCoalesceRef.current.t = Date.now();
|
|
467
|
+
},
|
|
468
|
+
selectLineAt: (offset) => {
|
|
469
|
+
cancelMouseExtendCoalesce();
|
|
470
|
+
const value = draftRef.current.value;
|
|
471
|
+
const off = Math.max(0, Math.min(value.length, Math.floor(Number(offset) || 0)));
|
|
472
|
+
const start = lineStart(value, off);
|
|
473
|
+
const end = lineEnd(value, off);
|
|
474
|
+
commitDraft({ ...draftRef.current, cursor: end, selectionAnchor: start });
|
|
475
|
+
mouseExtendCoalesceRef.current.t = Date.now();
|
|
476
|
+
},
|
|
312
477
|
clear: () => {
|
|
313
478
|
cancelMouseExtendCoalesce();
|
|
314
479
|
if (selectionRange(draftRef.current)) commitDraft(clearSelection(draftRef.current));
|
|
@@ -382,7 +547,8 @@ export function PromptInput({
|
|
|
382
547
|
|
|
383
548
|
useEffect(() => {
|
|
384
549
|
if (!draftOverride || typeof draftOverride.value !== 'string') return;
|
|
385
|
-
commitDraft({ value: draftOverride.value, cursor: draftOverride.value.length, selectionAnchor: null });
|
|
550
|
+
commitDraft({ value: draftOverride.value, cursor: draftOverride.value.length, selectionAnchor: null }, { skipHistory: true });
|
|
551
|
+
resetUndo();
|
|
386
552
|
}, [draftOverride?.id]);
|
|
387
553
|
|
|
388
554
|
useEffect(() => () => {
|
|
@@ -396,7 +562,8 @@ export function PromptInput({
|
|
|
396
562
|
commitDraft(next);
|
|
397
563
|
return;
|
|
398
564
|
}
|
|
399
|
-
commitDraft({ value: '', cursor: 0, selectionAnchor: null });
|
|
565
|
+
commitDraft({ value: '', cursor: 0, selectionAnchor: null }, { skipHistory: true });
|
|
566
|
+
resetUndo();
|
|
400
567
|
};
|
|
401
568
|
|
|
402
569
|
const submitEnterChunk = (prefix = '') => {
|
|
@@ -427,6 +594,21 @@ export function PromptInput({
|
|
|
427
594
|
const rawInput = String(input ?? '');
|
|
428
595
|
const inputKey = rawInput.toLowerCase();
|
|
429
596
|
|
|
597
|
+
// App owns Shift+Arrow when a transcript/status ink-grid selection is live.
|
|
598
|
+
// Because the parent (App) useInput handler fires AFTER this child handler
|
|
599
|
+
// for the same event, a flag SET in App's handler is always one event stale.
|
|
600
|
+
// Instead call a synchronous predicate derived from dragRef at event time.
|
|
601
|
+
const gridSelectionActive = typeof suppressShiftNavRef === 'function'
|
|
602
|
+
? suppressShiftNavRef()
|
|
603
|
+
: (typeof suppressShiftNavRef?.current === 'function'
|
|
604
|
+
? suppressShiftNavRef.current()
|
|
605
|
+
: Boolean(suppressShiftNavRef?.current));
|
|
606
|
+
if (gridSelectionActive) {
|
|
607
|
+
const isShiftArrow = key.shift
|
|
608
|
+
&& (key.leftArrow || key.rightArrow || key.upArrow || key.downArrow || key.home || key.end);
|
|
609
|
+
if (isShiftArrow) return;
|
|
610
|
+
}
|
|
611
|
+
|
|
430
612
|
// Drop SGR mouse-tracking sequences (wheel/click). When app mouse tracking
|
|
431
613
|
// is explicitly enabled, App parses these off raw stdin itself;
|
|
432
614
|
// ink still forwards the bytes here as "input", which would otherwise type
|
|
@@ -436,6 +618,17 @@ export function PromptInput({
|
|
|
436
618
|
return;
|
|
437
619
|
}
|
|
438
620
|
|
|
621
|
+
// Safety net: drop CSI-private replies/fragments like \x1b[?<n>u / \x1b[?...c
|
|
622
|
+
// (escape may be stripped → `[?7u` / `[?1;0c`). We no longer query the
|
|
623
|
+
// terminal (enables are written unconditionally at raw-mode-on), so these
|
|
624
|
+
// should not normally appear — but a terminal that volunteers such a report
|
|
625
|
+
// must never type it into the prompt. The required `?` after `[` means this
|
|
626
|
+
// never matches a real kitty KEY event (those are \x1b[<codepoint>;<mods>u,
|
|
627
|
+
// no `?`); the optional final byte also discards any partial fragment.
|
|
628
|
+
if (/^(?:\x1b)?\[\?[\d;]*[uc]?$/.test(rawInput)) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
439
632
|
const rawUpArrow = rawInput === '\x1b[A' || rawInput === '\x1bOA' || rawInput === '[A' || rawInput === 'OA';
|
|
440
633
|
const rawDownArrow = rawInput === '\x1b[B' || rawInput === '\x1bOB' || rawInput === '[B' || rawInput === 'OB';
|
|
441
634
|
// Shift+Arrow modifier sequences (xterm `\x1b[1;2<dir>`, rxvt `\x1b[<dir>`
|
|
@@ -448,13 +641,52 @@ export function PromptInput({
|
|
|
448
641
|
const rawShiftDown = rawInput === '\x1b[1;2B' || rawInput === '\x1b[b' || rawInput === '[1;2B';
|
|
449
642
|
const rawShiftRight = rawInput === '\x1b[1;2C' || rawInput === '\x1b[c' || rawInput === '[1;2C';
|
|
450
643
|
const rawShiftLeft = rawInput === '\x1b[1;2D' || rawInput === '\x1b[d' || rawInput === '[1;2D';
|
|
451
|
-
|
|
644
|
+
// Ctrl+Shift+Arrow modifier sequences: xterm mod=6 (1 + shift(1) + ctrl(4))
|
|
645
|
+
// arrives as `\x1b[1;6<dir>`; kitty keyboard protocol reports the same chord
|
|
646
|
+
// as `\x1b[<code>;6<dir>` (also mod=6). Ink decodes neither the `;6` for
|
|
647
|
+
// arrows, so the bytes arrive raw. Fold into a ctrlShiftHeld signal used to
|
|
648
|
+
// drive whole-word selection-extend below. `\x1b[1;6<dir>` covers both the
|
|
649
|
+
// classic xterm form and kitty's default (which emits the legacy arrow form
|
|
650
|
+
// with the CSI-u modifier param for arrow keys).
|
|
651
|
+
const rawCtrlShiftUp = rawInput === '\x1b[1;6A' || rawInput === '[1;6A';
|
|
652
|
+
const rawCtrlShiftDown = rawInput === '\x1b[1;6B' || rawInput === '[1;6B';
|
|
653
|
+
const rawCtrlShiftRight = rawInput === '\x1b[1;6C' || rawInput === '[1;6C';
|
|
654
|
+
const rawCtrlShiftLeft = rawInput === '\x1b[1;6D' || rawInput === '[1;6D';
|
|
655
|
+
const ctrlShiftHeld =
|
|
656
|
+
rawCtrlShiftUp || rawCtrlShiftDown || rawCtrlShiftLeft || rawCtrlShiftRight
|
|
657
|
+
|| (key.shift && key.ctrl && (key.leftArrow || key.rightArrow || key.upArrow || key.downArrow));
|
|
658
|
+
const shiftHeld = key.shift || rawShiftUp || rawShiftDown || rawShiftLeft || rawShiftRight
|
|
659
|
+
|| rawCtrlShiftUp || rawCtrlShiftDown || rawCtrlShiftLeft || rawCtrlShiftRight;
|
|
452
660
|
const lineBreakIndex = rawInput.search(/[\r\n]/);
|
|
453
661
|
const rawEnter = rawInput === '\r' || rawInput === '\n' || rawInput === '\r\n';
|
|
454
662
|
const trailingEnterPrefix = singleTrailingLineBreakPrefix(rawInput);
|
|
455
|
-
const rawCtrlEnter =
|
|
663
|
+
const rawCtrlEnter = isModifiedEnterSequence(rawInput);
|
|
456
664
|
const modifiedLineBreak = key.shift || key.meta || key.ctrl || rawCtrlEnter;
|
|
457
665
|
|
|
666
|
+
// Ctrl+J is the protocol-INDEPENDENT newline that works on every terminal.
|
|
667
|
+
// • Legacy / modifyOtherKeys terminals: Ctrl+J is a lone '\n' (0x0A). A real
|
|
668
|
+
// Enter is CR, which ink marks key.return (name 'return'); a lone '\n'
|
|
669
|
+
// arrives as name 'enter' with key.return false. A multi-char paste that
|
|
670
|
+
// contains '\n' is length > 1 (handled by the paste paths below).
|
|
671
|
+
// • Kitty protocol active: Ctrl+J arrives as \x1b[106;5u, which ink decodes
|
|
672
|
+
// to input 'j' with key.ctrl set.
|
|
673
|
+
// Either way → insert a newline. This MUST run before the trailing-newline/
|
|
674
|
+
// submit paths, since singleTrailingLineBreakPrefix('\n') returns '' (not
|
|
675
|
+
// null) and would otherwise route a bare Ctrl+J to submit.
|
|
676
|
+
if ((rawInput === '\n' && !key.return) || (key.ctrl && inputKey === 'j')) {
|
|
677
|
+
updateDraft((d) => replaceSelection(d, '\n'));
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// A modified Enter that is NOT a newline chord (e.g. Alt+Enter \x1b[13;3u or
|
|
682
|
+
// \x1b[27;3;13~). isModifiedEnterSequence already handled shift/ctrl above
|
|
683
|
+
// (→ newline); here we CONSUME any other modified Enter so its raw CSI bytes
|
|
684
|
+
// never type into the prompt under modifyOtherKeys. Plain Enter (mod=1) is
|
|
685
|
+
// not matched and still submits below.
|
|
686
|
+
if (!rawCtrlEnter && isAnyModifiedEnterSequence(rawInput)) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
458
690
|
const pasteFallback = lineBreakIndex !== -1 && trailingEnterPrefix === null && !rawEnter && (rawInput.length > 1 || !key.return);
|
|
459
691
|
if (pasteFallback) {
|
|
460
692
|
handleExternalPaste(rawInput, { source: 'paste-fallback' });
|
|
@@ -508,25 +740,58 @@ export function PromptInput({
|
|
|
508
740
|
return;
|
|
509
741
|
}
|
|
510
742
|
|
|
511
|
-
|
|
743
|
+
// Ctrl+Shift+Left/Right → extend selection whole-word. Kept before the plain
|
|
744
|
+
// shift-arrow branches so the ctrl+shift chord never falls through to a
|
|
745
|
+
// char-wise extend. (Up/Down ctrl+shift extend to line-relative vertical
|
|
746
|
+
// move with extend — same as shift alone; handled in the vertical branch.)
|
|
747
|
+
if (ctrlShiftHeld && (rawCtrlShiftLeft || (key.ctrl && key.shift && key.leftArrow))) {
|
|
748
|
+
if (!commandPaletteActive) {
|
|
749
|
+
updateDraft((d) => moveCursor(d, previousWordOffset(d.value, d.cursor), { extend: true }));
|
|
750
|
+
}
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (ctrlShiftHeld && (rawCtrlShiftRight || (key.ctrl && key.shift && key.rightArrow))) {
|
|
754
|
+
if (!commandPaletteActive) {
|
|
755
|
+
updateDraft((d) => moveCursor(d, nextWordOffset(d.value, d.cursor), { extend: true }));
|
|
756
|
+
}
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (key.upArrow || rawUpArrow || rawShiftUp || rawCtrlShiftUp) {
|
|
512
761
|
if (commandPaletteActive) {
|
|
513
762
|
onCommandPaletteNavigate?.(-1);
|
|
514
763
|
} else {
|
|
515
|
-
|
|
516
|
-
if
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
764
|
+
// A Shift-held Up is a SELECTION gesture, never history navigation:
|
|
765
|
+
// extend the selection up one visual line, and if already on the first
|
|
766
|
+
// line extend all the way to document start (offset 0). History
|
|
767
|
+
// navigation (restoreQueued / applyHistoryNavigation) MUST NOT fire.
|
|
768
|
+
if (shiftHeld) {
|
|
769
|
+
if (!moveDraftVertically(-1, { extend: true })) {
|
|
770
|
+
updateDraft((d) => moveCursor(d, 0, { extend: true }));
|
|
771
|
+
}
|
|
772
|
+
} else {
|
|
773
|
+
const hasDraftText = String(draftRef.current.value || '').trim().length > 0;
|
|
774
|
+
if (!hasDraftText) {
|
|
775
|
+
if (!restoreQueuedToDraft()) applyHistoryNavigation('up', { emptyDraft: true });
|
|
776
|
+
} else if (!moveDraftVertically(-1, { extend: false })) {
|
|
777
|
+
applyHistoryNavigation('up', { emptyDraft: false });
|
|
778
|
+
}
|
|
520
779
|
}
|
|
521
780
|
}
|
|
522
781
|
return;
|
|
523
782
|
}
|
|
524
783
|
|
|
525
|
-
if (key.downArrow || rawDownArrow || rawShiftDown) {
|
|
784
|
+
if (key.downArrow || rawDownArrow || rawShiftDown || rawCtrlShiftDown) {
|
|
526
785
|
if (commandPaletteActive) {
|
|
527
786
|
onCommandPaletteNavigate?.(1);
|
|
528
787
|
} else {
|
|
529
|
-
|
|
788
|
+
// Shift-held Down: extend selection down one line, or to document end
|
|
789
|
+
// (value.length) when already on the last line. Never history nav.
|
|
790
|
+
if (shiftHeld) {
|
|
791
|
+
if (!moveDraftVertically(1, { extend: true })) {
|
|
792
|
+
updateDraft((d) => moveCursor(d, d.value.length, { extend: true }));
|
|
793
|
+
}
|
|
794
|
+
} else if (!moveDraftVertically(1, { extend: false })) {
|
|
530
795
|
applyHistoryNavigation('down', { emptyDraft: String(draftRef.current.value || '').trim().length === 0 });
|
|
531
796
|
}
|
|
532
797
|
}
|
|
@@ -583,8 +848,8 @@ export function PromptInput({
|
|
|
583
848
|
commitDraft({ value: '', cursor: 0, selectionAnchor: null });
|
|
584
849
|
return;
|
|
585
850
|
}
|
|
586
|
-
// Active turn takes precedence over queue restore
|
|
587
|
-
//
|
|
851
|
+
// Active turn takes precedence over queue restore: Esc during a
|
|
852
|
+
// running turn interrupts the
|
|
588
853
|
// turn and leaves queued steering prompts intact to run afterward. Queued
|
|
589
854
|
// messages only pop back into the draft when the turn is idle.
|
|
590
855
|
if (interruptActive) {
|
|
@@ -644,6 +909,27 @@ export function PromptInput({
|
|
|
644
909
|
|
|
645
910
|
const editingKey = String(input || '').toLowerCase();
|
|
646
911
|
|
|
912
|
+
// Undo / redo. Covered encodings:
|
|
913
|
+
// • kitty protocol: ctrl+z → input 'z' + key.ctrl; ctrl+y → 'y' + key.ctrl;
|
|
914
|
+
// ctrl+shift+z → 'z' + key.ctrl + key.shift (redo).
|
|
915
|
+
// • legacy control bytes: Ctrl+Z is \x1a (SUB, 0x1A), Ctrl+Y is \x19 (EM,
|
|
916
|
+
// 0x19). ink may deliver these as raw input without key.ctrl on some
|
|
917
|
+
// terminals, so match the byte directly too.
|
|
918
|
+
const isCtrlZ = (key.ctrl && editingKey === 'z') || rawInput === '\x1a';
|
|
919
|
+
const isCtrlY = (key.ctrl && editingKey === 'y') || rawInput === '\x19';
|
|
920
|
+
if (isCtrlZ && (key.shift || shiftHeld)) {
|
|
921
|
+
performRedo();
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (isCtrlZ) {
|
|
925
|
+
performUndo();
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (isCtrlY) {
|
|
929
|
+
performRedo();
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
647
933
|
// ctrl+a selects all like a normal text box; ctrl+e keeps readline line-end.
|
|
648
934
|
if (key.ctrl && editingKey === 'a') {
|
|
649
935
|
updateDraft((d) => (d.value ? { ...d, cursor: d.value.length, selectionAnchor: 0 } : clearSelection(d)));
|
|
@@ -719,6 +1005,18 @@ export function PromptInput({
|
|
|
719
1005
|
return;
|
|
720
1006
|
}
|
|
721
1007
|
|
|
1008
|
+
// Ctrl+Space voice-record toggle. Two encodings observed across
|
|
1009
|
+
// terminals: a lone NUL byte (legacy Ctrl+Space, many terminals send
|
|
1010
|
+
// 0x00 directly) or the kitty CSI-u form `\x1b[32;5u` (codepoint 32
|
|
1011
|
+
// = space, modifier 5 = 1+4 = ctrl). Consumed here — never typed into
|
|
1012
|
+
// the prompt — and forwarded to the App-level recorder state machine
|
|
1013
|
+
// via onVoiceToggle; a no-op when the prop isn't wired (e.g. embedded
|
|
1014
|
+
// pickers) so this never throws.
|
|
1015
|
+
if (rawInput === '\x00' || /^(?:\x1b)?\[32;5u$/.test(rawInput)) {
|
|
1016
|
+
onVoiceToggle?.();
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
722
1020
|
// Printable input (ignore other control keys). Strip any embedded SGR mouse
|
|
723
1021
|
// sequences as a belt-and-suspenders guard (the early return above catches
|
|
724
1022
|
// whole-sequence inputs; this removes partials that rode in with real text).
|
|
@@ -739,6 +1037,12 @@ export function PromptInput({
|
|
|
739
1037
|
// resetAfterCommit BEFORE React layout effects — that lag made the 2nd+ char
|
|
740
1038
|
// appear to land behind the caret (the observed scramble). Computing inside
|
|
741
1039
|
// the fork, from the real layout + current refs, fixes that by construction.
|
|
1040
|
+
// Park the hardware cursor only when the prompt is a real, active edit target.
|
|
1041
|
+
// The anchor stays enabled during an active turn too: suppressing it while
|
|
1042
|
+
// the draft was empty (an earlier guard against a one-frame row-off paint
|
|
1043
|
+
// when the transcript height changed mid-turn) made the cursor vanish for
|
|
1044
|
+
// the whole turn after Enter, which reads as "the caret disappeared". A
|
|
1045
|
+
// momentary one-frame drift is the lesser evil versus a missing caret.
|
|
742
1046
|
cursorEnabledRef.current = !disabled && isRawModeSupported;
|
|
743
1047
|
installCursorAnchor();
|
|
744
1048
|
|
|
@@ -758,16 +1062,45 @@ export function PromptInput({
|
|
|
758
1062
|
const displayValue = mask ? value.replace(/[^\n]/g, '*') : value;
|
|
759
1063
|
const renderedValue = renderSelectedText(displayValue, selectionRange(draft), cursor === value.length);
|
|
760
1064
|
const hintMeta = hintStyle(hintTone);
|
|
1065
|
+
// Monotone voice indicator glyph text — zero-width (rendered as null) when
|
|
1066
|
+
// mode is 'off'. Braille frame drives installing/recording/transcribing;
|
|
1067
|
+
// the idle pulse advances one frame every VOICE_IDLE_FRAME_TICKS ticks of
|
|
1068
|
+
// the SAME interval (slower "breathing" cadence off one shared timer).
|
|
1069
|
+
const voiceBrailleFrame = VOICE_BRAILLE_FRAMES[voiceTick % VOICE_BRAILLE_FRAMES.length];
|
|
1070
|
+
const voiceIdleFrame = VOICE_IDLE_FRAMES[Math.floor(voiceTick / VOICE_IDLE_FRAME_TICKS) % VOICE_IDLE_FRAMES.length];
|
|
1071
|
+
const voiceIndicatorText = voiceIndicatorMode === 'installing' || voiceIndicatorMode === 'transcribing'
|
|
1072
|
+
? voiceBrailleFrame
|
|
1073
|
+
: voiceIndicatorMode === 'recording'
|
|
1074
|
+
? `◉${voiceBrailleFrame}`
|
|
1075
|
+
: voiceIndicatorMode === 'idle'
|
|
1076
|
+
? voiceIdleFrame
|
|
1077
|
+
: '';
|
|
1078
|
+
// stringWidth is unnecessary here — every glyph above is a single
|
|
1079
|
+
// 1-column codepoint (◎◍◉ and the braille frames), and 'recording'
|
|
1080
|
+
// concatenates exactly two 1-cell glyphs, so plain .length gives the
|
|
1081
|
+
// correct cell count without pulling in emoji-width special-casing.
|
|
1082
|
+
voiceIndicatorWidthRef.current = voiceIndicatorText.length;
|
|
761
1083
|
|
|
762
1084
|
return (
|
|
763
|
-
<Box ref={boxRef} flexDirection="row" flexGrow={1} flexShrink={1} backgroundColor={
|
|
764
|
-
<Box width={IME_LEFT_GUARD_COLUMNS} flexShrink={0} backgroundColor={
|
|
1085
|
+
<Box ref={boxRef} flexDirection="row" width="100%" flexGrow={1} flexShrink={1} backgroundColor={surfaceBackground()}>
|
|
1086
|
+
<Box width={IME_LEFT_GUARD_COLUMNS} flexShrink={0} backgroundColor={surfaceBackground()} />
|
|
765
1087
|
<Text color={theme.text} wrap="hard">{renderedValue}</Text>
|
|
766
1088
|
{!value && hint ? (
|
|
767
1089
|
<Box marginLeft={-1}>
|
|
768
1090
|
<Text color={hintMeta.textColor}>{hint}</Text>
|
|
769
1091
|
</Box>
|
|
770
1092
|
) : null}
|
|
1093
|
+
{voiceIndicatorText ? (
|
|
1094
|
+
<>
|
|
1095
|
+
{/* flexGrow spacer pins the indicator to the RIGHT edge of the row
|
|
1096
|
+
even when the typed text is short; without it the glyph would
|
|
1097
|
+
ride directly after the caret instead of the box edge. */}
|
|
1098
|
+
<Box flexGrow={1} backgroundColor={surfaceBackground()} />
|
|
1099
|
+
<Box flexShrink={0} width={voiceIndicatorText.length + 1} justifyContent="flex-end" backgroundColor={surfaceBackground()}>
|
|
1100
|
+
<Text color={theme.subtle}>{voiceIndicatorText}</Text>
|
|
1101
|
+
</Box>
|
|
1102
|
+
</>
|
|
1103
|
+
) : null}
|
|
771
1104
|
</Box>
|
|
772
1105
|
);
|
|
773
1106
|
}
|
|
@@ -22,7 +22,7 @@ export function QueuedCommands({ queued, columns }) {
|
|
|
22
22
|
// Windows auto-wrap/scroll that drifts the alt-screen frame (see UserMessage).
|
|
23
23
|
const bandColumns = Math.max(1, columns - 1);
|
|
24
24
|
return (
|
|
25
|
-
<Box
|
|
25
|
+
<Box flexDirection="column">
|
|
26
26
|
{queued.map((item) => {
|
|
27
27
|
// Truncate to 1 line so the row reservation (queued.length in App.jsx)
|
|
28
28
|
// stays accurate — wrapped text would push the input box off-screen.
|
|
@@ -65,6 +65,8 @@ export function SlashCommandPalette({ commands, selectedIndex = 0, title = 'Comm
|
|
|
65
65
|
const descriptionWidth = Math.max(0, columns - labelWidth - 12);
|
|
66
66
|
// Standard panel rhythm: title row, blank, description/hint row, blank, content.
|
|
67
67
|
const description = truncateText(SLASH_DESCRIPTION, Math.max(0, columns - 4));
|
|
68
|
+
const titleWidth = stringWidth(String(title || ''));
|
|
69
|
+
const help = truncateText(SLASH_HELP, Math.max(0, columns - titleWidth - 7));
|
|
68
70
|
|
|
69
71
|
return (
|
|
70
72
|
<Box flexDirection="column" flexShrink={0} width="100%">
|
|
@@ -76,8 +78,8 @@ export function SlashCommandPalette({ commands, selectedIndex = 0, title = 'Comm
|
|
|
76
78
|
width="100%"
|
|
77
79
|
>
|
|
78
80
|
<Box flexDirection="row" justifyContent="space-between">
|
|
79
|
-
<Text color={theme.panelTitle}>{title}</Text>
|
|
80
|
-
<Text color={theme.subtle}>{
|
|
81
|
+
<Text color={theme.panelTitle} wrap="truncate">{title}</Text>
|
|
82
|
+
<Text color={theme.subtle} wrap="truncate">{help}</Text>
|
|
81
83
|
</Box>
|
|
82
84
|
<Text> </Text>
|
|
83
85
|
<Text color={theme.subtle}>{description || ' '}</Text>
|
|
@@ -103,14 +105,15 @@ export function SlashCommandPalette({ commands, selectedIndex = 0, title = 'Comm
|
|
|
103
105
|
const CommandRow = React.memo(function CommandRow({ command, isSelected, labelWidth, descriptionWidth, query }) {
|
|
104
106
|
const label = truncateText(commandDisplayLabel(command, query), labelWidth);
|
|
105
107
|
const description = truncateText(command.description, descriptionWidth);
|
|
108
|
+
const rowText = isSelected ? theme.selectionText : theme.text;
|
|
106
109
|
|
|
107
110
|
return (
|
|
108
|
-
<Box flexDirection="row" width="100%" backgroundColor={isSelected ? theme.
|
|
109
|
-
<Text color={
|
|
111
|
+
<Box flexDirection="row" width="100%" backgroundColor={isSelected ? theme.selectionBackground : undefined}>
|
|
112
|
+
<Text color={rowText}>
|
|
110
113
|
{padCells(label, labelWidth)}
|
|
111
114
|
</Text>
|
|
112
115
|
{description ? (
|
|
113
|
-
<Text color={
|
|
116
|
+
<Text color={rowText}>
|
|
114
117
|
{' '}
|
|
115
118
|
{description}
|
|
116
119
|
</Text>
|
|
@@ -58,8 +58,6 @@ const MODE_VERBS = {
|
|
|
58
58
|
responding: ['Responding', 'Composing', 'Writing', 'Wrapping up'],
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
const ERROR_RED = { r: 171, g: 43, b: 63 };
|
|
62
|
-
|
|
63
61
|
function interpolateColor(a, b, t) {
|
|
64
62
|
return {
|
|
65
63
|
r: Math.round(a.r + (b.r - a.r) * t),
|
|
@@ -117,6 +115,7 @@ function spinnerRgb() {
|
|
|
117
115
|
SPINNER_GLYPH_RGB: parseRgb(theme.spinnerGlyph) ?? { r: 240, g: 240, b: 240 },
|
|
118
116
|
THINKING_INACTIVE: parseRgb(theme.thinkingBase) ?? parseRgb(theme.thinkingAccent) ?? { r: 153, g: 153, b: 153 },
|
|
119
117
|
THINKING_SHIMMER: parseRgb(theme.thinkingGlow) ?? { r: 255, g: 205, b: 175 },
|
|
118
|
+
STALL_RGB: parseRgb(theme.error) ?? { r: 171, g: 43, b: 63 },
|
|
120
119
|
};
|
|
121
120
|
}
|
|
122
121
|
|
|
@@ -172,7 +171,7 @@ function tokenModeGlyph(mode) {
|
|
|
172
171
|
|
|
173
172
|
export function Spinner({ verb = 'Working', startedAt, outputTokens = 0, tokens = 0, thinking = false, thinkingActiveSince = 0, mode = 'responding', columns = 80, marginTop = 1 }) {
|
|
174
173
|
useAnimation({ interval: FRAME_MS });
|
|
175
|
-
const { TEXT_RGB, SHIMMER_RGB, SPINNER_GLYPH_RGB, THINKING_INACTIVE, THINKING_SHIMMER } = spinnerRgb();
|
|
174
|
+
const { TEXT_RGB, SHIMMER_RGB, SPINNER_GLYPH_RGB, THINKING_INACTIVE, THINKING_SHIMMER, STALL_RGB } = spinnerRgb();
|
|
176
175
|
const now = Date.now();
|
|
177
176
|
const elapsedMs = startedAt ? Math.max(0, now - startedAt) : 0;
|
|
178
177
|
const frame = Math.floor(elapsedMs / FRAME_MS);
|
|
@@ -222,17 +221,18 @@ export function Spinner({ verb = 'Working', startedAt, outputTokens = 0, tokens
|
|
|
222
221
|
const glyphColor = stalledIntensity > 0
|
|
223
222
|
? toRgbString(interpolateColor(
|
|
224
223
|
SPINNER_GLYPH_RGB,
|
|
225
|
-
|
|
224
|
+
STALL_RGB,
|
|
226
225
|
stalledIntensity
|
|
227
226
|
))
|
|
228
227
|
: theme.spinnerGlyph;
|
|
229
228
|
|
|
230
229
|
// --- Verb shimmer (traveling highlight) ---
|
|
231
|
-
if (
|
|
232
|
-
displayVerbModeRef.current = mode;
|
|
230
|
+
if (!displayVerbRef.current) {
|
|
233
231
|
displayVerbRef.current = stableModeVerb(mode, verb);
|
|
234
232
|
nextVerbCheckRef.current = nextVerbCheckAt(now);
|
|
235
|
-
}
|
|
233
|
+
}
|
|
234
|
+
displayVerbModeRef.current = mode;
|
|
235
|
+
if (now >= nextVerbCheckRef.current) {
|
|
236
236
|
displayVerbRef.current = chooseNextVerb(mode, verb, displayVerbRef.current);
|
|
237
237
|
nextVerbCheckRef.current = nextVerbCheckAt(now);
|
|
238
238
|
}
|