mixdog 0.8.0 → 0.9.0
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/README.md +47 -23
- package/package.json +33 -27
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +344 -0
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1223 -95
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2325 -450
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +118 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +635 -107
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +145 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -98
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +367 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +617 -368
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8915 -2418
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +746 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +3 -16
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* components/PromptInput.jsx — the prompt input line.
|
|
3
|
-
*
|
|
4
|
-
* A self-contained editor built on ink's useInput. The text renders as plain
|
|
5
|
-
* text and a thin NATIVE hardware cursor sits at the insertion point (also where
|
|
6
|
-
* the terminal echoes typed/IME characters).
|
|
7
|
-
*
|
|
8
|
-
* CURSOR — via a small ink fork (vendor/ink, mirrored into node_modules): we
|
|
9
|
-
* tag the text-box node with `internal_cursorAnchor = { col, row }` (the caret's
|
|
10
|
-
* position WITHIN the box). Forked ink, during renderNodeToOutput, parks the
|
|
11
|
-
* hardware cursor at that node's REAL laid-out absolute cell + (col,row) — every
|
|
12
|
-
* frame, from the actual yoga layout. This replaces ink's stock useCursor, whose
|
|
13
|
-
* externally-supplied absolute coordinate drifted/vanished whenever the layout
|
|
14
|
-
* above the input changed (spinner/thinking growth, Enter, fullscreen relayout)
|
|
15
|
-
* because it was computed a beat before the final layout. The fork computes it
|
|
16
|
-
* at the exact moment of drawing, so it can never be stale.
|
|
17
|
-
*/
|
|
1
|
+
/*
|
|
2
|
+
* components/PromptInput.jsx — the prompt input line.
|
|
3
|
+
*
|
|
4
|
+
* A self-contained editor built on ink's useInput. The text renders as plain
|
|
5
|
+
* text and a thin NATIVE hardware cursor sits at the insertion point (also where
|
|
6
|
+
* the terminal echoes typed/IME characters).
|
|
7
|
+
*
|
|
8
|
+
* CURSOR — via a small ink fork (vendor/ink, mirrored into node_modules): we
|
|
9
|
+
* tag the text-box node with `internal_cursorAnchor = { col, row }` (the caret's
|
|
10
|
+
* position WITHIN the box). Forked ink, during renderNodeToOutput, parks the
|
|
11
|
+
* hardware cursor at that node's REAL laid-out absolute cell + (col,row) — every
|
|
12
|
+
* frame, from the actual yoga layout. This replaces ink's stock useCursor, whose
|
|
13
|
+
* externally-supplied absolute coordinate drifted/vanished whenever the layout
|
|
14
|
+
* above the input changed (spinner/thinking growth, Enter, fullscreen relayout)
|
|
15
|
+
* because it was computed a beat before the final layout. The fork computes it
|
|
16
|
+
* at the exact moment of drawing, so it can never be stale.
|
|
17
|
+
*/
|
|
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';
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
moveCursor,
|
|
33
33
|
nextOffset,
|
|
34
34
|
nextWordOffset,
|
|
35
|
+
offsetAtCell,
|
|
35
36
|
previousOffset,
|
|
36
37
|
previousWordOffset,
|
|
37
38
|
replaceSelection,
|
|
@@ -45,11 +46,17 @@ function hintStyle(tone) {
|
|
|
45
46
|
if (tone === 'plain') return { textColor: theme.subtle };
|
|
46
47
|
return { textColor: theme.inactive };
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
+
|
|
50
|
+
// Windows Terminal IME composition can clip a glyph that starts exactly at the
|
|
51
|
+
// left edge of the editable text node. The rounded prompt box already adds a
|
|
52
|
+
// paddingX of 1, so the typing start can sit directly against that padding
|
|
53
|
+
// without an extra guard column.
|
|
54
|
+
const IME_LEFT_GUARD_COLUMNS = 0;
|
|
55
|
+
|
|
49
56
|
function insertText(draft, input) {
|
|
50
|
-
if (!input) return draft;
|
|
57
|
+
if (!input) return draft;
|
|
51
58
|
return replaceSelection(draft, input);
|
|
52
|
-
}
|
|
59
|
+
}
|
|
53
60
|
|
|
54
61
|
function renderSelectedText(displayValue, range, trailingSpace = false) {
|
|
55
62
|
if (!range) return trailingSpace ? `${displayValue} ` : displayValue;
|
|
@@ -59,14 +66,14 @@ function renderSelectedText(displayValue, range, trailingSpace = false) {
|
|
|
59
66
|
<>
|
|
60
67
|
{start > 0 ? displayValue.slice(0, start) : null}
|
|
61
68
|
{end > start ? (
|
|
62
|
-
<Text color={theme.
|
|
69
|
+
<Text color={theme.selectionText} backgroundColor={theme.selectionBackground}>{displayValue.slice(start, end)}</Text>
|
|
63
70
|
) : null}
|
|
64
71
|
{displayValue.slice(end)}
|
|
65
72
|
{trailingSpace ? ' ' : ''}
|
|
66
73
|
</>
|
|
67
74
|
);
|
|
68
75
|
}
|
|
69
|
-
|
|
76
|
+
|
|
70
77
|
function normalizePastedText(text) {
|
|
71
78
|
return String(text ?? '').replace(/\r\n?/g, '\n');
|
|
72
79
|
}
|
|
@@ -78,10 +85,28 @@ function singleTrailingLineBreakPrefix(text) {
|
|
|
78
85
|
return prefix.includes('\n') ? null : prefix;
|
|
79
86
|
}
|
|
80
87
|
|
|
88
|
+
function draftStateEqual(a, b) {
|
|
89
|
+
return (
|
|
90
|
+
a.value === b.value
|
|
91
|
+
&& a.cursor === b.cursor
|
|
92
|
+
&& a.selectionAnchor === b.selectionAnchor
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isCtrlEnterSequence(input) {
|
|
97
|
+
const text = String(input ?? '');
|
|
98
|
+
const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
|
|
99
|
+
if (!body) return false;
|
|
100
|
+
const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
|
|
101
|
+
if (kitty) return ((Number(kitty[1]) - 1) & 4) !== 0;
|
|
102
|
+
const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
|
|
103
|
+
return Boolean(modifyOtherKeys && (((Number(modifyOtherKeys[1]) - 1) & 4) !== 0));
|
|
104
|
+
}
|
|
105
|
+
|
|
81
106
|
export function PromptInput({
|
|
82
|
-
onSubmit,
|
|
83
|
-
disabled = false,
|
|
84
|
-
onDraftChange,
|
|
107
|
+
onSubmit,
|
|
108
|
+
disabled = false,
|
|
109
|
+
onDraftChange,
|
|
85
110
|
interruptActive = false,
|
|
86
111
|
onInterrupt,
|
|
87
112
|
commandPaletteActive = false,
|
|
@@ -91,14 +116,18 @@ export function PromptInput({
|
|
|
91
116
|
initialValue = '',
|
|
92
117
|
draftOverride,
|
|
93
118
|
onEscape,
|
|
119
|
+
onTab,
|
|
94
120
|
onCommandPaletteNavigate,
|
|
95
121
|
onCommandPaletteAccept,
|
|
96
122
|
onCommandPaletteCancel,
|
|
97
123
|
onCommandPaletteComplete,
|
|
98
124
|
onRestoreQueued,
|
|
125
|
+
onHistoryNavigate,
|
|
99
126
|
onPasteText,
|
|
100
127
|
selectionRef,
|
|
101
128
|
valueRef,
|
|
129
|
+
boxRectRef,
|
|
130
|
+
mouseSelectionRef,
|
|
102
131
|
}) {
|
|
103
132
|
const [draft, setDraft] = useState(() => {
|
|
104
133
|
const value = String(initialValue || '');
|
|
@@ -109,43 +138,45 @@ export function PromptInput({
|
|
|
109
138
|
const lastReportedValueRef = useRef(draft.value);
|
|
110
139
|
if (valueRef) valueRef.current = draftRef.current.value;
|
|
111
140
|
const { isRawModeSupported } = useStdin();
|
|
112
|
-
// The text box's ink DOM node. We mark it as the cursor anchor (forked ink
|
|
113
|
-
// reads internal_cursorAnchor during render and parks the hardware cursor at
|
|
114
|
-
// that node's REAL laid-out position + our caret col/row — no external
|
|
115
|
-
// absolute-coordinate guessing, so it never drifts).
|
|
116
|
-
const boxRef = useRef(null);
|
|
117
|
-
const cursorEnabledRef = useRef(false); // latest enabled state, read by the anchor fn at render time
|
|
118
|
-
const contentWidthRef = useRef(80);
|
|
119
|
-
const preferredColumnRef = useRef(null);
|
|
120
|
-
const { value, cursor } = draft;
|
|
121
|
-
draftRef.current = draft;
|
|
122
|
-
if (selectionRef) {
|
|
123
|
-
const range = selectionRange(draft);
|
|
124
|
-
selectionRef.current = range
|
|
125
|
-
? { range, text: mask ? '' : draft.value.slice(range.start, range.end) }
|
|
126
|
-
: null;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Bypass ink's render throttle for keystroke echo. ink coalesces renders to
|
|
130
|
-
// maxFps (leading+trailing throttle), so when typing faster than one frame the
|
|
131
|
-
// last chars land on the trailing timer — felt as input lag ("a beat behind").
|
|
132
|
-
// ink exposes an UNthrottled `onImmediateRender` on the ink-root node; walking
|
|
133
|
-
// the parent chain from our box and firing it flushes the new draft in the same
|
|
134
|
-
// tick. Guarded so a structure change in the ink fork degrades to throttled
|
|
135
|
-
// (slower) rendering, never a crash.
|
|
141
|
+
// The text box's ink DOM node. We mark it as the cursor anchor (forked ink
|
|
142
|
+
// reads internal_cursorAnchor during render and parks the hardware cursor at
|
|
143
|
+
// that node's REAL laid-out position + our caret col/row — no external
|
|
144
|
+
// absolute-coordinate guessing, so it never drifts).
|
|
145
|
+
const boxRef = useRef(null);
|
|
146
|
+
const cursorEnabledRef = useRef(false); // latest enabled state, read by the anchor fn at render time
|
|
147
|
+
const contentWidthRef = useRef(80);
|
|
148
|
+
const preferredColumnRef = useRef(null);
|
|
149
|
+
const { value, cursor } = draft;
|
|
150
|
+
draftRef.current = draft;
|
|
151
|
+
if (selectionRef) {
|
|
152
|
+
const range = selectionRange(draft);
|
|
153
|
+
selectionRef.current = range
|
|
154
|
+
? { range, text: mask ? '' : draft.value.slice(range.start, range.end) }
|
|
155
|
+
: null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Bypass ink's render throttle for keystroke echo. ink coalesces renders to
|
|
159
|
+
// maxFps (leading+trailing throttle), so when typing faster than one frame the
|
|
160
|
+
// last chars land on the trailing timer — felt as input lag ("a beat behind").
|
|
161
|
+
// ink exposes an UNthrottled `onImmediateRender` on the ink-root node; walking
|
|
162
|
+
// the parent chain from our box and firing it flushes the new draft in the same
|
|
163
|
+
// tick. Guarded so a structure change in the ink fork degrades to throttled
|
|
164
|
+
// (slower) rendering, never a crash.
|
|
136
165
|
const flushImmediate = () => {
|
|
137
|
-
let node = boxRef.current;
|
|
138
|
-
for (let i = 0; node && i < 64; i++) {
|
|
139
|
-
if (node.nodeName === 'ink-root') {
|
|
140
|
-
if (typeof node.onImmediateRender === 'function') node.onImmediateRender();
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
node = node.parentNode;
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
|
|
166
|
+
let node = boxRef.current;
|
|
167
|
+
for (let i = 0; node && i < 64; i++) {
|
|
168
|
+
if (node.nodeName === 'ink-root') {
|
|
169
|
+
if (typeof node.onImmediateRender === 'function') node.onImmediateRender();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
node = node.parentNode;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
147
176
|
const commitDraft = (next, options = {}) => {
|
|
177
|
+
const sameDraft = draftStateEqual(draftRef.current, next);
|
|
148
178
|
if (!options.keepPreferredColumn) preferredColumnRef.current = null;
|
|
179
|
+
if (sameDraft) return;
|
|
149
180
|
draftRef.current = next;
|
|
150
181
|
setDraft(next);
|
|
151
182
|
queueMicrotask(flushImmediate);
|
|
@@ -159,21 +190,78 @@ export function PromptInput({
|
|
|
159
190
|
const installCursorAnchor = () => {
|
|
160
191
|
if (!boxRef.current || boxRef.current.internal_cursorAnchor) return false;
|
|
161
192
|
boxRef.current.internal_cursorAnchor = (yogaNode) => {
|
|
193
|
+
// [mixdog] Report the editable content box's REAL absolute rect up to App
|
|
194
|
+
// every frame so the mouse handler can map a click/drag cell to an edit
|
|
195
|
+
// offset. Walk the parent chain summing yoga computed offsets (same math
|
|
196
|
+
// render-node-to-output uses) — boxRef is the flex-row that holds the text
|
|
197
|
+
// node, so its absolute x/y is the first content cell (col 0,row 0).
|
|
198
|
+
if (boxRectRef) {
|
|
199
|
+
let absLeft = 0;
|
|
200
|
+
let absTop = 0;
|
|
201
|
+
let node = boxRef.current;
|
|
202
|
+
for (let i = 0; node && i < 64; i++) {
|
|
203
|
+
const yn = node.yogaNode;
|
|
204
|
+
if (yn?.getComputedLeft) {
|
|
205
|
+
absLeft += yn.getComputedLeft() || 0;
|
|
206
|
+
absTop += yn.getComputedTop() || 0;
|
|
207
|
+
}
|
|
208
|
+
if (node.nodeName === 'ink-root') break;
|
|
209
|
+
node = node.parentNode;
|
|
210
|
+
}
|
|
211
|
+
const wNow = yogaNode?.getComputedWidth?.() ?? 0;
|
|
212
|
+
const hNow = yogaNode?.getComputedHeight?.() ?? 1;
|
|
213
|
+
boxRectRef.current = {
|
|
214
|
+
top: absTop,
|
|
215
|
+
left: absLeft,
|
|
216
|
+
height: Math.max(1, hNow || 1),
|
|
217
|
+
contentWidth: contentWidthRef.current,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
162
220
|
if (!cursorEnabledRef.current) return null;
|
|
163
221
|
const d = draftRef.current;
|
|
164
222
|
const w = yogaNode?.getComputedWidth?.() ?? 0;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
223
|
+
const guardColumns = w > IME_LEFT_GUARD_COLUMNS ? IME_LEFT_GUARD_COLUMNS : 0;
|
|
224
|
+
const contentWidth = Math.max(1, (w ? w - guardColumns : contentWidthRef.current) || 80);
|
|
225
|
+
contentWidthRef.current = contentWidth;
|
|
226
|
+
const caret = w > 0
|
|
227
|
+
? caretPosition(d.value, d.cursor, contentWidth)
|
|
168
228
|
: { row: 0, col: stringWidth(d.value.slice(0, d.cursor)) };
|
|
229
|
+
return w > 0
|
|
230
|
+
? { ...caret, col: caret.col + guardColumns }
|
|
231
|
+
: caret;
|
|
169
232
|
};
|
|
170
233
|
return true;
|
|
171
234
|
};
|
|
172
|
-
|
|
235
|
+
|
|
173
236
|
const updateDraft = (fn, options = {}) => {
|
|
174
237
|
commitDraft(fn(draftRef.current), options);
|
|
175
238
|
};
|
|
176
239
|
|
|
240
|
+
// [mixdog] Mouse drag-selection driver. App's single mouse handler maps a
|
|
241
|
+
// click/drag cell over the prompt box to an edit offset and calls these so the
|
|
242
|
+
// SAME selectionAnchor/cursor engine that keyboard Shift-selection uses paints
|
|
243
|
+
// the highlight. Anchor on press, extend on drag/release; clear on a plain
|
|
244
|
+
// click. Reuses contentWidthRef (the real measured content width).
|
|
245
|
+
if (mouseSelectionRef) {
|
|
246
|
+
mouseSelectionRef.current = {
|
|
247
|
+
offsetAtCell: (row, col) => offsetAtCell(draftRef.current.value, row, col, contentWidthRef.current),
|
|
248
|
+
anchorAt: (offset) => {
|
|
249
|
+
const value = draftRef.current.value;
|
|
250
|
+
const off = Math.max(0, Math.min(value.length, Math.floor(Number(offset) || 0)));
|
|
251
|
+
commitDraft({ ...draftRef.current, cursor: off, selectionAnchor: off });
|
|
252
|
+
},
|
|
253
|
+
extendTo: (offset) => {
|
|
254
|
+
const d = draftRef.current;
|
|
255
|
+
const off = Math.max(0, Math.min(d.value.length, Math.floor(Number(offset) || 0)));
|
|
256
|
+
const anchor = Number.isFinite(d.selectionAnchor) ? d.selectionAnchor : d.cursor;
|
|
257
|
+
commitDraft({ ...d, cursor: off, selectionAnchor: anchor });
|
|
258
|
+
},
|
|
259
|
+
clear: () => {
|
|
260
|
+
if (selectionRange(draftRef.current)) commitDraft(clearSelection(draftRef.current));
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
177
265
|
const moveDraftVertically = (direction, { extend = false } = {}) => {
|
|
178
266
|
const current = draftRef.current;
|
|
179
267
|
const moved = verticalOffset(
|
|
@@ -193,6 +281,13 @@ export function PromptInput({
|
|
|
193
281
|
return onRestoreQueued?.(draftRef.current.value) === true;
|
|
194
282
|
};
|
|
195
283
|
|
|
284
|
+
const applyHistoryNavigation = (direction, meta = {}) => {
|
|
285
|
+
const nextValue = onHistoryNavigate?.(direction, draftRef.current.value, meta);
|
|
286
|
+
if (typeof nextValue !== 'string') return false;
|
|
287
|
+
commitDraft({ value: nextValue, cursor: nextValue.length, selectionAnchor: null });
|
|
288
|
+
return true;
|
|
289
|
+
};
|
|
290
|
+
|
|
196
291
|
const insertAtDraft = (text) => {
|
|
197
292
|
const value = String(text ?? '');
|
|
198
293
|
if (!value) return;
|
|
@@ -234,12 +329,12 @@ export function PromptInput({
|
|
|
234
329
|
}, [selectionRef]);
|
|
235
330
|
|
|
236
331
|
const submitDraft = (next) => {
|
|
237
|
-
const text = next.value;
|
|
238
|
-
const accepted = onSubmit?.(text) !== false;
|
|
239
|
-
if (!accepted) {
|
|
240
|
-
commitDraft(next);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
332
|
+
const text = next.value;
|
|
333
|
+
const accepted = onSubmit?.(text) !== false;
|
|
334
|
+
if (!accepted) {
|
|
335
|
+
commitDraft(next);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
243
338
|
commitDraft({ value: '', cursor: 0, selectionAnchor: null });
|
|
244
339
|
};
|
|
245
340
|
|
|
@@ -257,34 +352,47 @@ export function PromptInput({
|
|
|
257
352
|
}
|
|
258
353
|
submitDraft(next);
|
|
259
354
|
};
|
|
260
|
-
|
|
261
|
-
// Input capture is only active on a real TTY (raw mode). In pipes/CI the input
|
|
262
|
-
// is inert — useInput with isActive:false won't throw.
|
|
355
|
+
|
|
356
|
+
// Input capture is only active on a real TTY (raw mode). In pipes/CI the input
|
|
357
|
+
// is inert — useInput with isActive:false won't throw.
|
|
263
358
|
usePaste((text) => {
|
|
264
359
|
if (disabled) return;
|
|
265
360
|
handleExternalPaste(text, { source: 'paste' });
|
|
266
361
|
}, { isActive: isRawModeSupported && !disabled });
|
|
267
|
-
|
|
268
|
-
useInput((input, key) => {
|
|
269
|
-
if (disabled) return;
|
|
270
|
-
|
|
271
|
-
const rawInput = String(input ?? '');
|
|
272
|
-
const inputKey = rawInput.toLowerCase();
|
|
273
|
-
|
|
274
|
-
// Drop SGR mouse-tracking sequences (wheel/click). When app mouse tracking
|
|
275
|
-
// is explicitly enabled, App parses these off raw stdin itself;
|
|
276
|
-
// ink still forwards the bytes here as "input", which would otherwise type
|
|
277
|
-
// garbage like `[<64;55;22M` into the prompt. Match with or without the
|
|
278
|
-
// leading ESC (terminals/ink may strip it): CSI '<' … final 'M'/'m'.
|
|
362
|
+
|
|
363
|
+
useInput((input, key) => {
|
|
364
|
+
if (disabled) return;
|
|
365
|
+
|
|
366
|
+
const rawInput = String(input ?? '');
|
|
367
|
+
const inputKey = rawInput.toLowerCase();
|
|
368
|
+
|
|
369
|
+
// Drop SGR mouse-tracking sequences (wheel/click). When app mouse tracking
|
|
370
|
+
// is explicitly enabled, App parses these off raw stdin itself;
|
|
371
|
+
// ink still forwards the bytes here as "input", which would otherwise type
|
|
372
|
+
// garbage like `[<64;55;22M` into the prompt. Match with or without the
|
|
373
|
+
// leading ESC (terminals/ink may strip it): CSI '<' … final 'M'/'m'.
|
|
279
374
|
if (/(?:\x1b)?\[<\d+;\d+;\d+[Mm]/.test(rawInput) || /^\[?<\d+;\d+;\d+[Mm]?$/.test(rawInput)) {
|
|
280
375
|
return;
|
|
281
376
|
}
|
|
282
377
|
|
|
283
378
|
const rawUpArrow = rawInput === '\x1b[A' || rawInput === '\x1bOA' || rawInput === '[A' || rawInput === 'OA';
|
|
284
379
|
const rawDownArrow = rawInput === '\x1b[B' || rawInput === '\x1bOB' || rawInput === '[B' || rawInput === 'OB';
|
|
380
|
+
// Shift+Arrow modifier sequences (xterm `\x1b[1;2<dir>`, rxvt `\x1b[<dir>`
|
|
381
|
+
// lowercase). Ink's useInput does not decode the `;2` (shift) modifier into
|
|
382
|
+
// key.shift for arrows, so the bytes arrive as raw input and the plain-arrow
|
|
383
|
+
// matchers above miss them — selection-extend never fires. Detect them here
|
|
384
|
+
// and fold into a single `shiftHeld` signal used by every arrow/home/end
|
|
385
|
+
// branch below (alongside ink's key.shift for terminals that DO decode it).
|
|
386
|
+
const rawShiftUp = rawInput === '\x1b[1;2A' || rawInput === '\x1b[a' || rawInput === '[1;2A';
|
|
387
|
+
const rawShiftDown = rawInput === '\x1b[1;2B' || rawInput === '\x1b[b' || rawInput === '[1;2B';
|
|
388
|
+
const rawShiftRight = rawInput === '\x1b[1;2C' || rawInput === '\x1b[c' || rawInput === '[1;2C';
|
|
389
|
+
const rawShiftLeft = rawInput === '\x1b[1;2D' || rawInput === '\x1b[d' || rawInput === '[1;2D';
|
|
390
|
+
const shiftHeld = key.shift || rawShiftUp || rawShiftDown || rawShiftLeft || rawShiftRight;
|
|
285
391
|
const lineBreakIndex = rawInput.search(/[\r\n]/);
|
|
286
392
|
const rawEnter = rawInput === '\r' || rawInput === '\n' || rawInput === '\r\n';
|
|
287
393
|
const trailingEnterPrefix = singleTrailingLineBreakPrefix(rawInput);
|
|
394
|
+
const rawCtrlEnter = isCtrlEnterSequence(rawInput);
|
|
395
|
+
const modifiedLineBreak = key.shift || key.meta || key.ctrl || rawCtrlEnter;
|
|
288
396
|
|
|
289
397
|
const pasteFallback = lineBreakIndex !== -1 && trailingEnterPrefix === null && !rawEnter && (rawInput.length > 1 || !key.return);
|
|
290
398
|
if (pasteFallback) {
|
|
@@ -293,7 +401,7 @@ export function PromptInput({
|
|
|
293
401
|
}
|
|
294
402
|
|
|
295
403
|
if (trailingEnterPrefix !== null) {
|
|
296
|
-
if (
|
|
404
|
+
if (modifiedLineBreak) {
|
|
297
405
|
updateDraft((d) => insertText(d, `${trailingEnterPrefix}\n`));
|
|
298
406
|
return;
|
|
299
407
|
}
|
|
@@ -301,53 +409,65 @@ export function PromptInput({
|
|
|
301
409
|
return;
|
|
302
410
|
}
|
|
303
411
|
|
|
412
|
+
if (rawCtrlEnter) {
|
|
413
|
+
updateDraft((d) => replaceSelection(d, '\n'));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
304
417
|
if (!commandPaletteActive && ((key.ctrl && inputKey === 'v') || (key.meta && inputKey === 'v'))) {
|
|
305
418
|
handleExternalPaste('', { source: 'clipboard-image-shortcut' });
|
|
306
419
|
return;
|
|
307
420
|
}
|
|
308
421
|
|
|
309
|
-
if (key.return) {
|
|
310
|
-
if (
|
|
311
|
-
updateDraft((d) => replaceSelection(d, '\n'));
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (commandPaletteActive) {
|
|
316
|
-
const accepted = onCommandPaletteAccept?.(draftRef.current.value);
|
|
317
|
-
if (accepted !== false) {
|
|
318
|
-
commitDraft({ value: '', cursor: 0, selectionAnchor: null });
|
|
422
|
+
if (key.return) {
|
|
423
|
+
if (modifiedLineBreak) {
|
|
424
|
+
updateDraft((d) => replaceSelection(d, '\n'));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (commandPaletteActive) {
|
|
429
|
+
const accepted = onCommandPaletteAccept?.(draftRef.current.value);
|
|
430
|
+
if (accepted !== false) {
|
|
431
|
+
commitDraft({ value: '', cursor: 0, selectionAnchor: null });
|
|
319
432
|
}
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const current = draftRef.current;
|
|
324
|
-
if (current.value[current.cursor - 1] === '\\') {
|
|
325
|
-
updateDraft((d) => ({
|
|
326
|
-
value: `${d.value.slice(0, d.cursor - 1)}\n${d.value.slice(d.cursor)}`,
|
|
327
|
-
cursor: d.cursor,
|
|
328
|
-
selectionAnchor: null,
|
|
329
|
-
}));
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
submitDraft(current);
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (key.upArrow || rawUpArrow) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const current = draftRef.current;
|
|
437
|
+
if (current.value[current.cursor - 1] === '\\') {
|
|
438
|
+
updateDraft((d) => ({
|
|
439
|
+
value: `${d.value.slice(0, d.cursor - 1)}\n${d.value.slice(d.cursor)}`,
|
|
440
|
+
cursor: d.cursor,
|
|
441
|
+
selectionAnchor: null,
|
|
442
|
+
}));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
submitDraft(current);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (key.upArrow || rawUpArrow || rawShiftUp) {
|
|
338
451
|
if (commandPaletteActive) {
|
|
339
452
|
onCommandPaletteNavigate?.(-1);
|
|
340
453
|
} else {
|
|
341
|
-
|
|
454
|
+
const hasDraftText = String(draftRef.current.value || '').trim().length > 0;
|
|
455
|
+
if (!hasDraftText) {
|
|
456
|
+
if (!restoreQueuedToDraft()) applyHistoryNavigation('up', { emptyDraft: true });
|
|
457
|
+
} else if (!moveDraftVertically(-1, { extend: shiftHeld })) {
|
|
458
|
+
applyHistoryNavigation('up', { emptyDraft: false });
|
|
459
|
+
}
|
|
342
460
|
}
|
|
343
461
|
return;
|
|
344
462
|
}
|
|
345
463
|
|
|
346
|
-
if (key.downArrow || rawDownArrow) {
|
|
464
|
+
if (key.downArrow || rawDownArrow || rawShiftDown) {
|
|
347
465
|
if (commandPaletteActive) {
|
|
348
466
|
onCommandPaletteNavigate?.(1);
|
|
349
467
|
} else {
|
|
350
|
-
moveDraftVertically(1, { extend:
|
|
468
|
+
if (!moveDraftVertically(1, { extend: shiftHeld })) {
|
|
469
|
+
applyHistoryNavigation('down', { emptyDraft: String(draftRef.current.value || '').trim().length === 0 });
|
|
470
|
+
}
|
|
351
471
|
}
|
|
352
472
|
return;
|
|
353
473
|
}
|
|
@@ -372,14 +492,17 @@ export function PromptInput({
|
|
|
372
492
|
return;
|
|
373
493
|
}
|
|
374
494
|
|
|
375
|
-
if (key.tab
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
495
|
+
if (key.tab) {
|
|
496
|
+
if (commandPaletteActive) {
|
|
497
|
+
const completed = onCommandPaletteComplete?.(draftRef.current.value);
|
|
498
|
+
if (typeof completed === 'string') {
|
|
499
|
+
commitDraft({ value: completed, cursor: completed.length, selectionAnchor: null });
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (onTab?.(draftRef.current.value) === true) return;
|
|
504
|
+
}
|
|
505
|
+
|
|
383
506
|
if (key.escape) {
|
|
384
507
|
if (commandPaletteActive) {
|
|
385
508
|
onCommandPaletteCancel?.(draftRef.current.value);
|
|
@@ -399,6 +522,10 @@ export function PromptInput({
|
|
|
399
522
|
commitDraft({ value: '', cursor: 0, selectionAnchor: null });
|
|
400
523
|
return;
|
|
401
524
|
}
|
|
525
|
+
// Active turn takes precedence over queue restore (matches claude-code
|
|
526
|
+
// useCancelRequest priority): Esc during a running turn interrupts the
|
|
527
|
+
// turn and leaves queued steering prompts intact to run afterward. Queued
|
|
528
|
+
// messages only pop back into the draft when the turn is idle.
|
|
402
529
|
if (interruptActive) {
|
|
403
530
|
const restoredText = onInterrupt?.('');
|
|
404
531
|
if (typeof restoredText === 'string') {
|
|
@@ -413,145 +540,145 @@ export function PromptInput({
|
|
|
413
540
|
return;
|
|
414
541
|
}
|
|
415
542
|
|
|
416
|
-
if (key.leftArrow) {
|
|
543
|
+
if (key.leftArrow || rawShiftLeft) {
|
|
417
544
|
if (commandPaletteActive) {
|
|
418
545
|
onCommandPaletteNavigate?.('left');
|
|
419
546
|
return;
|
|
420
547
|
}
|
|
421
548
|
updateDraft((d) => {
|
|
422
|
-
const range = !
|
|
549
|
+
const range = !shiftHeld && !key.ctrl && !key.meta ? selectionRange(d) : null;
|
|
423
550
|
const cursor = range
|
|
424
551
|
? range.start
|
|
425
552
|
: key.ctrl || key.meta
|
|
426
553
|
? previousWordOffset(d.value, d.cursor)
|
|
427
554
|
: previousOffset(d.value, d.cursor);
|
|
428
|
-
return moveCursor(d, cursor, { extend:
|
|
555
|
+
return moveCursor(d, cursor, { extend: shiftHeld });
|
|
429
556
|
});
|
|
430
557
|
return;
|
|
431
558
|
}
|
|
432
|
-
if (key.rightArrow) {
|
|
559
|
+
if (key.rightArrow || rawShiftRight) {
|
|
433
560
|
if (commandPaletteActive) {
|
|
434
561
|
onCommandPaletteNavigate?.('right');
|
|
435
562
|
return;
|
|
436
563
|
}
|
|
437
564
|
updateDraft((d) => {
|
|
438
|
-
const range = !
|
|
565
|
+
const range = !shiftHeld && !key.ctrl && !key.meta ? selectionRange(d) : null;
|
|
439
566
|
const cursor = range
|
|
440
567
|
? range.end
|
|
441
568
|
: key.ctrl || key.meta
|
|
442
569
|
? nextWordOffset(d.value, d.cursor)
|
|
443
570
|
: nextOffset(d.value, d.cursor);
|
|
444
|
-
return moveCursor(d, cursor, { extend:
|
|
571
|
+
return moveCursor(d, cursor, { extend: shiftHeld });
|
|
445
572
|
});
|
|
446
573
|
return;
|
|
447
574
|
}
|
|
448
|
-
if (key.home) {
|
|
449
|
-
updateDraft((d) => moveCursor(d, lineStart(d.value, d.cursor), { extend:
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
if (key.end) {
|
|
575
|
+
if (key.home) {
|
|
576
|
+
updateDraft((d) => moveCursor(d, lineStart(d.value, d.cursor), { extend: shiftHeld }));
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (key.end) {
|
|
580
|
+
updateDraft((d) => moveCursor(d, lineEnd(d.value, d.cursor), { extend: shiftHeld }));
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const editingKey = String(input || '').toLowerCase();
|
|
585
|
+
|
|
586
|
+
// ctrl+a selects all like a normal text box; ctrl+e keeps readline line-end.
|
|
587
|
+
if (key.ctrl && editingKey === 'a') {
|
|
588
|
+
updateDraft((d) => (d.value ? { ...d, cursor: d.value.length, selectionAnchor: 0 } : clearSelection(d)));
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (key.ctrl && editingKey === 'e') {
|
|
453
592
|
updateDraft((d) => moveCursor(d, lineEnd(d.value, d.cursor), { extend: key.shift }));
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
return
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}, { isActive: isRawModeSupported && !disabled });
|
|
544
|
-
|
|
545
|
-
// Mark the text-box node with a cursor-anchor FUNCTION. Forked ink calls it
|
|
546
|
-
// during renderNodeToOutput — AFTER yoga layout is final and (crucially)
|
|
547
|
-
// during the same onRender that paints the new text. The function reads the
|
|
548
|
-
// latest caret from refs (synced every render below), so the cursor can never
|
|
549
|
-
// be stale: an earlier object-anchor set in a layout effect was always one
|
|
550
|
-
// keystroke behind, because ink's reconciler runs onRender inside
|
|
551
|
-
// resetAfterCommit BEFORE React layout effects — that lag made the 2nd+ char
|
|
552
|
-
// appear to land behind the caret (the observed scramble). Computing inside
|
|
553
|
-
// the fork, from the real layout + current refs, fixes that by construction.
|
|
554
|
-
cursorEnabledRef.current = !disabled && isRawModeSupported;
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
// ctrl+b / ctrl+f — character left / right.
|
|
596
|
+
if (key.ctrl && editingKey === 'b') {
|
|
597
|
+
updateDraft((d) => moveCursor(d, previousOffset(d.value, d.cursor), { extend: key.shift }));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (key.ctrl && editingKey === 'f') {
|
|
601
|
+
updateDraft((d) => moveCursor(d, nextOffset(d.value, d.cursor), { extend: key.shift }));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
// alt/option+b / alt/option+f — word left / right.
|
|
605
|
+
if (key.meta && editingKey === 'b') {
|
|
606
|
+
updateDraft((d) => moveCursor(d, previousWordOffset(d.value, d.cursor), { extend: key.shift }));
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (key.meta && editingKey === 'f') {
|
|
610
|
+
updateDraft((d) => moveCursor(d, nextWordOffset(d.value, d.cursor), { extend: key.shift }));
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
// ctrl+u / ctrl+k — delete to line start / end.
|
|
614
|
+
if (key.ctrl && editingKey === 'u') {
|
|
615
|
+
updateDraft(deleteToLineStart);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (key.ctrl && editingKey === 'k') {
|
|
619
|
+
updateDraft(deleteToLineEnd);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
// ctrl+w / alt+backspace — delete previous word.
|
|
623
|
+
if ((key.ctrl && editingKey === 'w') || ((key.ctrl || key.meta) && key.backspace)) {
|
|
624
|
+
updateDraft(deleteBackwardWord);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
// alt+d / ctrl+delete — delete next word.
|
|
628
|
+
if ((key.meta && editingKey === 'd') || (key.ctrl && key.delete)) {
|
|
629
|
+
updateDraft(deleteForwardWord);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (key.backspace) {
|
|
634
|
+
updateDraft((d) => {
|
|
635
|
+
if (selectionRange(d)) return deleteSelectedText(d);
|
|
636
|
+
if (d.cursor <= 0) return d;
|
|
637
|
+
const start = previousOffset(d.value, d.cursor);
|
|
638
|
+
return {
|
|
639
|
+
value: d.value.slice(0, start) + d.value.slice(d.cursor),
|
|
640
|
+
cursor: start,
|
|
641
|
+
selectionAnchor: null,
|
|
642
|
+
};
|
|
643
|
+
});
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (key.delete) {
|
|
648
|
+
updateDraft((d) => {
|
|
649
|
+
if (selectionRange(d)) return deleteSelectedText(d);
|
|
650
|
+
if (d.cursor >= d.value.length) return d;
|
|
651
|
+
const end = nextOffset(d.value, d.cursor);
|
|
652
|
+
return {
|
|
653
|
+
value: d.value.slice(0, d.cursor) + d.value.slice(end),
|
|
654
|
+
cursor: d.cursor,
|
|
655
|
+
selectionAnchor: null,
|
|
656
|
+
};
|
|
657
|
+
});
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Printable input (ignore other control keys). Strip any embedded SGR mouse
|
|
662
|
+
// sequences as a belt-and-suspenders guard (the early return above catches
|
|
663
|
+
// whole-sequence inputs; this removes partials that rode in with real text).
|
|
664
|
+
const printable = rawInput
|
|
665
|
+
.replace(/(?:\x1b)?\[<\d+;\d+;\d+[Mm]/g, '')
|
|
666
|
+
.replace(/[\r\n]/g, '');
|
|
667
|
+
if (printable && !key.ctrl && !key.meta) {
|
|
668
|
+
updateDraft((d) => insertText(d, printable));
|
|
669
|
+
}
|
|
670
|
+
}, { isActive: isRawModeSupported && !disabled });
|
|
671
|
+
|
|
672
|
+
// Mark the text-box node with a cursor-anchor FUNCTION. Forked ink calls it
|
|
673
|
+
// during renderNodeToOutput — AFTER yoga layout is final and (crucially)
|
|
674
|
+
// during the same onRender that paints the new text. The function reads the
|
|
675
|
+
// latest caret from refs (synced every render below), so the cursor can never
|
|
676
|
+
// be stale: an earlier object-anchor set in a layout effect was always one
|
|
677
|
+
// keystroke behind, because ink's reconciler runs onRender inside
|
|
678
|
+
// resetAfterCommit BEFORE React layout effects — that lag made the 2nd+ char
|
|
679
|
+
// appear to land behind the caret (the observed scramble). Computing inside
|
|
680
|
+
// the fork, from the real layout + current refs, fixes that by construction.
|
|
681
|
+
cursorEnabledRef.current = !disabled && isRawModeSupported;
|
|
555
682
|
installCursorAnchor();
|
|
556
683
|
|
|
557
684
|
useLayoutEffect(() => {
|
|
@@ -564,15 +691,16 @@ export function PromptInput({
|
|
|
564
691
|
if (disabled || !isRawModeSupported) return;
|
|
565
692
|
queueMicrotask(flushImmediate);
|
|
566
693
|
}, [disabled, isRawModeSupported]);
|
|
567
|
-
|
|
568
|
-
// Trailing space cell so the caret at end-of-input has a rendered cell to sit
|
|
569
|
-
// on (kept visually blank — no synthetic underline).
|
|
694
|
+
|
|
695
|
+
// Trailing space cell so the caret at end-of-input has a rendered cell to sit
|
|
696
|
+
// on (kept visually blank — no synthetic underline).
|
|
570
697
|
const displayValue = mask ? value.replace(/[^\n]/g, '*') : value;
|
|
571
698
|
const renderedValue = renderSelectedText(displayValue, selectionRange(draft), cursor === value.length);
|
|
572
699
|
const hintMeta = hintStyle(hintTone);
|
|
573
700
|
|
|
574
701
|
return (
|
|
575
702
|
<Box ref={boxRef} flexDirection="row" flexGrow={1} flexShrink={1} backgroundColor={theme.background}>
|
|
703
|
+
<Box width={IME_LEFT_GUARD_COLUMNS} flexShrink={0} backgroundColor={theme.background} />
|
|
576
704
|
<Text color={theme.text} wrap="hard">{renderedValue}</Text>
|
|
577
705
|
{!value && hint ? (
|
|
578
706
|
<Box marginLeft={-1}>
|