mixdog 0.8.1 → 0.9.1
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 +34 -28
- 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 +205 -33
- 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 +1222 -90
- 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 -29
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +242 -98
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2211 -446
- 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 +108 -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 +380 -268
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -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 +203 -71
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
- 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 +289 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- 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 +840 -612
- package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
- 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 +35 -8
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
- 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.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 +149 -51
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -6
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +519 -118
- 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/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
- package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
- 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 +1 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
- package/src/runtime/memory/lib/session-ingest.mjs +196 -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 +104 -473
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +44 -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 +139 -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 -95
- 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 +1 -78
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4800 -2140
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +14 -4
- 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 +428 -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 +597 -362
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +499 -0
- package/src/tui/components/tool-output-format.test.mjs +312 -0
- package/src/tui/dist/index.mjs +9192 -2399
- 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 +671 -69
- package/src/tui/markdown/format-token.test.mjs +312 -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 +756 -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 +2 -13
- package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
- 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/ink.js +54 -8
- 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/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- 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/channels/lib/hook-pipe-server.mjs +0 -671
- 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,20 @@ 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
|
+
// Coalesce prompt mouse-drag extend commits (SGR motion can fire faster than ink
|
|
56
|
+
// needs to immediate-render). Matches transcript selection paint cadence.
|
|
57
|
+
const MOUSE_EXTEND_COALESCE_MS = 24;
|
|
58
|
+
|
|
49
59
|
function insertText(draft, input) {
|
|
50
|
-
if (!input) return draft;
|
|
60
|
+
if (!input) return draft;
|
|
51
61
|
return replaceSelection(draft, input);
|
|
52
|
-
}
|
|
62
|
+
}
|
|
53
63
|
|
|
54
64
|
function renderSelectedText(displayValue, range, trailingSpace = false) {
|
|
55
65
|
if (!range) return trailingSpace ? `${displayValue} ` : displayValue;
|
|
@@ -59,14 +69,14 @@ function renderSelectedText(displayValue, range, trailingSpace = false) {
|
|
|
59
69
|
<>
|
|
60
70
|
{start > 0 ? displayValue.slice(0, start) : null}
|
|
61
71
|
{end > start ? (
|
|
62
|
-
<Text color={theme.
|
|
72
|
+
<Text color={theme.selectionText} backgroundColor={theme.selectionBackground}>{displayValue.slice(start, end)}</Text>
|
|
63
73
|
) : null}
|
|
64
74
|
{displayValue.slice(end)}
|
|
65
75
|
{trailingSpace ? ' ' : ''}
|
|
66
76
|
</>
|
|
67
77
|
);
|
|
68
78
|
}
|
|
69
|
-
|
|
79
|
+
|
|
70
80
|
function normalizePastedText(text) {
|
|
71
81
|
return String(text ?? '').replace(/\r\n?/g, '\n');
|
|
72
82
|
}
|
|
@@ -78,10 +88,28 @@ function singleTrailingLineBreakPrefix(text) {
|
|
|
78
88
|
return prefix.includes('\n') ? null : prefix;
|
|
79
89
|
}
|
|
80
90
|
|
|
91
|
+
function draftStateEqual(a, b) {
|
|
92
|
+
return (
|
|
93
|
+
a.value === b.value
|
|
94
|
+
&& a.cursor === b.cursor
|
|
95
|
+
&& a.selectionAnchor === b.selectionAnchor
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isCtrlEnterSequence(input) {
|
|
100
|
+
const text = String(input ?? '');
|
|
101
|
+
const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
|
|
102
|
+
if (!body) return false;
|
|
103
|
+
const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
|
|
104
|
+
if (kitty) return ((Number(kitty[1]) - 1) & 4) !== 0;
|
|
105
|
+
const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
|
|
106
|
+
return Boolean(modifyOtherKeys && (((Number(modifyOtherKeys[1]) - 1) & 4) !== 0));
|
|
107
|
+
}
|
|
108
|
+
|
|
81
109
|
export function PromptInput({
|
|
82
|
-
onSubmit,
|
|
83
|
-
disabled = false,
|
|
84
|
-
onDraftChange,
|
|
110
|
+
onSubmit,
|
|
111
|
+
disabled = false,
|
|
112
|
+
onDraftChange,
|
|
85
113
|
interruptActive = false,
|
|
86
114
|
onInterrupt,
|
|
87
115
|
commandPaletteActive = false,
|
|
@@ -91,14 +119,18 @@ export function PromptInput({
|
|
|
91
119
|
initialValue = '',
|
|
92
120
|
draftOverride,
|
|
93
121
|
onEscape,
|
|
122
|
+
onTab,
|
|
94
123
|
onCommandPaletteNavigate,
|
|
95
124
|
onCommandPaletteAccept,
|
|
96
125
|
onCommandPaletteCancel,
|
|
97
126
|
onCommandPaletteComplete,
|
|
98
127
|
onRestoreQueued,
|
|
128
|
+
onHistoryNavigate,
|
|
99
129
|
onPasteText,
|
|
100
130
|
selectionRef,
|
|
101
131
|
valueRef,
|
|
132
|
+
boxRectRef,
|
|
133
|
+
mouseSelectionRef,
|
|
102
134
|
}) {
|
|
103
135
|
const [draft, setDraft] = useState(() => {
|
|
104
136
|
const value = String(initialValue || '');
|
|
@@ -109,43 +141,46 @@ export function PromptInput({
|
|
|
109
141
|
const lastReportedValueRef = useRef(draft.value);
|
|
110
142
|
if (valueRef) valueRef.current = draftRef.current.value;
|
|
111
143
|
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 {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
144
|
+
// The text box's ink DOM node. We mark it as the cursor anchor (forked ink
|
|
145
|
+
// reads internal_cursorAnchor during render and parks the hardware cursor at
|
|
146
|
+
// that node's REAL laid-out position + our caret col/row — no external
|
|
147
|
+
// absolute-coordinate guessing, so it never drifts).
|
|
148
|
+
const boxRef = useRef(null);
|
|
149
|
+
const cursorEnabledRef = useRef(false); // latest enabled state, read by the anchor fn at render time
|
|
150
|
+
const contentWidthRef = useRef(80);
|
|
151
|
+
const preferredColumnRef = useRef(null);
|
|
152
|
+
const mouseExtendCoalesceRef = useRef({ pendingNext: null, timer: null, t: 0 });
|
|
153
|
+
const { value, cursor } = draft;
|
|
154
|
+
draftRef.current = draft;
|
|
155
|
+
if (selectionRef) {
|
|
156
|
+
const range = selectionRange(draft);
|
|
157
|
+
selectionRef.current = range
|
|
158
|
+
? { range, text: mask ? '' : draft.value.slice(range.start, range.end) }
|
|
159
|
+
: null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Bypass ink's render throttle for keystroke echo. ink coalesces renders to
|
|
163
|
+
// maxFps (leading+trailing throttle), so when typing faster than one frame the
|
|
164
|
+
// last chars land on the trailing timer — felt as input lag ("a beat behind").
|
|
165
|
+
// ink exposes an UNthrottled `onImmediateRender` on the ink-root node; walking
|
|
166
|
+
// the parent chain from our box and firing it flushes the new draft in the same
|
|
167
|
+
// tick. Guarded so a structure change in the ink fork degrades to throttled
|
|
168
|
+
// (slower) rendering, never a crash.
|
|
136
169
|
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
|
-
|
|
170
|
+
let node = boxRef.current;
|
|
171
|
+
for (let i = 0; node && i < 64; i++) {
|
|
172
|
+
if (node.nodeName === 'ink-root') {
|
|
173
|
+
if (typeof node.onImmediateRender === 'function') node.onImmediateRender();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
node = node.parentNode;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
147
180
|
const commitDraft = (next, options = {}) => {
|
|
181
|
+
const sameDraft = draftStateEqual(draftRef.current, next);
|
|
148
182
|
if (!options.keepPreferredColumn) preferredColumnRef.current = null;
|
|
183
|
+
if (sameDraft) return;
|
|
149
184
|
draftRef.current = next;
|
|
150
185
|
setDraft(next);
|
|
151
186
|
queueMicrotask(flushImmediate);
|
|
@@ -159,21 +194,135 @@ export function PromptInput({
|
|
|
159
194
|
const installCursorAnchor = () => {
|
|
160
195
|
if (!boxRef.current || boxRef.current.internal_cursorAnchor) return false;
|
|
161
196
|
boxRef.current.internal_cursorAnchor = (yogaNode) => {
|
|
197
|
+
// [mixdog] Report the editable content box's REAL absolute rect up to App
|
|
198
|
+
// every frame so the mouse handler can map a click/drag cell to an edit
|
|
199
|
+
// offset. Walk the parent chain summing yoga computed offsets (same math
|
|
200
|
+
// render-node-to-output uses) — boxRef is the flex-row that holds the text
|
|
201
|
+
// node, so its absolute x/y is the first content cell (col 0,row 0).
|
|
202
|
+
if (boxRectRef) {
|
|
203
|
+
let absLeft = 0;
|
|
204
|
+
let absTop = 0;
|
|
205
|
+
let node = boxRef.current;
|
|
206
|
+
for (let i = 0; node && i < 64; i++) {
|
|
207
|
+
const yn = node.yogaNode;
|
|
208
|
+
if (yn?.getComputedLeft) {
|
|
209
|
+
absLeft += yn.getComputedLeft() || 0;
|
|
210
|
+
absTop += yn.getComputedTop() || 0;
|
|
211
|
+
}
|
|
212
|
+
if (node.nodeName === 'ink-root') break;
|
|
213
|
+
node = node.parentNode;
|
|
214
|
+
}
|
|
215
|
+
const wNow = yogaNode?.getComputedWidth?.() ?? 0;
|
|
216
|
+
const hNow = yogaNode?.getComputedHeight?.() ?? 1;
|
|
217
|
+
boxRectRef.current = {
|
|
218
|
+
top: absTop,
|
|
219
|
+
left: absLeft,
|
|
220
|
+
height: Math.max(1, hNow || 1),
|
|
221
|
+
contentWidth: contentWidthRef.current,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
162
224
|
if (!cursorEnabledRef.current) return null;
|
|
163
225
|
const d = draftRef.current;
|
|
164
226
|
const w = yogaNode?.getComputedWidth?.() ?? 0;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
227
|
+
const guardColumns = w > IME_LEFT_GUARD_COLUMNS ? IME_LEFT_GUARD_COLUMNS : 0;
|
|
228
|
+
const contentWidth = Math.max(1, (w ? w - guardColumns : contentWidthRef.current) || 80);
|
|
229
|
+
contentWidthRef.current = contentWidth;
|
|
230
|
+
const caret = w > 0
|
|
231
|
+
? caretPosition(d.value, d.cursor, contentWidth)
|
|
168
232
|
: { row: 0, col: stringWidth(d.value.slice(0, d.cursor)) };
|
|
233
|
+
return w > 0
|
|
234
|
+
? { ...caret, col: caret.col + guardColumns }
|
|
235
|
+
: caret;
|
|
169
236
|
};
|
|
170
237
|
return true;
|
|
171
238
|
};
|
|
172
|
-
|
|
239
|
+
|
|
173
240
|
const updateDraft = (fn, options = {}) => {
|
|
174
241
|
commitDraft(fn(draftRef.current), options);
|
|
175
242
|
};
|
|
176
243
|
|
|
244
|
+
const cancelMouseExtendCoalesce = () => {
|
|
245
|
+
const state = mouseExtendCoalesceRef.current;
|
|
246
|
+
if (state.timer) {
|
|
247
|
+
clearTimeout(state.timer);
|
|
248
|
+
state.timer = null;
|
|
249
|
+
}
|
|
250
|
+
state.pendingNext = null;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const queueMouseExtendCommit = (next, immediate = false) => {
|
|
254
|
+
if (immediate) {
|
|
255
|
+
cancelMouseExtendCoalesce();
|
|
256
|
+
commitDraft(next);
|
|
257
|
+
mouseExtendCoalesceRef.current.t = Date.now();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (draftStateEqual(draftRef.current, next)) {
|
|
261
|
+
cancelMouseExtendCoalesce();
|
|
262
|
+
commitDraft(next);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const state = mouseExtendCoalesceRef.current;
|
|
266
|
+
state.pendingNext = next;
|
|
267
|
+
const now = Date.now();
|
|
268
|
+
const elapsed = now - state.t;
|
|
269
|
+
if (elapsed >= MOUSE_EXTEND_COALESCE_MS) {
|
|
270
|
+
cancelMouseExtendCoalesce();
|
|
271
|
+
state.t = now;
|
|
272
|
+
commitDraft(next);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (state.timer) return;
|
|
276
|
+
state.timer = setTimeout(() => {
|
|
277
|
+
const current = mouseExtendCoalesceRef.current;
|
|
278
|
+
const pending = current.pendingNext;
|
|
279
|
+
current.timer = null;
|
|
280
|
+
current.pendingNext = null;
|
|
281
|
+
current.t = Date.now();
|
|
282
|
+
if (pending) commitDraft(pending);
|
|
283
|
+
}, Math.max(1, MOUSE_EXTEND_COALESCE_MS - elapsed));
|
|
284
|
+
state.timer.unref?.();
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// [mixdog] Mouse drag-selection driver. App's single mouse handler maps a
|
|
288
|
+
// click/drag cell over the prompt box to an edit offset and calls these so the
|
|
289
|
+
// SAME selectionAnchor/cursor engine that keyboard Shift-selection uses paints
|
|
290
|
+
// the highlight. Anchor on press, extend on drag/release; clear on a plain
|
|
291
|
+
// click. Reuses contentWidthRef (the real measured content width).
|
|
292
|
+
if (mouseSelectionRef) {
|
|
293
|
+
mouseSelectionRef.current = {
|
|
294
|
+
offsetAtCell: (row, col) => offsetAtCell(draftRef.current.value, row, col, contentWidthRef.current),
|
|
295
|
+
anchorAt: (offset) => {
|
|
296
|
+
cancelMouseExtendCoalesce();
|
|
297
|
+
const value = draftRef.current.value;
|
|
298
|
+
const off = Math.max(0, Math.min(value.length, Math.floor(Number(offset) || 0)));
|
|
299
|
+
commitDraft({ ...draftRef.current, cursor: off, selectionAnchor: off });
|
|
300
|
+
mouseExtendCoalesceRef.current.t = Date.now();
|
|
301
|
+
},
|
|
302
|
+
extendTo: (offset, immediate = false) => {
|
|
303
|
+
if (offset == null) {
|
|
304
|
+
if (immediate) cancelMouseExtendCoalesce();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const d = draftRef.current;
|
|
308
|
+
const off = Math.max(0, Math.min(d.value.length, Math.floor(Number(offset) || 0)));
|
|
309
|
+
const anchor = Number.isFinite(d.selectionAnchor) ? d.selectionAnchor : d.cursor;
|
|
310
|
+
queueMouseExtendCommit({ ...d, cursor: off, selectionAnchor: anchor }, immediate);
|
|
311
|
+
},
|
|
312
|
+
clear: () => {
|
|
313
|
+
cancelMouseExtendCoalesce();
|
|
314
|
+
if (selectionRange(draftRef.current)) commitDraft(clearSelection(draftRef.current));
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
useEffect(() => () => {
|
|
320
|
+
const state = mouseExtendCoalesceRef.current;
|
|
321
|
+
if (state.timer) clearTimeout(state.timer);
|
|
322
|
+
state.timer = null;
|
|
323
|
+
state.pendingNext = null;
|
|
324
|
+
}, []);
|
|
325
|
+
|
|
177
326
|
const moveDraftVertically = (direction, { extend = false } = {}) => {
|
|
178
327
|
const current = draftRef.current;
|
|
179
328
|
const moved = verticalOffset(
|
|
@@ -193,6 +342,13 @@ export function PromptInput({
|
|
|
193
342
|
return onRestoreQueued?.(draftRef.current.value) === true;
|
|
194
343
|
};
|
|
195
344
|
|
|
345
|
+
const applyHistoryNavigation = (direction, meta = {}) => {
|
|
346
|
+
const nextValue = onHistoryNavigate?.(direction, draftRef.current.value, meta);
|
|
347
|
+
if (typeof nextValue !== 'string') return false;
|
|
348
|
+
commitDraft({ value: nextValue, cursor: nextValue.length, selectionAnchor: null });
|
|
349
|
+
return true;
|
|
350
|
+
};
|
|
351
|
+
|
|
196
352
|
const insertAtDraft = (text) => {
|
|
197
353
|
const value = String(text ?? '');
|
|
198
354
|
if (!value) return;
|
|
@@ -234,12 +390,12 @@ export function PromptInput({
|
|
|
234
390
|
}, [selectionRef]);
|
|
235
391
|
|
|
236
392
|
const submitDraft = (next) => {
|
|
237
|
-
const text = next.value;
|
|
238
|
-
const accepted = onSubmit?.(text) !== false;
|
|
239
|
-
if (!accepted) {
|
|
240
|
-
commitDraft(next);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
393
|
+
const text = next.value;
|
|
394
|
+
const accepted = onSubmit?.(text) !== false;
|
|
395
|
+
if (!accepted) {
|
|
396
|
+
commitDraft(next);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
243
399
|
commitDraft({ value: '', cursor: 0, selectionAnchor: null });
|
|
244
400
|
};
|
|
245
401
|
|
|
@@ -257,34 +413,47 @@ export function PromptInput({
|
|
|
257
413
|
}
|
|
258
414
|
submitDraft(next);
|
|
259
415
|
};
|
|
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.
|
|
416
|
+
|
|
417
|
+
// Input capture is only active on a real TTY (raw mode). In pipes/CI the input
|
|
418
|
+
// is inert — useInput with isActive:false won't throw.
|
|
263
419
|
usePaste((text) => {
|
|
264
420
|
if (disabled) return;
|
|
265
421
|
handleExternalPaste(text, { source: 'paste' });
|
|
266
422
|
}, { 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'.
|
|
423
|
+
|
|
424
|
+
useInput((input, key) => {
|
|
425
|
+
if (disabled) return;
|
|
426
|
+
|
|
427
|
+
const rawInput = String(input ?? '');
|
|
428
|
+
const inputKey = rawInput.toLowerCase();
|
|
429
|
+
|
|
430
|
+
// Drop SGR mouse-tracking sequences (wheel/click). When app mouse tracking
|
|
431
|
+
// is explicitly enabled, App parses these off raw stdin itself;
|
|
432
|
+
// ink still forwards the bytes here as "input", which would otherwise type
|
|
433
|
+
// garbage like `[<64;55;22M` into the prompt. Match with or without the
|
|
434
|
+
// leading ESC (terminals/ink may strip it): CSI '<' … final 'M'/'m'.
|
|
279
435
|
if (/(?:\x1b)?\[<\d+;\d+;\d+[Mm]/.test(rawInput) || /^\[?<\d+;\d+;\d+[Mm]?$/.test(rawInput)) {
|
|
280
436
|
return;
|
|
281
437
|
}
|
|
282
438
|
|
|
283
439
|
const rawUpArrow = rawInput === '\x1b[A' || rawInput === '\x1bOA' || rawInput === '[A' || rawInput === 'OA';
|
|
284
440
|
const rawDownArrow = rawInput === '\x1b[B' || rawInput === '\x1bOB' || rawInput === '[B' || rawInput === 'OB';
|
|
441
|
+
// Shift+Arrow modifier sequences (xterm `\x1b[1;2<dir>`, rxvt `\x1b[<dir>`
|
|
442
|
+
// lowercase). Ink's useInput does not decode the `;2` (shift) modifier into
|
|
443
|
+
// key.shift for arrows, so the bytes arrive as raw input and the plain-arrow
|
|
444
|
+
// matchers above miss them — selection-extend never fires. Detect them here
|
|
445
|
+
// and fold into a single `shiftHeld` signal used by every arrow/home/end
|
|
446
|
+
// branch below (alongside ink's key.shift for terminals that DO decode it).
|
|
447
|
+
const rawShiftUp = rawInput === '\x1b[1;2A' || rawInput === '\x1b[a' || rawInput === '[1;2A';
|
|
448
|
+
const rawShiftDown = rawInput === '\x1b[1;2B' || rawInput === '\x1b[b' || rawInput === '[1;2B';
|
|
449
|
+
const rawShiftRight = rawInput === '\x1b[1;2C' || rawInput === '\x1b[c' || rawInput === '[1;2C';
|
|
450
|
+
const rawShiftLeft = rawInput === '\x1b[1;2D' || rawInput === '\x1b[d' || rawInput === '[1;2D';
|
|
451
|
+
const shiftHeld = key.shift || rawShiftUp || rawShiftDown || rawShiftLeft || rawShiftRight;
|
|
285
452
|
const lineBreakIndex = rawInput.search(/[\r\n]/);
|
|
286
453
|
const rawEnter = rawInput === '\r' || rawInput === '\n' || rawInput === '\r\n';
|
|
287
454
|
const trailingEnterPrefix = singleTrailingLineBreakPrefix(rawInput);
|
|
455
|
+
const rawCtrlEnter = isCtrlEnterSequence(rawInput);
|
|
456
|
+
const modifiedLineBreak = key.shift || key.meta || key.ctrl || rawCtrlEnter;
|
|
288
457
|
|
|
289
458
|
const pasteFallback = lineBreakIndex !== -1 && trailingEnterPrefix === null && !rawEnter && (rawInput.length > 1 || !key.return);
|
|
290
459
|
if (pasteFallback) {
|
|
@@ -293,7 +462,7 @@ export function PromptInput({
|
|
|
293
462
|
}
|
|
294
463
|
|
|
295
464
|
if (trailingEnterPrefix !== null) {
|
|
296
|
-
if (
|
|
465
|
+
if (modifiedLineBreak) {
|
|
297
466
|
updateDraft((d) => insertText(d, `${trailingEnterPrefix}\n`));
|
|
298
467
|
return;
|
|
299
468
|
}
|
|
@@ -301,53 +470,65 @@ export function PromptInput({
|
|
|
301
470
|
return;
|
|
302
471
|
}
|
|
303
472
|
|
|
473
|
+
if (rawCtrlEnter) {
|
|
474
|
+
updateDraft((d) => replaceSelection(d, '\n'));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
304
478
|
if (!commandPaletteActive && ((key.ctrl && inputKey === 'v') || (key.meta && inputKey === 'v'))) {
|
|
305
479
|
handleExternalPaste('', { source: 'clipboard-image-shortcut' });
|
|
306
480
|
return;
|
|
307
481
|
}
|
|
308
482
|
|
|
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 });
|
|
483
|
+
if (key.return) {
|
|
484
|
+
if (modifiedLineBreak) {
|
|
485
|
+
updateDraft((d) => replaceSelection(d, '\n'));
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (commandPaletteActive) {
|
|
490
|
+
const accepted = onCommandPaletteAccept?.(draftRef.current.value);
|
|
491
|
+
if (accepted !== false) {
|
|
492
|
+
commitDraft({ value: '', cursor: 0, selectionAnchor: null });
|
|
319
493
|
}
|
|
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) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const current = draftRef.current;
|
|
498
|
+
if (current.value[current.cursor - 1] === '\\') {
|
|
499
|
+
updateDraft((d) => ({
|
|
500
|
+
value: `${d.value.slice(0, d.cursor - 1)}\n${d.value.slice(d.cursor)}`,
|
|
501
|
+
cursor: d.cursor,
|
|
502
|
+
selectionAnchor: null,
|
|
503
|
+
}));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
submitDraft(current);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (key.upArrow || rawUpArrow || rawShiftUp) {
|
|
338
512
|
if (commandPaletteActive) {
|
|
339
513
|
onCommandPaletteNavigate?.(-1);
|
|
340
514
|
} else {
|
|
341
|
-
|
|
515
|
+
const hasDraftText = String(draftRef.current.value || '').trim().length > 0;
|
|
516
|
+
if (!hasDraftText) {
|
|
517
|
+
if (!restoreQueuedToDraft()) applyHistoryNavigation('up', { emptyDraft: true });
|
|
518
|
+
} else if (!moveDraftVertically(-1, { extend: shiftHeld })) {
|
|
519
|
+
applyHistoryNavigation('up', { emptyDraft: false });
|
|
520
|
+
}
|
|
342
521
|
}
|
|
343
522
|
return;
|
|
344
523
|
}
|
|
345
524
|
|
|
346
|
-
if (key.downArrow || rawDownArrow) {
|
|
525
|
+
if (key.downArrow || rawDownArrow || rawShiftDown) {
|
|
347
526
|
if (commandPaletteActive) {
|
|
348
527
|
onCommandPaletteNavigate?.(1);
|
|
349
528
|
} else {
|
|
350
|
-
moveDraftVertically(1, { extend:
|
|
529
|
+
if (!moveDraftVertically(1, { extend: shiftHeld })) {
|
|
530
|
+
applyHistoryNavigation('down', { emptyDraft: String(draftRef.current.value || '').trim().length === 0 });
|
|
531
|
+
}
|
|
351
532
|
}
|
|
352
533
|
return;
|
|
353
534
|
}
|
|
@@ -372,14 +553,17 @@ export function PromptInput({
|
|
|
372
553
|
return;
|
|
373
554
|
}
|
|
374
555
|
|
|
375
|
-
if (key.tab
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
556
|
+
if (key.tab) {
|
|
557
|
+
if (commandPaletteActive) {
|
|
558
|
+
const completed = onCommandPaletteComplete?.(draftRef.current.value);
|
|
559
|
+
if (typeof completed === 'string') {
|
|
560
|
+
commitDraft({ value: completed, cursor: completed.length, selectionAnchor: null });
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (onTab?.(draftRef.current.value) === true) return;
|
|
565
|
+
}
|
|
566
|
+
|
|
383
567
|
if (key.escape) {
|
|
384
568
|
if (commandPaletteActive) {
|
|
385
569
|
onCommandPaletteCancel?.(draftRef.current.value);
|
|
@@ -399,6 +583,10 @@ export function PromptInput({
|
|
|
399
583
|
commitDraft({ value: '', cursor: 0, selectionAnchor: null });
|
|
400
584
|
return;
|
|
401
585
|
}
|
|
586
|
+
// Active turn takes precedence over queue restore (matches claude-code
|
|
587
|
+
// useCancelRequest priority): Esc during a running turn interrupts the
|
|
588
|
+
// turn and leaves queued steering prompts intact to run afterward. Queued
|
|
589
|
+
// messages only pop back into the draft when the turn is idle.
|
|
402
590
|
if (interruptActive) {
|
|
403
591
|
const restoredText = onInterrupt?.('');
|
|
404
592
|
if (typeof restoredText === 'string') {
|
|
@@ -413,145 +601,145 @@ export function PromptInput({
|
|
|
413
601
|
return;
|
|
414
602
|
}
|
|
415
603
|
|
|
416
|
-
if (key.leftArrow) {
|
|
604
|
+
if (key.leftArrow || rawShiftLeft) {
|
|
417
605
|
if (commandPaletteActive) {
|
|
418
606
|
onCommandPaletteNavigate?.('left');
|
|
419
607
|
return;
|
|
420
608
|
}
|
|
421
609
|
updateDraft((d) => {
|
|
422
|
-
const range = !
|
|
610
|
+
const range = !shiftHeld && !key.ctrl && !key.meta ? selectionRange(d) : null;
|
|
423
611
|
const cursor = range
|
|
424
612
|
? range.start
|
|
425
613
|
: key.ctrl || key.meta
|
|
426
614
|
? previousWordOffset(d.value, d.cursor)
|
|
427
615
|
: previousOffset(d.value, d.cursor);
|
|
428
|
-
return moveCursor(d, cursor, { extend:
|
|
616
|
+
return moveCursor(d, cursor, { extend: shiftHeld });
|
|
429
617
|
});
|
|
430
618
|
return;
|
|
431
619
|
}
|
|
432
|
-
if (key.rightArrow) {
|
|
620
|
+
if (key.rightArrow || rawShiftRight) {
|
|
433
621
|
if (commandPaletteActive) {
|
|
434
622
|
onCommandPaletteNavigate?.('right');
|
|
435
623
|
return;
|
|
436
624
|
}
|
|
437
625
|
updateDraft((d) => {
|
|
438
|
-
const range = !
|
|
626
|
+
const range = !shiftHeld && !key.ctrl && !key.meta ? selectionRange(d) : null;
|
|
439
627
|
const cursor = range
|
|
440
628
|
? range.end
|
|
441
629
|
: key.ctrl || key.meta
|
|
442
630
|
? nextWordOffset(d.value, d.cursor)
|
|
443
631
|
: nextOffset(d.value, d.cursor);
|
|
444
|
-
return moveCursor(d, cursor, { extend:
|
|
632
|
+
return moveCursor(d, cursor, { extend: shiftHeld });
|
|
445
633
|
});
|
|
446
634
|
return;
|
|
447
635
|
}
|
|
448
|
-
if (key.home) {
|
|
449
|
-
updateDraft((d) => moveCursor(d, lineStart(d.value, d.cursor), { extend:
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
if (key.end) {
|
|
636
|
+
if (key.home) {
|
|
637
|
+
updateDraft((d) => moveCursor(d, lineStart(d.value, d.cursor), { extend: shiftHeld }));
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (key.end) {
|
|
641
|
+
updateDraft((d) => moveCursor(d, lineEnd(d.value, d.cursor), { extend: shiftHeld }));
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const editingKey = String(input || '').toLowerCase();
|
|
646
|
+
|
|
647
|
+
// ctrl+a selects all like a normal text box; ctrl+e keeps readline line-end.
|
|
648
|
+
if (key.ctrl && editingKey === 'a') {
|
|
649
|
+
updateDraft((d) => (d.value ? { ...d, cursor: d.value.length, selectionAnchor: 0 } : clearSelection(d)));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (key.ctrl && editingKey === 'e') {
|
|
453
653
|
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;
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
// ctrl+b / ctrl+f — character left / right.
|
|
657
|
+
if (key.ctrl && editingKey === 'b') {
|
|
658
|
+
updateDraft((d) => moveCursor(d, previousOffset(d.value, d.cursor), { extend: key.shift }));
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (key.ctrl && editingKey === 'f') {
|
|
662
|
+
updateDraft((d) => moveCursor(d, nextOffset(d.value, d.cursor), { extend: key.shift }));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
// alt/option+b / alt/option+f — word left / right.
|
|
666
|
+
if (key.meta && editingKey === 'b') {
|
|
667
|
+
updateDraft((d) => moveCursor(d, previousWordOffset(d.value, d.cursor), { extend: key.shift }));
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (key.meta && editingKey === 'f') {
|
|
671
|
+
updateDraft((d) => moveCursor(d, nextWordOffset(d.value, d.cursor), { extend: key.shift }));
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
// ctrl+u / ctrl+k — delete to line start / end.
|
|
675
|
+
if (key.ctrl && editingKey === 'u') {
|
|
676
|
+
updateDraft(deleteToLineStart);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (key.ctrl && editingKey === 'k') {
|
|
680
|
+
updateDraft(deleteToLineEnd);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
// ctrl+w / alt+backspace — delete previous word.
|
|
684
|
+
if ((key.ctrl && editingKey === 'w') || ((key.ctrl || key.meta) && key.backspace)) {
|
|
685
|
+
updateDraft(deleteBackwardWord);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
// alt+d / ctrl+delete — delete next word.
|
|
689
|
+
if ((key.meta && editingKey === 'd') || (key.ctrl && key.delete)) {
|
|
690
|
+
updateDraft(deleteForwardWord);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (key.backspace) {
|
|
695
|
+
updateDraft((d) => {
|
|
696
|
+
if (selectionRange(d)) return deleteSelectedText(d);
|
|
697
|
+
if (d.cursor <= 0) return d;
|
|
698
|
+
const start = previousOffset(d.value, d.cursor);
|
|
699
|
+
return {
|
|
700
|
+
value: d.value.slice(0, start) + d.value.slice(d.cursor),
|
|
701
|
+
cursor: start,
|
|
702
|
+
selectionAnchor: null,
|
|
703
|
+
};
|
|
704
|
+
});
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (key.delete) {
|
|
709
|
+
updateDraft((d) => {
|
|
710
|
+
if (selectionRange(d)) return deleteSelectedText(d);
|
|
711
|
+
if (d.cursor >= d.value.length) return d;
|
|
712
|
+
const end = nextOffset(d.value, d.cursor);
|
|
713
|
+
return {
|
|
714
|
+
value: d.value.slice(0, d.cursor) + d.value.slice(end),
|
|
715
|
+
cursor: d.cursor,
|
|
716
|
+
selectionAnchor: null,
|
|
717
|
+
};
|
|
718
|
+
});
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Printable input (ignore other control keys). Strip any embedded SGR mouse
|
|
723
|
+
// sequences as a belt-and-suspenders guard (the early return above catches
|
|
724
|
+
// whole-sequence inputs; this removes partials that rode in with real text).
|
|
725
|
+
const printable = rawInput
|
|
726
|
+
.replace(/(?:\x1b)?\[<\d+;\d+;\d+[Mm]/g, '')
|
|
727
|
+
.replace(/[\r\n]/g, '');
|
|
728
|
+
if (printable && !key.ctrl && !key.meta) {
|
|
729
|
+
updateDraft((d) => insertText(d, printable));
|
|
730
|
+
}
|
|
731
|
+
}, { isActive: isRawModeSupported && !disabled });
|
|
732
|
+
|
|
733
|
+
// Mark the text-box node with a cursor-anchor FUNCTION. Forked ink calls it
|
|
734
|
+
// during renderNodeToOutput — AFTER yoga layout is final and (crucially)
|
|
735
|
+
// during the same onRender that paints the new text. The function reads the
|
|
736
|
+
// latest caret from refs (synced every render below), so the cursor can never
|
|
737
|
+
// be stale: an earlier object-anchor set in a layout effect was always one
|
|
738
|
+
// keystroke behind, because ink's reconciler runs onRender inside
|
|
739
|
+
// resetAfterCommit BEFORE React layout effects — that lag made the 2nd+ char
|
|
740
|
+
// appear to land behind the caret (the observed scramble). Computing inside
|
|
741
|
+
// the fork, from the real layout + current refs, fixes that by construction.
|
|
742
|
+
cursorEnabledRef.current = !disabled && isRawModeSupported;
|
|
555
743
|
installCursorAnchor();
|
|
556
744
|
|
|
557
745
|
useLayoutEffect(() => {
|
|
@@ -564,15 +752,16 @@ export function PromptInput({
|
|
|
564
752
|
if (disabled || !isRawModeSupported) return;
|
|
565
753
|
queueMicrotask(flushImmediate);
|
|
566
754
|
}, [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).
|
|
755
|
+
|
|
756
|
+
// Trailing space cell so the caret at end-of-input has a rendered cell to sit
|
|
757
|
+
// on (kept visually blank — no synthetic underline).
|
|
570
758
|
const displayValue = mask ? value.replace(/[^\n]/g, '*') : value;
|
|
571
759
|
const renderedValue = renderSelectedText(displayValue, selectionRange(draft), cursor === value.length);
|
|
572
760
|
const hintMeta = hintStyle(hintTone);
|
|
573
761
|
|
|
574
762
|
return (
|
|
575
763
|
<Box ref={boxRef} flexDirection="row" flexGrow={1} flexShrink={1} backgroundColor={theme.background}>
|
|
764
|
+
<Box width={IME_LEFT_GUARD_COLUMNS} flexShrink={0} backgroundColor={theme.background} />
|
|
576
765
|
<Text color={theme.text} wrap="hard">{renderedValue}</Text>
|
|
577
766
|
{!value && hint ? (
|
|
578
767
|
<Box marginLeft={-1}>
|