mixdog 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -1
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +306 -66
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +4 -2
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +4 -2
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/rules-builder.cjs +32 -54
- package/src/mixdog-session-runtime.mjs +710 -318
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +12 -2
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +7 -8
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +7 -0
- package/src/rules/shared/01-tool.md +17 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +332 -57
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +59 -32
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +78 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +202 -98
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +183 -20
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +15 -9
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
- package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
- package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +441 -1224
- package/src/runtime/channels/lib/config.mjs +54 -2
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/output-forwarder.mjs +80 -67
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +283 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +184 -19
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory.mjs +101 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/session-ingest.mjs +107 -0
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +6 -3
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +129 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/standalone/agent-tool.mjs +255 -109
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +8 -291
- package/src/standalone/explore-tool.mjs +2 -2
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/provider-admin.mjs +11 -0
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2096 -732
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +83 -7
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +170 -98
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +159 -21
- package/src/tui/components/tool-output-format.test.mjs +87 -0
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +6965 -2391
- package/src/tui/engine.mjs +287 -126
- package/src/tui/index.jsx +117 -7
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +453 -0
- package/src/tui/markdown/format-token.mjs +129 -76
- package/src/tui/markdown/format-token.test.mjs +61 -19
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +0 -11
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -657
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +81 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +26 -27
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +39 -12
- package/src/workflows/sequential/WORKFLOW.md +46 -0
- package/src/workflows/solo/WORKFLOW.md +7 -0
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +100 -12
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import React, { useEffect, useLayoutEffect, useState, useRef } from 'react';
|
|
19
19
|
import { Box, Text, useInput, usePaste, useStdin } from 'ink';
|
|
20
20
|
import stringWidth from 'string-width';
|
|
21
|
-
import { theme } from '../theme.mjs';
|
|
21
|
+
import { theme, surfaceBackground } from '../theme.mjs';
|
|
22
22
|
import {
|
|
23
23
|
caretPosition,
|
|
24
24
|
clearSelection,
|
|
@@ -96,14 +96,36 @@ function draftStateEqual(a, b) {
|
|
|
96
96
|
);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
// Recognize a MODIFIED Enter delivered via the kitty keyboard protocol
|
|
100
|
+
// (\x1b[13;<mod>u) or modifyOtherKeys (\x1b[27;<mod>;13~). The xterm modifier
|
|
101
|
+
// param is (1 + bitmask) where the bitmask bits are shift=1, alt=2, ctrl=4. We
|
|
102
|
+
// treat Ctrl+Enter AND Shift+Enter (the two common "insert newline" chords) as a
|
|
103
|
+
// newline, so we match when the shift OR ctrl bit is set. Ctrl+J is handled
|
|
104
|
+
// separately as the protocol-independent fallback.
|
|
105
|
+
const MODIFIED_ENTER_SHIFT_OR_CTRL = 1 | 4;
|
|
106
|
+
function isModifiedEnterSequence(input) {
|
|
100
107
|
const text = String(input ?? '');
|
|
101
108
|
const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
|
|
102
109
|
if (!body) return false;
|
|
103
110
|
const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
|
|
104
|
-
if (kitty) return ((Number(kitty[1]) - 1) &
|
|
111
|
+
if (kitty) return ((Number(kitty[1]) - 1) & MODIFIED_ENTER_SHIFT_OR_CTRL) !== 0;
|
|
105
112
|
const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
|
|
106
|
-
return Boolean(modifyOtherKeys && (((Number(modifyOtherKeys[1]) - 1) &
|
|
113
|
+
return Boolean(modifyOtherKeys && (((Number(modifyOtherKeys[1]) - 1) & MODIFIED_ENTER_SHIFT_OR_CTRL) !== 0));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Recognize ANY modified Enter (any modifier bitmask, e.g. Alt+Enter \x1b[13;3u
|
|
117
|
+
// / \x1b[27;3;13~). Used to CONSUME modified-Enter sequences we don't map to a
|
|
118
|
+
// newline (Alt-only, etc.) so they aren't typed into the prompt as raw CSI text
|
|
119
|
+
// under modifyOtherKeys. Plain Enter (mod param = 1, bitmask 0) is intentionally
|
|
120
|
+
// NOT matched, so it still submits.
|
|
121
|
+
function isAnyModifiedEnterSequence(input) {
|
|
122
|
+
const text = String(input ?? '');
|
|
123
|
+
const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
|
|
124
|
+
if (!body) return false;
|
|
125
|
+
const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
|
|
126
|
+
if (kitty) return (Number(kitty[1]) - 1) !== 0;
|
|
127
|
+
const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
|
|
128
|
+
return Boolean(modifyOtherKeys && ((Number(modifyOtherKeys[1]) - 1) !== 0));
|
|
107
129
|
}
|
|
108
130
|
|
|
109
131
|
export function PromptInput({
|
|
@@ -127,6 +149,7 @@ export function PromptInput({
|
|
|
127
149
|
onRestoreQueued,
|
|
128
150
|
onHistoryNavigate,
|
|
129
151
|
onPasteText,
|
|
152
|
+
onVoiceToggle,
|
|
130
153
|
selectionRef,
|
|
131
154
|
valueRef,
|
|
132
155
|
boxRectRef,
|
|
@@ -436,6 +459,17 @@ export function PromptInput({
|
|
|
436
459
|
return;
|
|
437
460
|
}
|
|
438
461
|
|
|
462
|
+
// Safety net: drop CSI-private replies/fragments like \x1b[?<n>u / \x1b[?...c
|
|
463
|
+
// (escape may be stripped → `[?7u` / `[?1;0c`). We no longer query the
|
|
464
|
+
// terminal (enables are written unconditionally at raw-mode-on), so these
|
|
465
|
+
// should not normally appear — but a terminal that volunteers such a report
|
|
466
|
+
// must never type it into the prompt. The required `?` after `[` means this
|
|
467
|
+
// never matches a real kitty KEY event (those are \x1b[<codepoint>;<mods>u,
|
|
468
|
+
// no `?`); the optional final byte also discards any partial fragment.
|
|
469
|
+
if (/^(?:\x1b)?\[\?[\d;]*[uc]?$/.test(rawInput)) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
439
473
|
const rawUpArrow = rawInput === '\x1b[A' || rawInput === '\x1bOA' || rawInput === '[A' || rawInput === 'OA';
|
|
440
474
|
const rawDownArrow = rawInput === '\x1b[B' || rawInput === '\x1bOB' || rawInput === '[B' || rawInput === 'OB';
|
|
441
475
|
// Shift+Arrow modifier sequences (xterm `\x1b[1;2<dir>`, rxvt `\x1b[<dir>`
|
|
@@ -452,9 +486,33 @@ export function PromptInput({
|
|
|
452
486
|
const lineBreakIndex = rawInput.search(/[\r\n]/);
|
|
453
487
|
const rawEnter = rawInput === '\r' || rawInput === '\n' || rawInput === '\r\n';
|
|
454
488
|
const trailingEnterPrefix = singleTrailingLineBreakPrefix(rawInput);
|
|
455
|
-
const rawCtrlEnter =
|
|
489
|
+
const rawCtrlEnter = isModifiedEnterSequence(rawInput);
|
|
456
490
|
const modifiedLineBreak = key.shift || key.meta || key.ctrl || rawCtrlEnter;
|
|
457
491
|
|
|
492
|
+
// Ctrl+J is the protocol-INDEPENDENT newline that works on every terminal.
|
|
493
|
+
// • Legacy / modifyOtherKeys terminals: Ctrl+J is a lone '\n' (0x0A). A real
|
|
494
|
+
// Enter is CR, which ink marks key.return (name 'return'); a lone '\n'
|
|
495
|
+
// arrives as name 'enter' with key.return false. A multi-char paste that
|
|
496
|
+
// contains '\n' is length > 1 (handled by the paste paths below).
|
|
497
|
+
// • Kitty protocol active: Ctrl+J arrives as \x1b[106;5u, which ink decodes
|
|
498
|
+
// to input 'j' with key.ctrl set.
|
|
499
|
+
// Either way → insert a newline. This MUST run before the trailing-newline/
|
|
500
|
+
// submit paths, since singleTrailingLineBreakPrefix('\n') returns '' (not
|
|
501
|
+
// null) and would otherwise route a bare Ctrl+J to submit.
|
|
502
|
+
if ((rawInput === '\n' && !key.return) || (key.ctrl && inputKey === 'j')) {
|
|
503
|
+
updateDraft((d) => replaceSelection(d, '\n'));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// A modified Enter that is NOT a newline chord (e.g. Alt+Enter \x1b[13;3u or
|
|
508
|
+
// \x1b[27;3;13~). isModifiedEnterSequence already handled shift/ctrl above
|
|
509
|
+
// (→ newline); here we CONSUME any other modified Enter so its raw CSI bytes
|
|
510
|
+
// never type into the prompt under modifyOtherKeys. Plain Enter (mod=1) is
|
|
511
|
+
// not matched and still submits below.
|
|
512
|
+
if (!rawCtrlEnter && isAnyModifiedEnterSequence(rawInput)) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
458
516
|
const pasteFallback = lineBreakIndex !== -1 && trailingEnterPrefix === null && !rawEnter && (rawInput.length > 1 || !key.return);
|
|
459
517
|
if (pasteFallback) {
|
|
460
518
|
handleExternalPaste(rawInput, { source: 'paste-fallback' });
|
|
@@ -719,6 +777,18 @@ export function PromptInput({
|
|
|
719
777
|
return;
|
|
720
778
|
}
|
|
721
779
|
|
|
780
|
+
// Ctrl+Space voice-record toggle. Two encodings observed across
|
|
781
|
+
// terminals: a lone NUL byte (legacy Ctrl+Space, many terminals send
|
|
782
|
+
// 0x00 directly) or the kitty CSI-u form `\x1b[32;5u` (codepoint 32
|
|
783
|
+
// = space, modifier 5 = 1+4 = ctrl). Consumed here — never typed into
|
|
784
|
+
// the prompt — and forwarded to the App-level recorder state machine
|
|
785
|
+
// via onVoiceToggle; a no-op when the prop isn't wired (e.g. embedded
|
|
786
|
+
// pickers) so this never throws.
|
|
787
|
+
if (rawInput === '\x00' || /^(?:\x1b)?\[32;5u$/.test(rawInput)) {
|
|
788
|
+
onVoiceToggle?.();
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
|
|
722
792
|
// Printable input (ignore other control keys). Strip any embedded SGR mouse
|
|
723
793
|
// sequences as a belt-and-suspenders guard (the early return above catches
|
|
724
794
|
// whole-sequence inputs; this removes partials that rode in with real text).
|
|
@@ -739,6 +809,12 @@ export function PromptInput({
|
|
|
739
809
|
// resetAfterCommit BEFORE React layout effects — that lag made the 2nd+ char
|
|
740
810
|
// appear to land behind the caret (the observed scramble). Computing inside
|
|
741
811
|
// the fork, from the real layout + current refs, fixes that by construction.
|
|
812
|
+
// Park the hardware cursor only when the prompt is a real, active edit target.
|
|
813
|
+
// The anchor stays enabled during an active turn too: suppressing it while
|
|
814
|
+
// the draft was empty (an earlier guard against a one-frame row-off paint
|
|
815
|
+
// when the transcript height changed mid-turn) made the cursor vanish for
|
|
816
|
+
// the whole turn after Enter, which reads as "the caret disappeared". A
|
|
817
|
+
// momentary one-frame drift is the lesser evil versus a missing caret.
|
|
742
818
|
cursorEnabledRef.current = !disabled && isRawModeSupported;
|
|
743
819
|
installCursorAnchor();
|
|
744
820
|
|
|
@@ -760,8 +836,8 @@ export function PromptInput({
|
|
|
760
836
|
const hintMeta = hintStyle(hintTone);
|
|
761
837
|
|
|
762
838
|
return (
|
|
763
|
-
<Box ref={boxRef} flexDirection="row" flexGrow={1} flexShrink={1} backgroundColor={
|
|
764
|
-
<Box width={IME_LEFT_GUARD_COLUMNS} flexShrink={0} backgroundColor={
|
|
839
|
+
<Box ref={boxRef} flexDirection="row" width="100%" flexGrow={1} flexShrink={1} backgroundColor={surfaceBackground()}>
|
|
840
|
+
<Box width={IME_LEFT_GUARD_COLUMNS} flexShrink={0} backgroundColor={surfaceBackground()} />
|
|
765
841
|
<Text color={theme.text} wrap="hard">{renderedValue}</Text>
|
|
766
842
|
{!value && hint ? (
|
|
767
843
|
<Box marginLeft={-1}>
|
|
@@ -22,7 +22,7 @@ export function QueuedCommands({ queued, columns }) {
|
|
|
22
22
|
// Windows auto-wrap/scroll that drifts the alt-screen frame (see UserMessage).
|
|
23
23
|
const bandColumns = Math.max(1, columns - 1);
|
|
24
24
|
return (
|
|
25
|
-
<Box
|
|
25
|
+
<Box flexDirection="column">
|
|
26
26
|
{queued.map((item) => {
|
|
27
27
|
// Truncate to 1 line so the row reservation (queued.length in App.jsx)
|
|
28
28
|
// stays accurate — wrapped text would push the input box off-screen.
|
|
@@ -65,6 +65,8 @@ export function SlashCommandPalette({ commands, selectedIndex = 0, title = 'Comm
|
|
|
65
65
|
const descriptionWidth = Math.max(0, columns - labelWidth - 12);
|
|
66
66
|
// Standard panel rhythm: title row, blank, description/hint row, blank, content.
|
|
67
67
|
const description = truncateText(SLASH_DESCRIPTION, Math.max(0, columns - 4));
|
|
68
|
+
const titleWidth = stringWidth(String(title || ''));
|
|
69
|
+
const help = truncateText(SLASH_HELP, Math.max(0, columns - titleWidth - 7));
|
|
68
70
|
|
|
69
71
|
return (
|
|
70
72
|
<Box flexDirection="column" flexShrink={0} width="100%">
|
|
@@ -76,8 +78,8 @@ export function SlashCommandPalette({ commands, selectedIndex = 0, title = 'Comm
|
|
|
76
78
|
width="100%"
|
|
77
79
|
>
|
|
78
80
|
<Box flexDirection="row" justifyContent="space-between">
|
|
79
|
-
<Text color={theme.panelTitle}>{title}</Text>
|
|
80
|
-
<Text color={theme.subtle}>{
|
|
81
|
+
<Text color={theme.panelTitle} wrap="truncate">{title}</Text>
|
|
82
|
+
<Text color={theme.subtle} wrap="truncate">{help}</Text>
|
|
81
83
|
</Box>
|
|
82
84
|
<Text> </Text>
|
|
83
85
|
<Text color={theme.subtle}>{description || ' '}</Text>
|
|
@@ -103,14 +105,15 @@ export function SlashCommandPalette({ commands, selectedIndex = 0, title = 'Comm
|
|
|
103
105
|
const CommandRow = React.memo(function CommandRow({ command, isSelected, labelWidth, descriptionWidth, query }) {
|
|
104
106
|
const label = truncateText(commandDisplayLabel(command, query), labelWidth);
|
|
105
107
|
const description = truncateText(command.description, descriptionWidth);
|
|
108
|
+
const rowText = isSelected ? theme.selectionText : theme.text;
|
|
106
109
|
|
|
107
110
|
return (
|
|
108
|
-
<Box flexDirection="row" width="100%" backgroundColor={isSelected ? theme.
|
|
109
|
-
<Text color={
|
|
111
|
+
<Box flexDirection="row" width="100%" backgroundColor={isSelected ? theme.selectionBackground : undefined}>
|
|
112
|
+
<Text color={rowText}>
|
|
110
113
|
{padCells(label, labelWidth)}
|
|
111
114
|
</Text>
|
|
112
115
|
{description ? (
|
|
113
|
-
<Text color={
|
|
116
|
+
<Text color={rowText}>
|
|
114
117
|
{' '}
|
|
115
118
|
{description}
|
|
116
119
|
</Text>
|
|
@@ -58,8 +58,6 @@ const MODE_VERBS = {
|
|
|
58
58
|
responding: ['Responding', 'Composing', 'Writing', 'Wrapping up'],
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
const ERROR_RED = { r: 171, g: 43, b: 63 };
|
|
62
|
-
|
|
63
61
|
function interpolateColor(a, b, t) {
|
|
64
62
|
return {
|
|
65
63
|
r: Math.round(a.r + (b.r - a.r) * t),
|
|
@@ -117,6 +115,7 @@ function spinnerRgb() {
|
|
|
117
115
|
SPINNER_GLYPH_RGB: parseRgb(theme.spinnerGlyph) ?? { r: 240, g: 240, b: 240 },
|
|
118
116
|
THINKING_INACTIVE: parseRgb(theme.thinkingBase) ?? parseRgb(theme.thinkingAccent) ?? { r: 153, g: 153, b: 153 },
|
|
119
117
|
THINKING_SHIMMER: parseRgb(theme.thinkingGlow) ?? { r: 255, g: 205, b: 175 },
|
|
118
|
+
STALL_RGB: parseRgb(theme.error) ?? { r: 171, g: 43, b: 63 },
|
|
120
119
|
};
|
|
121
120
|
}
|
|
122
121
|
|
|
@@ -172,7 +171,7 @@ function tokenModeGlyph(mode) {
|
|
|
172
171
|
|
|
173
172
|
export function Spinner({ verb = 'Working', startedAt, outputTokens = 0, tokens = 0, thinking = false, thinkingActiveSince = 0, mode = 'responding', columns = 80, marginTop = 1 }) {
|
|
174
173
|
useAnimation({ interval: FRAME_MS });
|
|
175
|
-
const { TEXT_RGB, SHIMMER_RGB, SPINNER_GLYPH_RGB, THINKING_INACTIVE, THINKING_SHIMMER } = spinnerRgb();
|
|
174
|
+
const { TEXT_RGB, SHIMMER_RGB, SPINNER_GLYPH_RGB, THINKING_INACTIVE, THINKING_SHIMMER, STALL_RGB } = spinnerRgb();
|
|
176
175
|
const now = Date.now();
|
|
177
176
|
const elapsedMs = startedAt ? Math.max(0, now - startedAt) : 0;
|
|
178
177
|
const frame = Math.floor(elapsedMs / FRAME_MS);
|
|
@@ -222,17 +221,18 @@ export function Spinner({ verb = 'Working', startedAt, outputTokens = 0, tokens
|
|
|
222
221
|
const glyphColor = stalledIntensity > 0
|
|
223
222
|
? toRgbString(interpolateColor(
|
|
224
223
|
SPINNER_GLYPH_RGB,
|
|
225
|
-
|
|
224
|
+
STALL_RGB,
|
|
226
225
|
stalledIntensity
|
|
227
226
|
))
|
|
228
227
|
: theme.spinnerGlyph;
|
|
229
228
|
|
|
230
229
|
// --- Verb shimmer (traveling highlight) ---
|
|
231
|
-
if (
|
|
232
|
-
displayVerbModeRef.current = mode;
|
|
230
|
+
if (!displayVerbRef.current) {
|
|
233
231
|
displayVerbRef.current = stableModeVerb(mode, verb);
|
|
234
232
|
nextVerbCheckRef.current = nextVerbCheckAt(now);
|
|
235
|
-
}
|
|
233
|
+
}
|
|
234
|
+
displayVerbModeRef.current = mode;
|
|
235
|
+
if (now >= nextVerbCheckRef.current) {
|
|
236
236
|
displayVerbRef.current = chooseNextVerb(mode, verb, displayVerbRef.current);
|
|
237
237
|
nextVerbCheckRef.current = nextVerbCheckAt(now);
|
|
238
238
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import React, { useEffect, useRef, useState } from 'react';
|
|
10
10
|
import { Box, Text } from 'ink';
|
|
11
11
|
import { displayModelName, shortenModelName } from '../../ui/model-display.mjs';
|
|
12
|
-
import { theme } from '../theme.mjs';
|
|
12
|
+
import { theme, surfaceBackground } from '../theme.mjs';
|
|
13
13
|
import {
|
|
14
14
|
normalizeStatuslineAnsi,
|
|
15
15
|
statuslineFooterCacheKey,
|
|
@@ -33,6 +33,20 @@ function resetStatuslineModuleLoad() {
|
|
|
33
33
|
|
|
34
34
|
const RESET = '\x1b[0m';
|
|
35
35
|
const STATUSLINE_RENDER_DEBOUNCE_MS = 150;
|
|
36
|
+
/** Background import of statusline.mjs before the +1000ms full-render tick. */
|
|
37
|
+
const STATUSLINE_MODULE_PREWARM_DELAY_MS = 300;
|
|
38
|
+
|
|
39
|
+
let statuslineModulePrewarmScheduled = false;
|
|
40
|
+
|
|
41
|
+
function scheduleStatuslineModulePrewarm() {
|
|
42
|
+
if (statuslineModulePrewarmScheduled) return;
|
|
43
|
+
statuslineModulePrewarmScheduled = true;
|
|
44
|
+
const timer = setTimeout(() => {
|
|
45
|
+
loadStatuslineModule().catch(() => {});
|
|
46
|
+
}, STATUSLINE_MODULE_PREWARM_DELAY_MS);
|
|
47
|
+
timer.unref?.();
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
const STATUSLINE_BOOT_FULL_DELAY_MS = 1000;
|
|
37
51
|
const STATUSLINE_BOOT_FULL_DELAY_ACTIVE_MS = 2200;
|
|
38
52
|
const STATUSLINE_BOOT_FULL_RETRY_MS = 2000;
|
|
@@ -46,7 +60,7 @@ function isTerminalStatus(statusText) {
|
|
|
46
60
|
|
|
47
61
|
function hasRunningStatuslineWorkers(agentWorkers = [], agentJobs = []) {
|
|
48
62
|
for (const worker of Array.isArray(agentWorkers) ? agentWorkers : []) {
|
|
49
|
-
const tag = String(worker?.tag || worker?.
|
|
63
|
+
const tag = String(worker?.tag || worker?.agent || worker?.name || '').trim();
|
|
50
64
|
if (tag && !isTerminalStatus(worker?.stage || worker?.status)) return true;
|
|
51
65
|
}
|
|
52
66
|
for (const job of Array.isArray(agentJobs) ? agentJobs : []) {
|
|
@@ -143,8 +157,8 @@ function localContextPct({
|
|
|
143
157
|
: (localNum(contextWindow) > 0
|
|
144
158
|
? localNum(contextWindow)
|
|
145
159
|
: (localNum(rawContextWindow) > 0 ? localNum(rawContextWindow) : 200_000)));
|
|
146
|
-
|
|
147
|
-
const window =
|
|
160
|
+
// autoCompactTokenLimit is the compaction trigger only; pct uses the boundary window.
|
|
161
|
+
const window = baseWindow;
|
|
148
162
|
const s = stats && typeof stats === 'object' ? stats : {};
|
|
149
163
|
const source = String(s.currentContextSource || '').toLowerCase();
|
|
150
164
|
const estimated = localNum(s.currentEstimatedContextTokens);
|
|
@@ -190,11 +204,6 @@ const LOCAL_WORKER_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '
|
|
|
190
204
|
// render stay in sync (no flicker).
|
|
191
205
|
const LOCAL_L2_SPINNER_FRAME_MS = 120;
|
|
192
206
|
|
|
193
|
-
function localWorkerSpinnerFrame(now = Date.now()) {
|
|
194
|
-
const index = Math.floor(now / 160) % LOCAL_WORKER_SPINNER_FRAMES.length;
|
|
195
|
-
return LOCAL_WORKER_SPINNER_FRAMES[index] || LOCAL_WORKER_SPINNER_FRAMES[0];
|
|
196
|
-
}
|
|
197
|
-
|
|
198
207
|
function localL2SpinnerFrame(now = Date.now()) {
|
|
199
208
|
const index = Math.floor(now / LOCAL_L2_SPINNER_FRAME_MS) % LOCAL_WORKER_SPINNER_FRAMES.length;
|
|
200
209
|
return LOCAL_WORKER_SPINNER_FRAMES[index] || LOCAL_WORKER_SPINNER_FRAMES[0];
|
|
@@ -224,13 +233,13 @@ function localFormatElapsed(ms) {
|
|
|
224
233
|
function localRunningWorkerCount(agentWorkers = [], agentJobs = []) {
|
|
225
234
|
const seen = new Set();
|
|
226
235
|
for (const worker of Array.isArray(agentWorkers) ? agentWorkers : []) {
|
|
227
|
-
const tag = String(worker?.tag || worker?.
|
|
236
|
+
const tag = String(worker?.tag || worker?.agent || worker?.name || '').trim();
|
|
228
237
|
if (!tag || isTerminalStatus(worker?.stage || worker?.status)) continue;
|
|
229
238
|
seen.add(tag);
|
|
230
239
|
}
|
|
231
240
|
for (const job of Array.isArray(agentJobs) ? agentJobs : []) {
|
|
232
241
|
if (!/running/i.test(String(job?.status || job?.stage || ''))) continue;
|
|
233
|
-
const tag = String(job?.tag || job?.
|
|
242
|
+
const tag = String(job?.tag || job?.agent || job?.type || job?.task_id || job?.taskId || '').trim();
|
|
234
243
|
if (!tag) continue;
|
|
235
244
|
seen.add(tag);
|
|
236
245
|
}
|
|
@@ -241,14 +250,14 @@ function localRunningWorkerTags(agentWorkers = [], agentJobs = [], limit = 3) {
|
|
|
241
250
|
const tags = [];
|
|
242
251
|
const seen = new Set();
|
|
243
252
|
for (const worker of Array.isArray(agentWorkers) ? agentWorkers : []) {
|
|
244
|
-
const tag = String(worker?.tag || worker?.
|
|
253
|
+
const tag = String(worker?.tag || worker?.agent || worker?.name || '').trim();
|
|
245
254
|
if (!tag || isTerminalStatus(worker?.stage || worker?.status) || seen.has(tag)) continue;
|
|
246
255
|
seen.add(tag);
|
|
247
256
|
tags.push(tag);
|
|
248
257
|
}
|
|
249
258
|
for (const job of Array.isArray(agentJobs) ? agentJobs : []) {
|
|
250
259
|
if (!/running/i.test(String(job?.status || job?.stage || ''))) continue;
|
|
251
|
-
const tag = String(job?.tag || job?.
|
|
260
|
+
const tag = String(job?.tag || job?.agent || job?.type || job?.task_id || job?.taskId || '').trim();
|
|
252
261
|
if (!tag || seen.has(tag)) continue;
|
|
253
262
|
seen.add(tag);
|
|
254
263
|
tags.push(tag);
|
|
@@ -417,12 +426,13 @@ export function normalizeStatusLine(text) {
|
|
|
417
426
|
return normalizeStatuslineAnsi(text, statusColors(), { reset: RESET });
|
|
418
427
|
}
|
|
419
428
|
|
|
420
|
-
function workflowModeLabel(workflow = {}) {
|
|
429
|
+
function workflowModeLabel(workflow = {}, remoteEnabled = false) {
|
|
421
430
|
const name = String(workflow?.name || workflow?.id || 'Default').trim() || 'Default';
|
|
422
|
-
|
|
431
|
+
const base = `${name} Mode`;
|
|
432
|
+
return remoteEnabled === true ? `Remote / ${base}` : base;
|
|
423
433
|
}
|
|
424
434
|
|
|
425
|
-
function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fast, cwd, stats, contextWindow, displayContextWindow = 0, compactBoundaryTokens = 0, autoCompactTokenLimit = 0, rawContextWindow, resizeEpoch, agentRevision = '', agentWorkers = [], agentJobs = [], activeTools = null, initialLine = '', workflow = null, themeEpoch = 0 }) {
|
|
435
|
+
function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fast, cwd, stats, contextWindow, displayContextWindow = 0, compactBoundaryTokens = 0, autoCompactTokenLimit = 0, rawContextWindow, resizeEpoch, agentRevision = '', agentWorkers = [], agentJobs = [], activeTools = null, initialLine = '', workflow = null, remoteEnabled = false, themeEpoch = 0 }) {
|
|
426
436
|
const [line, setLine] = useState(() => normalizeStatusLine(initialLine || localBootStatusLine({
|
|
427
437
|
provider,
|
|
428
438
|
model,
|
|
@@ -474,6 +484,10 @@ function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fas
|
|
|
474
484
|
: '';
|
|
475
485
|
const refreshMs = hasActiveStatuslineWork(line, agentWorkers, agentJobs, activeTools) ? STATUSLINE_ACTIVE_REFRESH_MS : STATUSLINE_REFRESH_MS;
|
|
476
486
|
|
|
487
|
+
useEffect(() => {
|
|
488
|
+
scheduleStatuslineModulePrewarm();
|
|
489
|
+
}, []);
|
|
490
|
+
|
|
477
491
|
useEffect(() => {
|
|
478
492
|
const timer = setInterval(() => {
|
|
479
493
|
setRefreshTick((tick) => (tick + 1) % 1_000_000);
|
|
@@ -634,11 +648,12 @@ function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fas
|
|
|
634
648
|
}, [sessionId, clientHostPid, provider, model, effort, fast, cwd, stats, contextWindow, displayContextWindow, compactBoundaryTokens, autoCompactTokenLimit, rawContextWindow, resizeEpoch, agentRevision, agentWorkers, agentJobs, activeToolsSignature, refreshTick, themeEpoch]);
|
|
635
649
|
|
|
636
650
|
const lines = line ? line.split('\n').slice(0, 2) : [' ', ' '];
|
|
637
|
-
const workflowLabel = workflowModeLabel(workflow);
|
|
638
|
-
// Footer footprint stays 3 rows total
|
|
639
|
-
//
|
|
651
|
+
const workflowLabel = workflowModeLabel(workflow, remoteEnabled);
|
|
652
|
+
// Footer footprint stays 3 rows total (L1 + L2 + one spacer). Keep L1/L2 in
|
|
653
|
+
// the TOP two rows so the statusline is visually attached to the prompt box;
|
|
654
|
+
// the leftover breathing row sits below L2 at the terminal bottom.
|
|
640
655
|
return (
|
|
641
|
-
<Box flexDirection="column" width="100%" height={3} overflow="hidden" paddingLeft={2} backgroundColor={
|
|
656
|
+
<Box flexDirection="column" width="100%" height={3} overflow="hidden" justifyContent="flex-start" paddingLeft={2} backgroundColor={surfaceBackground()}>
|
|
642
657
|
<Box flexDirection="row" width="100%" overflow="hidden">
|
|
643
658
|
<Box flexGrow={1} flexShrink={1} overflow="hidden">
|
|
644
659
|
<Text wrap="truncate">{lines[0] || ' '}</Text>
|
|
@@ -647,7 +662,11 @@ function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fas
|
|
|
647
662
|
<Text color={theme.statusText} wrap="truncate">{workflowLabel}</Text>
|
|
648
663
|
</Box>
|
|
649
664
|
</Box>
|
|
650
|
-
<
|
|
665
|
+
<Box flexDirection="row" width="100%" overflow="hidden">
|
|
666
|
+
<Box flexGrow={1} flexShrink={1} overflow="hidden">
|
|
667
|
+
<Text wrap="truncate">{lines[1] || ' '}</Text>
|
|
668
|
+
</Box>
|
|
669
|
+
</Box>
|
|
651
670
|
</Box>
|
|
652
671
|
);
|
|
653
672
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
5
5
|
import { Box, Text, useInput, usePaste, useStdin } from 'ink';
|
|
6
6
|
import stringWidth from 'string-width';
|
|
7
|
-
import { theme } from '../theme.mjs';
|
|
7
|
+
import { theme, surfaceBackground } from '../theme.mjs';
|
|
8
8
|
import {
|
|
9
9
|
clearSelection,
|
|
10
10
|
deleteBackwardWord,
|
|
@@ -126,14 +126,34 @@ function singleTrailingLineBreakPrefix(text) {
|
|
|
126
126
|
return prefix.includes('\n') ? null : prefix;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
// Recognize a MODIFIED Enter (Ctrl+Enter or Shift+Enter) delivered via the kitty
|
|
130
|
+
// keyboard protocol (\x1b[13;<mod>u) or modifyOtherKeys (\x1b[27;<mod>;13~). The
|
|
131
|
+
// xterm modifier param is (1 + bitmask): shift=1, alt=2, ctrl=4. We match when
|
|
132
|
+
// the shift OR ctrl bit is set so both chords insert a newline. Ctrl+J is the
|
|
133
|
+
// protocol-independent fallback, handled separately.
|
|
134
|
+
const MODIFIED_ENTER_SHIFT_OR_CTRL = 1 | 4;
|
|
135
|
+
function isModifiedEnterSequence(input) {
|
|
130
136
|
const text = String(input ?? '');
|
|
131
137
|
const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
|
|
132
138
|
if (!body) return false;
|
|
133
139
|
const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
|
|
134
|
-
if (kitty) return ((Number(kitty[1]) - 1) &
|
|
140
|
+
if (kitty) return ((Number(kitty[1]) - 1) & MODIFIED_ENTER_SHIFT_OR_CTRL) !== 0;
|
|
135
141
|
const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
|
|
136
|
-
return Boolean(modifyOtherKeys && (((Number(modifyOtherKeys[1]) - 1) &
|
|
142
|
+
return Boolean(modifyOtherKeys && (((Number(modifyOtherKeys[1]) - 1) & MODIFIED_ENTER_SHIFT_OR_CTRL) !== 0));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Recognize ANY modified Enter (any modifier bitmask, e.g. Alt+Enter). Used to
|
|
146
|
+
// CONSUME modified-Enter sequences we don't map to a newline so they aren't
|
|
147
|
+
// typed as raw CSI text under modifyOtherKeys. Plain Enter (mod=1) is NOT
|
|
148
|
+
// matched, so it still submits.
|
|
149
|
+
function isAnyModifiedEnterSequence(input) {
|
|
150
|
+
const text = String(input ?? '');
|
|
151
|
+
const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
|
|
152
|
+
if (!body) return false;
|
|
153
|
+
const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
|
|
154
|
+
if (kitty) return (Number(kitty[1]) - 1) !== 0;
|
|
155
|
+
const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
|
|
156
|
+
return Boolean(modifyOtherKeys && ((Number(modifyOtherKeys[1]) - 1) !== 0));
|
|
137
157
|
}
|
|
138
158
|
|
|
139
159
|
export function TextEntryPanel({
|
|
@@ -227,6 +247,11 @@ export function TextEntryPanel({
|
|
|
227
247
|
const rawSource = String(input ?? '');
|
|
228
248
|
const rawInput = normalizeInput(input);
|
|
229
249
|
if (/(?:\x1b)?\[<\d+;\d+;\d+[Mm]/.test(rawSource)) return;
|
|
250
|
+
// Safety net: drop CSI-private replies/fragments (\x1b[?<n>u / \x1b[?...c).
|
|
251
|
+
// We no longer query the terminal, so these should not normally appear, but
|
|
252
|
+
// a volunteered report must never type into the field. See PromptInput for
|
|
253
|
+
// the full rationale; optional final byte also discards partial fragments.
|
|
254
|
+
if (/^(?:\x1b)?\[\?[\d;]*[uc]?$/.test(rawSource)) return;
|
|
230
255
|
|
|
231
256
|
if (key.escape) {
|
|
232
257
|
if (selectionRange(draftRef.current)) {
|
|
@@ -237,8 +262,27 @@ export function TextEntryPanel({
|
|
|
237
262
|
return;
|
|
238
263
|
}
|
|
239
264
|
const trailingEnterPrefix = singleTrailingLineBreakPrefix(rawInput);
|
|
240
|
-
const rawCtrlEnter =
|
|
265
|
+
const rawCtrlEnter = isModifiedEnterSequence(rawSource) || isModifiedEnterSequence(rawInput);
|
|
241
266
|
const modifiedLineBreak = key.shift || key.meta || key.ctrl || rawCtrlEnter;
|
|
267
|
+
|
|
268
|
+
// Ctrl+J — the protocol-independent newline that works on every terminal.
|
|
269
|
+
// Legacy/modifyOtherKeys: a lone '\n' (real Enter is CR → key.return). Kitty:
|
|
270
|
+
// \x1b[106;5u → input 'j' with key.ctrl. Either → insert a newline. Must
|
|
271
|
+
// precede the trailing-newline/submit paths since
|
|
272
|
+
// singleTrailingLineBreakPrefix('\n') returns '' (not null) and would
|
|
273
|
+
// otherwise route Ctrl+J to submit.
|
|
274
|
+
if ((rawSource === '\n' && !key.return) || (key.ctrl && rawSource.toLowerCase() === 'j')) {
|
|
275
|
+
updateDraft((d) => replaceSelection(d, '\n'));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// A modified Enter that is NOT a newline chord (e.g. Alt+Enter): consume it
|
|
280
|
+
// so its raw CSI bytes don't type into the field under modifyOtherKeys. Plain
|
|
281
|
+
// Enter (mod=1) is not matched and still submits below.
|
|
282
|
+
if (!rawCtrlEnter && (isAnyModifiedEnterSequence(rawSource) || isAnyModifiedEnterSequence(rawInput))) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
242
286
|
const pasteFallback = rawInput.includes('\n') && trailingEnterPrefix === null && (rawInput.length > 1 || !key.return);
|
|
243
287
|
if (pasteFallback) {
|
|
244
288
|
updateDraft((d) => insertText(d, rawInput));
|
|
@@ -256,7 +300,7 @@ export function TextEntryPanel({
|
|
|
256
300
|
updateDraft((d) => replaceSelection(d, '\n'));
|
|
257
301
|
return;
|
|
258
302
|
}
|
|
259
|
-
if (key.return
|
|
303
|
+
if (key.return) {
|
|
260
304
|
if (modifiedLineBreak) {
|
|
261
305
|
updateDraft((d) => replaceSelection(d, '\n'));
|
|
262
306
|
return;
|
|
@@ -432,7 +476,7 @@ export function TextEntryPanel({
|
|
|
432
476
|
<Text> </Text>
|
|
433
477
|
<Text color={theme.subtle}>{hintText || ' '}</Text>
|
|
434
478
|
<Text> </Text>
|
|
435
|
-
<Box ref={boxRef} flexDirection="row" width="100%" backgroundColor={
|
|
479
|
+
<Box ref={boxRef} flexDirection="row" width="100%" backgroundColor={surfaceBackground()}>
|
|
436
480
|
<Text color={theme.inactive}>{promptLabel}</Text>
|
|
437
481
|
<Text color={theme.text} wrap="truncate">{renderedValue}</Text>
|
|
438
482
|
</Box>
|