mixdog 0.9.0 → 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 +10 -3
- 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/session-ingest-smoke.mjs +2 -2
- 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/mixdog-debug.cjs +0 -22
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +34 -56
- package/src/mixdog-session-runtime.mjs +710 -319
- 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 -4
- 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 +359 -106
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
- 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 +86 -30
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
- 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 +265 -1
- 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/bash-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
- 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 -1254
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +54 -3
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/session-discovery.mjs +0 -4
- package/src/runtime/channels/lib/telegram-format.mjs +283 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/tool-defs.mjs +1 -1
- 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 +187 -43
- 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/llm-worker-host.mjs +0 -4
- 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-ops-policy.mjs +0 -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/runtime-fetcher.mjs +43 -18
- package/src/runtime/memory/lib/session-ingest.mjs +116 -7
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +6 -3
- package/src/runtime/search/index.mjs +2 -7
- 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 +0 -1
- 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/child-spawn-gate.mjs +0 -6
- 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/seeds.mjs +1 -11
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2137 -750
- 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 +146 -9
- 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 +177 -100
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +312 -40
- package/src/tui/components/tool-output-format.test.mjs +180 -1
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +7324 -2393
- 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 +354 -142
- package/src/tui/markdown/format-token.test.mjs +155 -17
- 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 -647
- 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-lib.mjs +0 -623
- 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 +154 -20
- 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/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/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- 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,
|
|
@@ -52,6 +52,9 @@ function hintStyle(tone) {
|
|
|
52
52
|
// paddingX of 1, so the typing start can sit directly against that padding
|
|
53
53
|
// without an extra guard column.
|
|
54
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;
|
|
55
58
|
|
|
56
59
|
function insertText(draft, input) {
|
|
57
60
|
if (!input) return draft;
|
|
@@ -93,14 +96,36 @@ function draftStateEqual(a, b) {
|
|
|
93
96
|
);
|
|
94
97
|
}
|
|
95
98
|
|
|
96
|
-
|
|
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) {
|
|
97
107
|
const text = String(input ?? '');
|
|
98
108
|
const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
|
|
99
109
|
if (!body) return false;
|
|
100
110
|
const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
|
|
101
|
-
if (kitty) return ((Number(kitty[1]) - 1) &
|
|
111
|
+
if (kitty) return ((Number(kitty[1]) - 1) & MODIFIED_ENTER_SHIFT_OR_CTRL) !== 0;
|
|
102
112
|
const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
|
|
103
|
-
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));
|
|
104
129
|
}
|
|
105
130
|
|
|
106
131
|
export function PromptInput({
|
|
@@ -124,6 +149,7 @@ export function PromptInput({
|
|
|
124
149
|
onRestoreQueued,
|
|
125
150
|
onHistoryNavigate,
|
|
126
151
|
onPasteText,
|
|
152
|
+
onVoiceToggle,
|
|
127
153
|
selectionRef,
|
|
128
154
|
valueRef,
|
|
129
155
|
boxRectRef,
|
|
@@ -146,6 +172,7 @@ export function PromptInput({
|
|
|
146
172
|
const cursorEnabledRef = useRef(false); // latest enabled state, read by the anchor fn at render time
|
|
147
173
|
const contentWidthRef = useRef(80);
|
|
148
174
|
const preferredColumnRef = useRef(null);
|
|
175
|
+
const mouseExtendCoalesceRef = useRef({ pendingNext: null, timer: null, t: 0 });
|
|
149
176
|
const { value, cursor } = draft;
|
|
150
177
|
draftRef.current = draft;
|
|
151
178
|
if (selectionRef) {
|
|
@@ -237,6 +264,49 @@ export function PromptInput({
|
|
|
237
264
|
commitDraft(fn(draftRef.current), options);
|
|
238
265
|
};
|
|
239
266
|
|
|
267
|
+
const cancelMouseExtendCoalesce = () => {
|
|
268
|
+
const state = mouseExtendCoalesceRef.current;
|
|
269
|
+
if (state.timer) {
|
|
270
|
+
clearTimeout(state.timer);
|
|
271
|
+
state.timer = null;
|
|
272
|
+
}
|
|
273
|
+
state.pendingNext = null;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const queueMouseExtendCommit = (next, immediate = false) => {
|
|
277
|
+
if (immediate) {
|
|
278
|
+
cancelMouseExtendCoalesce();
|
|
279
|
+
commitDraft(next);
|
|
280
|
+
mouseExtendCoalesceRef.current.t = Date.now();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (draftStateEqual(draftRef.current, next)) {
|
|
284
|
+
cancelMouseExtendCoalesce();
|
|
285
|
+
commitDraft(next);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const state = mouseExtendCoalesceRef.current;
|
|
289
|
+
state.pendingNext = next;
|
|
290
|
+
const now = Date.now();
|
|
291
|
+
const elapsed = now - state.t;
|
|
292
|
+
if (elapsed >= MOUSE_EXTEND_COALESCE_MS) {
|
|
293
|
+
cancelMouseExtendCoalesce();
|
|
294
|
+
state.t = now;
|
|
295
|
+
commitDraft(next);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (state.timer) return;
|
|
299
|
+
state.timer = setTimeout(() => {
|
|
300
|
+
const current = mouseExtendCoalesceRef.current;
|
|
301
|
+
const pending = current.pendingNext;
|
|
302
|
+
current.timer = null;
|
|
303
|
+
current.pendingNext = null;
|
|
304
|
+
current.t = Date.now();
|
|
305
|
+
if (pending) commitDraft(pending);
|
|
306
|
+
}, Math.max(1, MOUSE_EXTEND_COALESCE_MS - elapsed));
|
|
307
|
+
state.timer.unref?.();
|
|
308
|
+
};
|
|
309
|
+
|
|
240
310
|
// [mixdog] Mouse drag-selection driver. App's single mouse handler maps a
|
|
241
311
|
// click/drag cell over the prompt box to an edit offset and calls these so the
|
|
242
312
|
// SAME selectionAnchor/cursor engine that keyboard Shift-selection uses paints
|
|
@@ -246,22 +316,36 @@ export function PromptInput({
|
|
|
246
316
|
mouseSelectionRef.current = {
|
|
247
317
|
offsetAtCell: (row, col) => offsetAtCell(draftRef.current.value, row, col, contentWidthRef.current),
|
|
248
318
|
anchorAt: (offset) => {
|
|
319
|
+
cancelMouseExtendCoalesce();
|
|
249
320
|
const value = draftRef.current.value;
|
|
250
321
|
const off = Math.max(0, Math.min(value.length, Math.floor(Number(offset) || 0)));
|
|
251
322
|
commitDraft({ ...draftRef.current, cursor: off, selectionAnchor: off });
|
|
323
|
+
mouseExtendCoalesceRef.current.t = Date.now();
|
|
252
324
|
},
|
|
253
|
-
extendTo: (offset) => {
|
|
325
|
+
extendTo: (offset, immediate = false) => {
|
|
326
|
+
if (offset == null) {
|
|
327
|
+
if (immediate) cancelMouseExtendCoalesce();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
254
330
|
const d = draftRef.current;
|
|
255
331
|
const off = Math.max(0, Math.min(d.value.length, Math.floor(Number(offset) || 0)));
|
|
256
332
|
const anchor = Number.isFinite(d.selectionAnchor) ? d.selectionAnchor : d.cursor;
|
|
257
|
-
|
|
333
|
+
queueMouseExtendCommit({ ...d, cursor: off, selectionAnchor: anchor }, immediate);
|
|
258
334
|
},
|
|
259
335
|
clear: () => {
|
|
336
|
+
cancelMouseExtendCoalesce();
|
|
260
337
|
if (selectionRange(draftRef.current)) commitDraft(clearSelection(draftRef.current));
|
|
261
338
|
},
|
|
262
339
|
};
|
|
263
340
|
}
|
|
264
341
|
|
|
342
|
+
useEffect(() => () => {
|
|
343
|
+
const state = mouseExtendCoalesceRef.current;
|
|
344
|
+
if (state.timer) clearTimeout(state.timer);
|
|
345
|
+
state.timer = null;
|
|
346
|
+
state.pendingNext = null;
|
|
347
|
+
}, []);
|
|
348
|
+
|
|
265
349
|
const moveDraftVertically = (direction, { extend = false } = {}) => {
|
|
266
350
|
const current = draftRef.current;
|
|
267
351
|
const moved = verticalOffset(
|
|
@@ -375,6 +459,17 @@ export function PromptInput({
|
|
|
375
459
|
return;
|
|
376
460
|
}
|
|
377
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
|
+
|
|
378
473
|
const rawUpArrow = rawInput === '\x1b[A' || rawInput === '\x1bOA' || rawInput === '[A' || rawInput === 'OA';
|
|
379
474
|
const rawDownArrow = rawInput === '\x1b[B' || rawInput === '\x1bOB' || rawInput === '[B' || rawInput === 'OB';
|
|
380
475
|
// Shift+Arrow modifier sequences (xterm `\x1b[1;2<dir>`, rxvt `\x1b[<dir>`
|
|
@@ -391,9 +486,33 @@ export function PromptInput({
|
|
|
391
486
|
const lineBreakIndex = rawInput.search(/[\r\n]/);
|
|
392
487
|
const rawEnter = rawInput === '\r' || rawInput === '\n' || rawInput === '\r\n';
|
|
393
488
|
const trailingEnterPrefix = singleTrailingLineBreakPrefix(rawInput);
|
|
394
|
-
const rawCtrlEnter =
|
|
489
|
+
const rawCtrlEnter = isModifiedEnterSequence(rawInput);
|
|
395
490
|
const modifiedLineBreak = key.shift || key.meta || key.ctrl || rawCtrlEnter;
|
|
396
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
|
+
|
|
397
516
|
const pasteFallback = lineBreakIndex !== -1 && trailingEnterPrefix === null && !rawEnter && (rawInput.length > 1 || !key.return);
|
|
398
517
|
if (pasteFallback) {
|
|
399
518
|
handleExternalPaste(rawInput, { source: 'paste-fallback' });
|
|
@@ -658,6 +777,18 @@ export function PromptInput({
|
|
|
658
777
|
return;
|
|
659
778
|
}
|
|
660
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
|
+
|
|
661
792
|
// Printable input (ignore other control keys). Strip any embedded SGR mouse
|
|
662
793
|
// sequences as a belt-and-suspenders guard (the early return above catches
|
|
663
794
|
// whole-sequence inputs; this removes partials that rode in with real text).
|
|
@@ -678,6 +809,12 @@ export function PromptInput({
|
|
|
678
809
|
// resetAfterCommit BEFORE React layout effects — that lag made the 2nd+ char
|
|
679
810
|
// appear to land behind the caret (the observed scramble). Computing inside
|
|
680
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.
|
|
681
818
|
cursorEnabledRef.current = !disabled && isRawModeSupported;
|
|
682
819
|
installCursorAnchor();
|
|
683
820
|
|
|
@@ -699,8 +836,8 @@ export function PromptInput({
|
|
|
699
836
|
const hintMeta = hintStyle(hintTone);
|
|
700
837
|
|
|
701
838
|
return (
|
|
702
|
-
<Box ref={boxRef} flexDirection="row" flexGrow={1} flexShrink={1} backgroundColor={
|
|
703
|
-
<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()} />
|
|
704
841
|
<Text color={theme.text} wrap="hard">{renderedValue}</Text>
|
|
705
842
|
{!value && hint ? (
|
|
706
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>
|