mixdog 0.9.94 → 0.9.96
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/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/MIT.txt +56 -0
- package/LICENSES/codex-NOTICE.txt +6 -0
- package/NOTICE.md +109 -0
- package/package.json +26 -13
- package/scripts/.tmp-coverage-report.mjs +96 -0
- package/scripts/build-tui.mjs +17 -1
- package/scripts/fixtures/patch-replay-corpus.json +98 -0
- package/scripts/lib/isolated-root-cleanup.mjs +19 -0
- package/scripts/run-suite.mjs +100 -0
- package/src/defaults/mixdog-config.template.json +5 -8
- package/src/lib/rules-builder.cjs +4 -3
- package/src/output-styles/detailed.md +13 -15
- package/src/output-styles/extreme-minimal.md +7 -11
- package/src/output-styles/minimal.md +4 -7
- package/src/output-styles/simple.md +11 -13
- package/src/rules/agent/30-explorer.md +7 -4
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-brief.md +4 -5
- package/src/rules/lead/lead-tool.md +3 -2
- package/src/rules/shared/01-tool.md +20 -35
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +13 -2
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +29 -0
- package/src/runtime/agent/orchestrator/config.mjs +246 -67
- package/src/runtime/agent/orchestrator/mcp/client.mjs +29 -14
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +97 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +4 -6
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +8 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +29 -16
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +14 -16
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +5 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +13 -14
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/registry.mjs +51 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +82 -22
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +40 -36
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +10 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -33
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +0 -14
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -8
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -27
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -10
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -4
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +168 -83
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +34 -18
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +89 -11
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +6 -39
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -6
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -4
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +56 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +124 -19
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +7 -16
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +10 -26
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -23
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +24 -10
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +20 -43
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +76 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +13 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +10 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +38 -13
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +98 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +8 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +138 -25
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +47 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +40 -2
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +57 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +177 -16
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +21 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +217 -25
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +10 -10
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +14 -15
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +60 -8
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/channels/backends/discord.mjs +5 -14
- package/src/runtime/channels/backends/telegram.mjs +0 -5
- package/src/runtime/channels/lib/config.mjs +2 -2
- package/src/runtime/channels/lib/inbound-handler.mjs +1 -2
- package/src/runtime/channels/lib/output-forwarder.mjs +24 -5
- package/src/runtime/channels/lib/scheduler.mjs +3 -3
- package/src/runtime/channels/lib/worker-main.mjs +1 -1
- package/src/runtime/media/renditions.mjs +21 -2
- package/src/runtime/memory/index.mjs +5 -38
- package/src/runtime/memory/lib/agent-ipc.mjs +145 -81
- package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +12 -33
- package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
- package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
- package/src/runtime/memory/lib/http-router.mjs +8 -0
- package/src/runtime/memory/lib/ko-morph.mjs +3 -20
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
- package/src/runtime/memory/lib/pg/process.mjs +32 -9
- package/src/runtime/memory/tool-defs.mjs +7 -11
- package/src/runtime/search/tool-defs.mjs +2 -2
- package/src/runtime/shared/agent-route-config.mjs +119 -0
- package/src/runtime/shared/atomic-file.mjs +133 -4
- package/src/runtime/shared/background-tasks.mjs +6 -3
- package/src/runtime/shared/channel-notification-routing.mjs +2 -2
- package/src/runtime/shared/child-guardian.mjs +204 -108
- package/src/runtime/shared/child-spawn-gate.mjs +60 -31
- package/src/runtime/shared/config.mjs +130 -44
- package/src/runtime/shared/resource-admission.mjs +146 -58
- package/src/runtime/shared/stream-progress.mjs +6 -0
- package/src/runtime/shared/tool-status.mjs +10 -1
- package/src/runtime/shared/tool-surface.mjs +2 -7
- package/src/runtime/shared/turn-snapshot.mjs +411 -21
- package/src/runtime/shared/turn-worktree-snapshot.mjs +552 -0
- package/src/session-runtime/config-helpers.mjs +39 -37
- package/src/session-runtime/context-status.mjs +9 -3
- package/src/session-runtime/lifecycle-api.mjs +35 -5
- package/src/session-runtime/mcp-glue.mjs +11 -6
- package/src/session-runtime/model-route-api.mjs +7 -5
- package/src/session-runtime/provider-auth-api.mjs +0 -7
- package/src/session-runtime/provider-models.mjs +94 -43
- package/src/session-runtime/runtime-core.mjs +57 -19
- package/src/session-runtime/runtime-tunables.mjs +4 -0
- package/src/session-runtime/self-update.mjs +33 -4
- package/src/session-runtime/session-lifecycle.mjs +11 -2
- package/src/session-runtime/session-text.mjs +2 -1
- package/src/session-runtime/session-turn-api.mjs +139 -23
- package/src/session-runtime/settings-api.mjs +5 -17
- package/src/session-runtime/tool-defs.mjs +3 -3
- package/src/session-runtime/workflow-agents-api.mjs +55 -81
- package/src/session-runtime/workflow.mjs +25 -28
- package/src/standalone/agent-dispatch-broker.mjs +198 -0
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
- package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
- package/src/standalone/agent-tool/tool-def.mjs +0 -14
- package/src/standalone/agent-tool.mjs +4 -4
- package/src/standalone/backend-daemon.mjs +631 -0
- package/src/standalone/channel-admin.mjs +12 -12
- package/src/standalone/channel-daemon-client.mjs +17 -1
- package/src/standalone/channel-daemon-transport.mjs +231 -9
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/engine-daemon-client.mjs +1069 -0
- package/src/standalone/engine-daemon-protocol.mjs +32 -0
- package/src/standalone/engine-daemon-service.mjs +1108 -0
- package/src/standalone/engine-daemon-transport.mjs +794 -0
- package/src/standalone/explore-tool.mjs +187 -70
- package/src/standalone/fair-call-scheduler.mjs +264 -0
- package/src/standalone/session-protocol.mjs +188 -0
- package/src/tui/App.jsx +92 -64
- package/src/tui/app/app-format.mjs +4 -2
- package/src/tui/app/app-view.jsx +10 -2
- package/src/tui/app/channel-pickers.mjs +7 -6
- package/src/tui/app/core-memory-picker.mjs +20 -20
- package/src/tui/app/doctor.mjs +5 -11
- package/src/tui/app/extension-pickers.mjs +20 -18
- package/src/tui/app/maintenance-pickers.mjs +27 -27
- package/src/tui/app/message-selector.mjs +103 -0
- package/src/tui/app/onboarding-steps.mjs +24 -20
- package/src/tui/app/project-picker.mjs +80 -56
- package/src/tui/app/prompt-submit.mjs +59 -43
- package/src/tui/app/resume-picker.mjs +2 -2
- package/src/tui/app/route-pickers.mjs +16 -9
- package/src/tui/app/settings-picker.mjs +60 -65
- package/src/tui/app/slash-dispatch.mjs +32 -28
- package/src/tui/app/transcript-window.mjs +57 -0
- package/src/tui/app/usage-context-panels.mjs +22 -7
- package/src/tui/app/use-mouse-input.mjs +25 -3
- package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
- package/src/tui/app/use-prompt-handlers.mjs +86 -21
- package/src/tui/app/use-prompt-hint.mjs +3 -2
- package/src/tui/app/use-prompt-queue-history.mjs +39 -17
- package/src/tui/app/use-transcript-scroll.mjs +39 -10
- package/src/tui/app/use-transcript-window.mjs +62 -14
- package/src/tui/app/use-welcome-prompt-hint.mjs +2 -2
- package/src/tui/components/PromptInput.jsx +81 -38
- package/src/tui/components/Spinner.jsx +89 -95
- package/src/tui/components/TextEntryPanel.jsx +14 -0
- package/src/tui/components/ToolExecution.jsx +2 -2
- package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
- package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
- package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
- package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
- package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
- package/src/tui/dist/index.mjs +1694 -10633
- package/src/tui/engine/agent-job-feed.mjs +2 -2
- package/src/tui/engine/live-share.mjs +110 -3
- package/src/tui/engine/session-api-ext.mjs +24 -5
- package/src/tui/engine/session-api.mjs +165 -55
- package/src/tui/engine/session-flow.mjs +50 -5
- package/src/tui/engine/tool-card-results.mjs +10 -2
- package/src/tui/engine/tool-result-text.mjs +10 -0
- package/src/tui/engine/turn.mjs +11 -64
- package/src/tui/engine-local-session.mjs +1124 -0
- package/src/tui/engine.mjs +16 -1065
- package/src/tui/index.jsx +41 -4
- package/src/tui/markdown/stream-fence.mjs +1 -1
- package/src/tui/spinner-meta.mjs +80 -0
- package/src/tui/spinner-verbs.mjs +83 -0
- package/src/ui/statusline-segments.mjs +43 -10
- package/src/ui/statusline.mjs +10 -1
- package/scripts/tmp-cdp-errors.mjs +0 -41
- package/scripts/tmp-cdp-inspect.mjs +0 -41
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +0 -176
- package/src/standalone/channel-daemon.mjs +0 -226
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
cancelPromptImmediateFlush,
|
|
54
54
|
schedulePromptImmediateFlush,
|
|
55
55
|
} from './prompt-input/immediate-render.mjs';
|
|
56
|
+
import { classifyPromptEscape } from './prompt-input/escape-policy.mjs';
|
|
56
57
|
|
|
57
58
|
// Windows Terminal IME composition can clip a glyph that starts exactly at the
|
|
58
59
|
// left edge of the editable text node. The rounded prompt box already adds a
|
|
@@ -86,6 +87,7 @@ export function PromptInput({
|
|
|
86
87
|
interruptActive = false,
|
|
87
88
|
onInterrupt,
|
|
88
89
|
commandPaletteActive = false,
|
|
90
|
+
commandPaletteOpen = commandPaletteActive,
|
|
89
91
|
mask = false,
|
|
90
92
|
hint = '',
|
|
91
93
|
hintTone = 'info',
|
|
@@ -98,6 +100,8 @@ export function PromptInput({
|
|
|
98
100
|
onCommandPaletteCancel,
|
|
99
101
|
onCommandPaletteComplete,
|
|
100
102
|
onRestoreQueued,
|
|
103
|
+
hasQueuedMessages = false,
|
|
104
|
+
hasMessages = false,
|
|
101
105
|
onHistoryNavigate,
|
|
102
106
|
onPasteText,
|
|
103
107
|
selectionRef,
|
|
@@ -112,13 +116,16 @@ export function PromptInput({
|
|
|
112
116
|
});
|
|
113
117
|
const [, bumpCursorAnchorEpoch] = useState(0);
|
|
114
118
|
const draftRef = useRef(draft);
|
|
115
|
-
const interruptActiveRef = useRef(interruptActive);
|
|
116
119
|
const lastReportedValueRef = useRef(draft.value);
|
|
117
120
|
// Bumped on every submit/draftOverride replace/unmount so an async paste
|
|
118
121
|
// (clipboard read or onPasteText promise) that resolves after the draft
|
|
119
122
|
// moved on can detect staleness and drop its result instead of mutating
|
|
120
123
|
// whatever prompt is now in the box.
|
|
121
124
|
const pasteGenerationRef = useRef(0);
|
|
125
|
+
// One physical Enter must not enqueue twice when the terminal delivers the
|
|
126
|
+
// chord as multiple input events before the draft clears (Windows CR/LF).
|
|
127
|
+
const submitGateRef = useRef(false);
|
|
128
|
+
const escapeClearAtRef = useRef(0);
|
|
122
129
|
if (valueRef) valueRef.current = draftRef.current.value;
|
|
123
130
|
const { isRawModeSupported } = useStdin();
|
|
124
131
|
// The text box's ink DOM node. We mark it as the cursor anchor (forked ink
|
|
@@ -138,7 +145,6 @@ export function PromptInput({
|
|
|
138
145
|
const undoRef = useRef({ past: [], future: [], lastPushAt: 0, lastValue: null });
|
|
139
146
|
const { value, cursor } = draft;
|
|
140
147
|
draftRef.current = draft;
|
|
141
|
-
interruptActiveRef.current = interruptActive;
|
|
142
148
|
if (selectionRef) {
|
|
143
149
|
const range = selectionRange(draft);
|
|
144
150
|
selectionRef.current = range
|
|
@@ -171,14 +177,12 @@ export function PromptInput({
|
|
|
171
177
|
}
|
|
172
178
|
};
|
|
173
179
|
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
// Idle input keeps the leading+trailing immediate flush for crisp caret echo.
|
|
180
|
+
// Keep prompt echo on the same leading+trailing immediate cadence while a
|
|
181
|
+
// turn is active. Claude Code keeps mid-turn typing responsive; suppressing
|
|
182
|
+
// this path made the draft visibly trail the keyboard under streaming load.
|
|
178
183
|
const scheduleImmediateFlush = () => {
|
|
179
184
|
schedulePromptImmediateFlush({
|
|
180
185
|
throttle: flushThrottleRef.current,
|
|
181
|
-
isSuppressed: () => interruptActiveRef.current,
|
|
182
186
|
flush: flushImmediate,
|
|
183
187
|
});
|
|
184
188
|
};
|
|
@@ -188,6 +192,7 @@ export function PromptInput({
|
|
|
188
192
|
}, []);
|
|
189
193
|
|
|
190
194
|
const commitDraft = (next, options = {}) => {
|
|
195
|
+
escapeClearAtRef.current = 0;
|
|
191
196
|
const sameDraft = draftStateEqual(draftRef.current, next);
|
|
192
197
|
if (!options.keepPreferredColumn) preferredColumnRef.current = null;
|
|
193
198
|
if (sameDraft) {
|
|
@@ -446,8 +451,12 @@ export function PromptInput({
|
|
|
446
451
|
return true;
|
|
447
452
|
};
|
|
448
453
|
|
|
449
|
-
const restoreQueuedToDraft = () => {
|
|
450
|
-
return onRestoreQueued?.(
|
|
454
|
+
const restoreQueuedToDraft = ({ showHint = false } = {}) => {
|
|
455
|
+
return onRestoreQueued?.({
|
|
456
|
+
restoreDraft: true,
|
|
457
|
+
showHint,
|
|
458
|
+
currentText: draftRef.current.value,
|
|
459
|
+
}) === true;
|
|
451
460
|
};
|
|
452
461
|
|
|
453
462
|
const applyHistoryNavigation = (direction, meta = {}) => {
|
|
@@ -504,15 +513,22 @@ export function PromptInput({
|
|
|
504
513
|
}, [selectionRef]);
|
|
505
514
|
|
|
506
515
|
const submitDraft = (next) => {
|
|
516
|
+
if (submitGateRef.current) return;
|
|
507
517
|
const text = next.value;
|
|
518
|
+
if (!String(text || '').trim()) return;
|
|
519
|
+
submitGateRef.current = true;
|
|
508
520
|
const accepted = onSubmit?.(text) !== false;
|
|
509
521
|
if (!accepted) {
|
|
522
|
+
submitGateRef.current = false;
|
|
510
523
|
commitDraft(next);
|
|
511
524
|
return;
|
|
512
525
|
}
|
|
513
526
|
pasteGenerationRef.current += 1;
|
|
514
527
|
commitDraft({ value: '', cursor: 0, selectionAnchor: null }, { skipHistory: true });
|
|
515
528
|
resetUndo();
|
|
529
|
+
// Unlock after this input batch drains so a second return event in the
|
|
530
|
+
// same chord cannot re-submit the pre-clear draft.
|
|
531
|
+
queueMicrotask(() => { submitGateRef.current = false; });
|
|
516
532
|
};
|
|
517
533
|
|
|
518
534
|
const submitEnterChunk = (prefix = '') => {
|
|
@@ -542,6 +558,7 @@ export function PromptInput({
|
|
|
542
558
|
|
|
543
559
|
const rawInput = String(input ?? '');
|
|
544
560
|
const inputKey = rawInput.toLowerCase();
|
|
561
|
+
if (!key.escape) escapeClearAtRef.current = 0;
|
|
545
562
|
const rawShiftArrowForGrid =
|
|
546
563
|
rawInput === '\x1b[1;2A' || rawInput === '\x1b[a' || rawInput === '[1;2A'
|
|
547
564
|
|| rawInput === '\x1b[1;2B' || rawInput === '\x1b[b' || rawInput === '[1;2B'
|
|
@@ -618,8 +635,8 @@ export function PromptInput({
|
|
|
618
635
|
const lineBreakIndex = rawInput.search(/[\r\n]/);
|
|
619
636
|
const rawEnter = rawInput === '\r' || rawInput === '\n' || rawInput === '\r\n';
|
|
620
637
|
const trailingEnterPrefix = singleTrailingLineBreakPrefix(rawInput);
|
|
621
|
-
const
|
|
622
|
-
const modifiedLineBreak = key.shift || key.meta || key.ctrl ||
|
|
638
|
+
const rawModifiedEnter = isModifiedEnterSequence(rawInput);
|
|
639
|
+
const modifiedLineBreak = key.shift || key.meta || key.ctrl || rawModifiedEnter;
|
|
623
640
|
|
|
624
641
|
// Ctrl+J is the protocol-INDEPENDENT newline that works on every terminal.
|
|
625
642
|
// • Legacy / modifyOtherKeys terminals: Ctrl+J is a lone '\n' (0x0A). A real
|
|
@@ -636,12 +653,9 @@ export function PromptInput({
|
|
|
636
653
|
return;
|
|
637
654
|
}
|
|
638
655
|
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
|
|
642
|
-
// never type into the prompt under modifyOtherKeys. Plain Enter (mod=1) is
|
|
643
|
-
// not matched and still submits below.
|
|
644
|
-
if (!rawCtrlEnter && isAnyModifiedEnterSequence(rawInput)) {
|
|
656
|
+
// Consume uncommon modified-Enter combinations outside the normal
|
|
657
|
+
// Shift/Alt/Ctrl newline set so raw CSI bytes never enter the prompt.
|
|
658
|
+
if (!rawModifiedEnter && isAnyModifiedEnterSequence(rawInput)) {
|
|
645
659
|
return;
|
|
646
660
|
}
|
|
647
661
|
|
|
@@ -666,7 +680,7 @@ export function PromptInput({
|
|
|
666
680
|
return;
|
|
667
681
|
}
|
|
668
682
|
|
|
669
|
-
if (
|
|
683
|
+
if (rawModifiedEnter) {
|
|
670
684
|
updateDraft((d) => replaceSelection(d, '\n'));
|
|
671
685
|
return;
|
|
672
686
|
}
|
|
@@ -735,12 +749,10 @@ export function PromptInput({
|
|
|
735
749
|
if (!moveDraftVertically(-1, { extend: true })) {
|
|
736
750
|
updateDraft((d) => moveCursor(d, 0, { extend: true }));
|
|
737
751
|
}
|
|
738
|
-
} else {
|
|
739
|
-
const
|
|
740
|
-
if (!
|
|
741
|
-
|
|
742
|
-
} else if (!moveDraftVertically(-1, { extend: false })) {
|
|
743
|
-
applyHistoryNavigation('up', { emptyDraft: false });
|
|
752
|
+
} else if (!moveDraftVertically(-1, { extend: false })) {
|
|
753
|
+
const emptyDraft = String(draftRef.current.value || '').length === 0;
|
|
754
|
+
if (!hasQueuedMessages || !restoreQueuedToDraft()) {
|
|
755
|
+
applyHistoryNavigation('up', { emptyDraft });
|
|
744
756
|
}
|
|
745
757
|
}
|
|
746
758
|
}
|
|
@@ -758,7 +770,7 @@ export function PromptInput({
|
|
|
758
770
|
updateDraft((d) => moveCursor(d, d.value.length, { extend: true }));
|
|
759
771
|
}
|
|
760
772
|
} else if (!moveDraftVertically(1, { extend: false })) {
|
|
761
|
-
applyHistoryNavigation('down', { emptyDraft: String(draftRef.current.value || '').
|
|
773
|
+
applyHistoryNavigation('down', { emptyDraft: String(draftRef.current.value || '').length === 0 });
|
|
762
774
|
}
|
|
763
775
|
}
|
|
764
776
|
return;
|
|
@@ -796,9 +808,8 @@ export function PromptInput({
|
|
|
796
808
|
}
|
|
797
809
|
|
|
798
810
|
if (key.escape) {
|
|
799
|
-
if (
|
|
811
|
+
if (commandPaletteOpen) {
|
|
800
812
|
onCommandPaletteCancel?.(draftRef.current.value);
|
|
801
|
-
commitDraft({ value: '', cursor: 0, selectionAnchor: null });
|
|
802
813
|
return;
|
|
803
814
|
}
|
|
804
815
|
if (selectionRange(draftRef.current)) {
|
|
@@ -809,23 +820,55 @@ export function PromptInput({
|
|
|
809
820
|
if (onEscape?.(currentValue, { phase: 'before' }) === true) {
|
|
810
821
|
return;
|
|
811
822
|
}
|
|
812
|
-
|
|
823
|
+
let escape = classifyPromptEscape({
|
|
824
|
+
interruptActive,
|
|
825
|
+
hasQueuedMessages,
|
|
826
|
+
hasMessages,
|
|
827
|
+
value: currentValue,
|
|
828
|
+
lastClearPressAt: escapeClearAtRef.current,
|
|
829
|
+
});
|
|
830
|
+
if (escape.action === 'restore-queue') {
|
|
831
|
+
if (restoreQueuedToDraft()) {
|
|
832
|
+
escapeClearAtRef.current = 0;
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
// A stale projected queue can empty between render and key handling.
|
|
836
|
+
// Fall through to the normal draft/idle action in that case.
|
|
837
|
+
escape = classifyPromptEscape({
|
|
838
|
+
interruptActive,
|
|
839
|
+
hasMessages,
|
|
840
|
+
value: currentValue,
|
|
841
|
+
lastClearPressAt: escapeClearAtRef.current,
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
escapeClearAtRef.current = escape.nextClearPressAt;
|
|
845
|
+
// Active work always wins, even if the user has already typed a steering
|
|
846
|
+
// draft. The draft is preserved; the old submitted prompt is restored only
|
|
847
|
+
// when this box is still empty after cancellation.
|
|
848
|
+
if (escape.action === 'interrupt') {
|
|
849
|
+
const restoredText = onInterrupt?.(currentValue);
|
|
850
|
+
if (!currentValue && typeof restoredText === 'string') {
|
|
851
|
+
commitDraft({ value: restoredText, cursor: restoredText.length, selectionAnchor: null });
|
|
852
|
+
}
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
if (escape.action === 'arm-clear') {
|
|
856
|
+
onEscape?.(currentValue, { phase: 'clear-arm' });
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
if (escape.action === 'clear') {
|
|
813
860
|
onEscape?.(currentValue, { phase: 'clear' });
|
|
814
861
|
commitDraft({ value: '', cursor: 0, selectionAnchor: null });
|
|
815
862
|
return;
|
|
816
863
|
}
|
|
817
|
-
//
|
|
818
|
-
//
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
if (interruptActive) {
|
|
822
|
-
const restoredText = onInterrupt?.('');
|
|
823
|
-
if (typeof restoredText === 'string') {
|
|
824
|
-
commitDraft({ value: restoredText, cursor: restoredText.length, selectionAnchor: null });
|
|
825
|
-
}
|
|
864
|
+
// Empty draft + conversation history: first press arms, the second
|
|
865
|
+
// opens the message selector (Claude Code's jump-back list).
|
|
866
|
+
if (escape.action === 'arm-select') {
|
|
867
|
+
onEscape?.('', { phase: 'select-arm' });
|
|
826
868
|
return;
|
|
827
869
|
}
|
|
828
|
-
if (
|
|
870
|
+
if (escape.action === 'message-selector') {
|
|
871
|
+
onEscape?.('', { phase: 'select' });
|
|
829
872
|
return;
|
|
830
873
|
}
|
|
831
874
|
onEscape?.('', { phase: 'empty' });
|
|
@@ -21,7 +21,8 @@ import React, { useRef } from 'react';
|
|
|
21
21
|
import { Box, Text } from 'ink';
|
|
22
22
|
import { useSharedTick } from '../hooks/useSharedTick.mjs';
|
|
23
23
|
import { theme } from '../theme.mjs';
|
|
24
|
-
import { SPINNER_FRAMES } from '../spinner-verbs.mjs';
|
|
24
|
+
import { SPINNER_FRAMES, SPINNER_MODE_OVERRIDE_VERBS, spinnerVerbFor } from '../spinner-verbs.mjs';
|
|
25
|
+
import { buildSpinnerMeta, isReducedMotion } from '../spinner-meta.mjs';
|
|
25
26
|
import { DOWN_ARROW, UP_ARROW } from '../figures.mjs';
|
|
26
27
|
import { formatDuration } from '../time-format.mjs';
|
|
27
28
|
|
|
@@ -32,38 +33,23 @@ const FRAMES = [...SPINNER_FRAMES, ...[...SPINNER_FRAMES].reverse()];
|
|
|
32
33
|
// Stall: response must grow within this window or the glyph reddens.
|
|
33
34
|
const STALL_TIMEOUT_MS = 3000;
|
|
34
35
|
const STALL_FADE_MS = 2000; // fade red over 2s
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
// Reduced motion: the glyph freezes, the shimmer stops and the tick drops to
|
|
37
|
+
// one second so only the timer keeps moving.
|
|
38
|
+
const REDUCED_MOTION_TICK_MS = 1000;
|
|
39
|
+
// A running turn always states how to stop it.
|
|
40
|
+
const INTERRUPT_HINT = 'esc to interrupt';
|
|
39
41
|
// Thinking shimmer starts after this delay.
|
|
40
42
|
const THINKING_DELAY_MS = 3000;
|
|
41
43
|
|
|
42
44
|
// One-way shimmer. The tail runs past the final character before restarting.
|
|
43
45
|
const GLIMMER_SPEED_MS = { requesting: 70, reconnecting: 70, compacting: 120, 'auto-clear': 120, resuming: 120, 'tool-use': 120, responding: 120, thinking: 120, 'tool-input': 120 };
|
|
44
|
-
|
|
46
|
+
// A wider trail turns the highlight into a wipe travelling across the phrase
|
|
47
|
+
// instead of a single bright character hopping along it.
|
|
48
|
+
const GLIMMER_TRAIL = 6;
|
|
45
49
|
const THINKING_GLIMMER_SPEED_MS = 120;
|
|
46
50
|
const THINKING_GLIMMER_TRAIL = 4;
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// often never visibly fired.
|
|
50
|
-
const VERB_ROTATE_MIN_MS = 30000;
|
|
51
|
-
const VERB_ROTATE_SPREAD_MS = 0;
|
|
52
|
-
const VERB_CHANGE_PROBABILITY = 1;
|
|
53
|
-
|
|
54
|
-
const MODE_VERBS = {
|
|
55
|
-
requesting: ['Requesting'],
|
|
56
|
-
compacting: ['Compacting conversation'],
|
|
57
|
-
'auto-clear': ['Auto-clearing conversation'],
|
|
58
|
-
resuming: ['Resuming conversation'],
|
|
59
|
-
// Conservative phrasing (user decision): each pool only rotates through
|
|
60
|
-
// synonyms of what the mode ACTUALLY means — never a concrete action the
|
|
61
|
-
// engine may not be doing (no "Checking files" during a web search).
|
|
62
|
-
thinking: ['Thinking', 'Considering', 'Organizing'],
|
|
63
|
-
'tool-use': ['Working', 'Running tools', 'Reviewing output'],
|
|
64
|
-
'tool-input': ['Working', 'Running tools', 'Reviewing output'],
|
|
65
|
-
responding: ['Writing', 'Wrapping up'],
|
|
66
|
-
};
|
|
51
|
+
// The phrase itself comes from the shared pool in spinner-verbs.mjs — see
|
|
52
|
+
// spinnerVerbFor(). Mode only drives shimmer speed, token glyph and stall tint.
|
|
67
53
|
|
|
68
54
|
function interpolateColor(a, b, t) {
|
|
69
55
|
return {
|
|
@@ -126,38 +112,9 @@ function spinnerRgb() {
|
|
|
126
112
|
};
|
|
127
113
|
}
|
|
128
114
|
|
|
129
|
-
const compactNumberFormatter = new Intl.NumberFormat('en-US', {
|
|
130
|
-
notation: 'compact',
|
|
131
|
-
maximumFractionDigits: 1,
|
|
132
|
-
minimumFractionDigits: 0,
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
function formatNumber(n) {
|
|
136
|
-
const value = Math.max(0, Number(n || 0));
|
|
137
|
-
if (value >= 1000) return compactNumberFormatter.format(value).toLowerCase();
|
|
138
|
-
return String(Math.round(value));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
115
|
const STATUS_SEP = ' · ';
|
|
142
116
|
const SEP_WIDTH = STATUS_SEP.length;
|
|
143
117
|
|
|
144
|
-
function stableModeVerb(mode, fallback) {
|
|
145
|
-
const phrases = MODE_VERBS[mode] || [fallback || 'Working'];
|
|
146
|
-
return phrases[0] || fallback || 'Working';
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function nextVerbCheckAt(now) {
|
|
150
|
-
return now + VERB_ROTATE_MIN_MS + Math.floor(Math.random() * VERB_ROTATE_SPREAD_MS);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function chooseNextVerb(mode, fallback, current) {
|
|
154
|
-
const phrases = MODE_VERBS[mode] || [fallback || 'Working'];
|
|
155
|
-
if (phrases.length <= 1 || Math.random() > VERB_CHANGE_PROBABILITY) return current || phrases[0];
|
|
156
|
-
const candidates = phrases.filter((phrase) => phrase && phrase !== current);
|
|
157
|
-
if (!candidates.length) return current || phrases[0];
|
|
158
|
-
return candidates[Math.floor(Math.random() * candidates.length)];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
118
|
// Token-direction glyph by mode:
|
|
162
119
|
// up while uploading the request, down while receiving a response, and no
|
|
163
120
|
// glyph at all for non-streaming modes (compacting/resuming/auto-clear) where
|
|
@@ -176,24 +133,44 @@ function tokenModeGlyph(mode) {
|
|
|
176
133
|
}
|
|
177
134
|
}
|
|
178
135
|
|
|
179
|
-
export function Spinner({ verb = 'Working', startedAt, outputTokens = 0, tokens = 0, thinking = false, thinkingActiveSince = 0, mode = 'responding', columns = 80, marginTop = 1 }) {
|
|
136
|
+
export function Spinner({ verb = 'Working', startedAt, outputTokens = 0, tokens = 0, thinking = false, thinkingActiveSince = 0, thinkingMs = 0, effort = '', hasActiveTools = false, paused = false, interruptible = false, mode = 'responding', columns = 80, marginTop = 1 }) {
|
|
137
|
+
const reducedMotion = isReducedMotion();
|
|
180
138
|
// Re-render at the frame cadence off the shared tick (no dedicated timer).
|
|
181
139
|
// Glyph/shimmer/token/elapsed values are all derived from Date.now() below.
|
|
182
|
-
useSharedTick(FRAME_MS);
|
|
140
|
+
useSharedTick(reducedMotion ? REDUCED_MOTION_TICK_MS : FRAME_MS);
|
|
183
141
|
const { TEXT_RGB, SHIMMER_RGB, SPINNER_GLYPH_RGB, THINKING_INACTIVE, THINKING_SHIMMER, STALL_RGB } = spinnerRgb();
|
|
184
142
|
const now = Date.now();
|
|
185
|
-
|
|
186
|
-
|
|
143
|
+
// Animation clock: raw wall time since the turn began. It never pauses, so a
|
|
144
|
+
// frozen timer (below) still animates instead of looking hung.
|
|
145
|
+
const rawElapsedMs = startedAt ? Math.max(0, now - startedAt) : 0;
|
|
146
|
+
const frame = reducedMotion ? 0 : Math.floor(rawElapsedMs / FRAME_MS);
|
|
187
147
|
const lastGrowRef = useRef(now);
|
|
188
148
|
const lastTokensRef = useRef(0);
|
|
189
149
|
const displayedOutputRef = useRef(0);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
150
|
+
// Pause accounting: time spent waiting on the USER (a tool approval prompt)
|
|
151
|
+
// is not turn time. Without this the reported duration counts however long
|
|
152
|
+
// the prompt sat unanswered and the short-turn token gate opens on it.
|
|
153
|
+
const turnAnchorRef = useRef(startedAt);
|
|
154
|
+
const pausedTotalRef = useRef(0);
|
|
155
|
+
const pauseStartRef = useRef(0);
|
|
193
156
|
// Stall smoothing refs (exponential fade)
|
|
194
157
|
const stallSmoothRef = useRef(0);
|
|
195
158
|
const lastStallTickRef = useRef(0);
|
|
196
159
|
|
|
160
|
+
if (turnAnchorRef.current !== startedAt) {
|
|
161
|
+
turnAnchorRef.current = startedAt;
|
|
162
|
+
pausedTotalRef.current = 0;
|
|
163
|
+
pauseStartRef.current = 0;
|
|
164
|
+
}
|
|
165
|
+
if (paused && !pauseStartRef.current) {
|
|
166
|
+
pauseStartRef.current = now;
|
|
167
|
+
} else if (!paused && pauseStartRef.current) {
|
|
168
|
+
pausedTotalRef.current += Math.max(0, now - pauseStartRef.current);
|
|
169
|
+
pauseStartRef.current = 0;
|
|
170
|
+
}
|
|
171
|
+
const pausedMs = pausedTotalRef.current + (pauseStartRef.current ? Math.max(0, now - pauseStartRef.current) : 0);
|
|
172
|
+
const elapsedMs = Math.max(0, rawElapsedMs - pausedMs);
|
|
173
|
+
|
|
197
174
|
const targetOutputTokens = Math.max(0, Number(outputTokens || tokens || 0));
|
|
198
175
|
|
|
199
176
|
// Stall detection — track output growth, because input usually arrives as one
|
|
@@ -203,8 +180,14 @@ export function Spinner({ verb = 'Working', startedAt, outputTokens = 0, tokens
|
|
|
203
180
|
lastGrowRef.current = now;
|
|
204
181
|
}
|
|
205
182
|
|
|
183
|
+
// Waiting is not stalling: while a tool runs (no tokens can arrive) or while
|
|
184
|
+
// the user owns an approval prompt, hold the stall clock at zero. Otherwise a
|
|
185
|
+
// 30s shell command or a slow approval reddens a perfectly healthy turn.
|
|
186
|
+
const workBlocked = paused || hasActiveTools || mode === 'tool-use' || mode === 'tool-input';
|
|
187
|
+
if (workBlocked) lastGrowRef.current = now;
|
|
188
|
+
|
|
206
189
|
const stallMs = now - lastGrowRef.current;
|
|
207
|
-
const isStalled = targetOutputTokens > 0 && stallMs > STALL_TIMEOUT_MS;
|
|
190
|
+
const isStalled = !reducedMotion && targetOutputTokens > 0 && stallMs > STALL_TIMEOUT_MS;
|
|
208
191
|
// Stall smoothing: exponential fade toward target
|
|
209
192
|
const rawIntensity = isStalled
|
|
210
193
|
? Math.min(1, (stallMs - STALL_TIMEOUT_MS) / STALL_FADE_MS)
|
|
@@ -235,36 +218,31 @@ export function Spinner({ verb = 'Working', startedAt, outputTokens = 0, tokens
|
|
|
235
218
|
))
|
|
236
219
|
: theme.spinnerGlyph;
|
|
237
220
|
|
|
238
|
-
// --- Verb
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
// never fires. The cadence holds from the first mount of the turn.
|
|
244
|
-
if (!nextVerbCheckRef.current) nextVerbCheckRef.current = nextVerbCheckAt(now);
|
|
245
|
-
}
|
|
246
|
-
displayVerbModeRef.current = mode;
|
|
247
|
-
if (now >= nextVerbCheckRef.current) {
|
|
248
|
-
displayVerbRef.current = chooseNextVerb(mode, verb, displayVerbRef.current);
|
|
249
|
-
nextVerbCheckRef.current = nextVerbCheckAt(now);
|
|
250
|
-
}
|
|
251
|
-
const displayVerb = displayVerbRef.current;
|
|
221
|
+
// --- Verb (one common pool, one phrase per 30s window) ---
|
|
222
|
+
// Anchored to `startedAt`, so a mode flip cannot rewrite the label and the
|
|
223
|
+
// desktop shows the same word at the same second.
|
|
224
|
+
const displayVerb = SPINNER_MODE_OVERRIDE_VERBS[mode]
|
|
225
|
+
|| (mode === 'reconnecting' ? (String(verb || '').trim() || 'Reconnecting') : spinnerVerbFor(startedAt, now));
|
|
252
226
|
const messageText = mode === 'reconnecting' ? displayVerb : `${displayVerb}…`;
|
|
253
227
|
const messageLen = messageText.length;
|
|
254
228
|
|
|
255
229
|
// Glimmer speed per mode.
|
|
256
230
|
const glimmerSpeed = GLIMMER_SPEED_MS[mode] ?? 200;
|
|
257
231
|
const shimmerSpan = Math.max(1, messageLen + GLIMMER_TRAIL);
|
|
258
|
-
const shimmerHead = Math.floor(
|
|
232
|
+
const shimmerHead = Math.floor(rawElapsedMs / glimmerSpeed) % shimmerSpan;
|
|
259
233
|
|
|
260
234
|
// Keep the verb shimmer moving even during stalls/tool waits. Stall tinting is
|
|
261
235
|
// limited to the glyph; tinting the whole verb made the sweep disappear after
|
|
262
236
|
// a few seconds and read as a stuck dark label.
|
|
263
|
-
const verbContent = messageLen > 0 && TEXT_RGB && SHIMMER_RGB
|
|
237
|
+
const verbContent = messageLen > 0 && !reducedMotion && TEXT_RGB && SHIMMER_RGB
|
|
264
238
|
? renderShimmerText(messageText, shimmerHead, GLIMMER_TRAIL, TEXT_RGB, SHIMMER_RGB, theme.spinnerText, 'verb', shimmerSpan)
|
|
265
|
-
: null;
|
|
239
|
+
: (messageLen > 0 ? <Text color={theme.spinnerText}>{messageText}</Text> : null);
|
|
266
240
|
|
|
267
241
|
const advanceCounter = (ref, target) => {
|
|
242
|
+
if (reducedMotion) {
|
|
243
|
+
ref.current = target;
|
|
244
|
+
return Math.round(target);
|
|
245
|
+
}
|
|
268
246
|
if (ref.current > target) {
|
|
269
247
|
ref.current = target;
|
|
270
248
|
} else if (ref.current < target) {
|
|
@@ -284,10 +262,19 @@ export function Spinner({ verb = 'Working', startedAt, outputTokens = 0, tokens
|
|
|
284
262
|
// thinking, tool-use, tool-input). Input token totals are not shown.
|
|
285
263
|
const displayedOutputTokens = advanceCounter(displayedOutputRef, targetOutputTokens);
|
|
286
264
|
|
|
265
|
+
// Byline text comes from the shared builder so the desktop band reads the
|
|
266
|
+
// same way (token gate, thinking vs "thought for Ns").
|
|
267
|
+
const meta = buildSpinnerMeta({
|
|
268
|
+
elapsedMs,
|
|
269
|
+
outputTokens: displayedOutputTokens,
|
|
270
|
+
thinking,
|
|
271
|
+
thinkingSince: thinkingActiveSince,
|
|
272
|
+
thinkingMs,
|
|
273
|
+
effort,
|
|
274
|
+
});
|
|
287
275
|
const tokenGlyph = tokenModeGlyph(mode);
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
? (tokenGlyph ? `${tokenGlyph} ${tokenCountText} tokens` : `${tokenCountText} tokens`)
|
|
276
|
+
const tokenText = meta.tokensText
|
|
277
|
+
? (tokenGlyph ? `${tokenGlyph} ${meta.tokensText}` : meta.tokensText)
|
|
291
278
|
: '';
|
|
292
279
|
const tokenW = tokenText.length;
|
|
293
280
|
|
|
@@ -299,20 +286,22 @@ export function Spinner({ verb = 'Working', startedAt, outputTokens = 0, tokens
|
|
|
299
286
|
const timerText = formatDuration(elapsedMs);
|
|
300
287
|
const timerLabel = timerText;
|
|
301
288
|
const timerW = timerLabel.length;
|
|
302
|
-
const thinkingActive =
|
|
303
|
-
const thinkingStatusText =
|
|
304
|
-
? 'thinking'
|
|
305
|
-
: '';
|
|
289
|
+
const thinkingActive = meta.thinkingActive;
|
|
290
|
+
const thinkingStatusText = meta.thinkingText;
|
|
306
291
|
const thinkingStatusW = thinkingStatusText.length;
|
|
307
292
|
// Turn elapsed time is the headline metric here, not a thinking sub-stat, so
|
|
308
293
|
// it shows from 1s onward (formatDuration returns '' below 1s). Tokens keep
|
|
309
294
|
// Short-turn gate: tokens only appear once the turn runs long.
|
|
310
|
-
const wantsTokens =
|
|
311
|
-
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
const
|
|
315
|
-
const
|
|
295
|
+
const wantsTokens = meta.showTokens;
|
|
296
|
+
|
|
297
|
+
// Interrupt hint and thinking status win the width race (they render LAST in
|
|
298
|
+
// the byline but are gated FIRST), then timer, then tokens.
|
|
299
|
+
const interruptText = interruptible ? INTERRUPT_HINT : '';
|
|
300
|
+
const interruptW = interruptText.length;
|
|
301
|
+
const showInterrupt = Boolean(interruptText) && avail > interruptW;
|
|
302
|
+
const usedAfterInterrupt = showInterrupt ? interruptW + SEP_WIDTH : 0;
|
|
303
|
+
const showThinkingStatus = Boolean(thinkingStatusText) && avail > usedAfterInterrupt + thinkingStatusW;
|
|
304
|
+
const usedAfterThinking = usedAfterInterrupt + (showThinkingStatus ? thinkingStatusW + SEP_WIDTH : 0);
|
|
316
305
|
const showTimer = Boolean(timerLabel) && avail > usedAfterThinking + timerW;
|
|
317
306
|
const usedAfterTimer = usedAfterThinking + (showTimer ? timerW + SEP_WIDTH : 0);
|
|
318
307
|
const showTokens = wantsTokens && tokenText && avail > usedAfterTimer + tokenW;
|
|
@@ -331,13 +320,18 @@ export function Spinner({ verb = 'Working', startedAt, outputTokens = 0, tokens
|
|
|
331
320
|
}
|
|
332
321
|
if (showThinkingStatus) {
|
|
333
322
|
const thinkingSpan = Math.max(1, thinkingStatusText.length + THINKING_GLIMMER_TRAIL);
|
|
334
|
-
const thinkingHead = Math.floor(Math.max(0,
|
|
323
|
+
const thinkingHead = Math.floor(Math.max(0, rawElapsedMs - THINKING_DELAY_MS) / THINKING_GLIMMER_SPEED_MS) % thinkingSpan;
|
|
335
324
|
segments.push(
|
|
336
|
-
thinkingActive
|
|
325
|
+
thinkingActive && !reducedMotion
|
|
337
326
|
? <Text key="thinking-status">{renderShimmerText(thinkingStatusText, thinkingHead, THINKING_GLIMMER_TRAIL, THINKING_INACTIVE, THINKING_SHIMMER, theme.thinkingBase, 'thinking-status', thinkingSpan)}</Text>
|
|
338
327
|
: <Text key="thinking-status" color={theme.statusSubtle}>{thinkingStatusText}</Text>
|
|
339
328
|
);
|
|
340
329
|
}
|
|
330
|
+
if (showInterrupt) {
|
|
331
|
+
segments.push(
|
|
332
|
+
<Text key="interrupt" color={theme.statusSubtle}>{interruptText}</Text>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
341
335
|
return (
|
|
342
336
|
<Box marginTop={marginTop} flexDirection="row">
|
|
343
337
|
<Box flexWrap="wrap" height={1} width={2}>
|
|
@@ -180,6 +180,7 @@ export function TextEntryPanel({
|
|
|
180
180
|
const [draft, setDraft] = useState(() => ({ value: String(initialValue || ''), cursor: String(initialValue || '').length, selectionAnchor: null }));
|
|
181
181
|
const [, bumpCursorAnchorEpoch] = useState(0);
|
|
182
182
|
const draftRef = useRef(draft);
|
|
183
|
+
const submitGateRef = useRef(false);
|
|
183
184
|
const boxRef = useRef(null);
|
|
184
185
|
const cursorEnabledRef = useRef(false);
|
|
185
186
|
const contentWidthRef = useRef(80);
|
|
@@ -244,20 +245,33 @@ export function TextEntryPanel({
|
|
|
244
245
|
}, [multiline, draft.value, mask, contentCells, maxContentRows, onContentRowsChange]);
|
|
245
246
|
|
|
246
247
|
const submit = () => {
|
|
248
|
+
if (submitGateRef.current) return;
|
|
249
|
+
if (!String(draftRef.current.value || '').trim()) return;
|
|
250
|
+
submitGateRef.current = true;
|
|
247
251
|
const accepted = onSubmit?.(draftRef.current.value) !== false;
|
|
248
252
|
if (accepted) {
|
|
249
253
|
commitDraft({ value: '', cursor: 0, selectionAnchor: null });
|
|
254
|
+
queueMicrotask(() => { submitGateRef.current = false; });
|
|
255
|
+
} else {
|
|
256
|
+
submitGateRef.current = false;
|
|
250
257
|
}
|
|
251
258
|
};
|
|
252
259
|
|
|
253
260
|
const submitEnterChunk = (prefix = '') => {
|
|
261
|
+
if (submitGateRef.current) return;
|
|
254
262
|
const current = draftRef.current;
|
|
255
263
|
const next = prefix ? insertText(current, prefix) : current;
|
|
264
|
+
if (!String(next.value || '').trim()) return;
|
|
265
|
+
submitGateRef.current = true;
|
|
256
266
|
const accepted = onSubmit?.(next.value) !== false;
|
|
257
267
|
if (accepted) {
|
|
258
268
|
commitDraft({ value: '', cursor: 0, selectionAnchor: null });
|
|
269
|
+
queueMicrotask(() => { submitGateRef.current = false; });
|
|
259
270
|
} else if (next !== current) {
|
|
271
|
+
submitGateRef.current = false;
|
|
260
272
|
commitDraft(next);
|
|
273
|
+
} else {
|
|
274
|
+
submitGateRef.current = false;
|
|
261
275
|
}
|
|
262
276
|
};
|
|
263
277
|
|
|
@@ -369,8 +369,8 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
369
369
|
let labelOut;
|
|
370
370
|
let summaryOut;
|
|
371
371
|
// Shell headers stay label-only ("Ran 1 command") on every surface —
|
|
372
|
-
//
|
|
373
|
-
//
|
|
372
|
+
// the desktop card contract: the raw command line never rides inline in
|
|
373
|
+
// the header; it stays in the expanded detail body.
|
|
374
374
|
const headerSummaryText = isShellSurface ? '' : summaryText;
|
|
375
375
|
if (stringWidth(labelText) >= avail) {
|
|
376
376
|
labelOut = truncateToWidth(labelText, avail);
|
|
@@ -41,26 +41,24 @@ export function draftStateEqual(a, b) {
|
|
|
41
41
|
// Recognize a MODIFIED Enter delivered via the kitty keyboard protocol
|
|
42
42
|
// (\x1b[13;<mod>u) or modifyOtherKeys (\x1b[27;<mod>;13~). The xterm modifier
|
|
43
43
|
// param is (1 + bitmask) where the bitmask bits are shift=1, alt=2, ctrl=4. We
|
|
44
|
-
// treat
|
|
45
|
-
//
|
|
46
|
-
// separately as the protocol-independent fallback.
|
|
47
|
-
const
|
|
44
|
+
// treat Shift+Enter, Alt/Meta+Enter, and Ctrl+Enter as newline chords. Claude
|
|
45
|
+
// Code maps Shift/Meta+Enter to newline; Ctrl+Enter remains a compatible Mixdog
|
|
46
|
+
// extension. Ctrl+J is handled separately as the protocol-independent fallback.
|
|
47
|
+
const MODIFIED_ENTER_NEWLINE = 1 | 2 | 4;
|
|
48
48
|
|
|
49
49
|
export function isModifiedEnterSequence(input) {
|
|
50
50
|
const text = String(input ?? '');
|
|
51
51
|
const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
|
|
52
52
|
if (!body) return false;
|
|
53
53
|
const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
|
|
54
|
-
if (kitty) return ((Number(kitty[1]) - 1) &
|
|
54
|
+
if (kitty) return ((Number(kitty[1]) - 1) & MODIFIED_ENTER_NEWLINE) !== 0;
|
|
55
55
|
const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
|
|
56
|
-
return Boolean(modifyOtherKeys && (((Number(modifyOtherKeys[1]) - 1) &
|
|
56
|
+
return Boolean(modifyOtherKeys && (((Number(modifyOtherKeys[1]) - 1) & MODIFIED_ENTER_NEWLINE) !== 0));
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
// Recognize ANY modified Enter
|
|
60
|
-
// /
|
|
61
|
-
//
|
|
62
|
-
// under modifyOtherKeys. Plain Enter (mod param = 1, bitmask 0) is intentionally
|
|
63
|
-
// NOT matched, so it still submits.
|
|
59
|
+
// Recognize ANY modified Enter. Used to consume uncommon modifier combinations
|
|
60
|
+
// outside the Shift/Alt/Ctrl newline set so raw CSI bytes never reach the draft.
|
|
61
|
+
// Plain Enter (mod param = 1, bitmask 0) intentionally remains a submit.
|
|
64
62
|
export function isAnyModifiedEnterSequence(input) {
|
|
65
63
|
const text = String(input ?? '');
|
|
66
64
|
const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
|