mixdog 0.9.2 → 0.9.3
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 +2 -1
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/tool-smoke.mjs +7 -4
- package/src/agents/debugger/AGENT.md +2 -2
- package/src/agents/heavy-worker/AGENT.md +20 -11
- package/src/agents/reviewer/AGENT.md +2 -2
- package/src/agents/worker/AGENT.md +17 -11
- package/src/mixdog-session-runtime.mjs +424 -1812
- package/src/repl.mjs +5 -5
- package/src/rules/agent/30-explorer.md +8 -11
- package/src/rules/lead/lead-tool.md +9 -5
- package/src/rules/shared/01-tool.md +11 -5
- package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +22 -68
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +46 -7
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +7 -5
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +33 -23
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +31 -14
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +169 -918
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +65 -1032
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
- package/src/runtime/agent/orchestrator/tools/patch.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/index.mjs +14 -233
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +8 -0
- package/src/runtime/channels/lib/telegram-format.mjs +19 -22
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/memory/index.mjs +314 -359
- package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +56 -3
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +20 -0
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/tool-defs.mjs +4 -4
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/transcript-writer.mjs +29 -2
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +49 -10
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/explore-tool.mjs +10 -3
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +14 -3
- package/src/tui/App.jsx +884 -143
- package/src/tui/components/PromptInput.jsx +272 -15
- package/src/tui/components/ToolExecution.jsx +20 -10
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/dist/index.mjs +1771 -1947
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +311 -851
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +2 -2
- package/src/tui/lib/voice-recorder.mjs +35 -19
- package/src/tui/markdown/format-token.mjs +7 -8
- package/src/tui/markdown/format-token.test.mjs +3 -3
- package/src/tui/paste-attachments.mjs +38 -0
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/themes/base.mjs +2 -2
- package/src/tui/themes/kanagawa.mjs +4 -4
- package/src/tui/themes/teal.mjs +4 -5
- package/src/tui/themes/utils.mjs +1 -1
- package/src/ui/statusline.mjs +49 -0
- package/src/workflows/default/WORKFLOW.md +16 -9
- package/src/workflows/sequential/WORKFLOW.md +16 -11
- package/src/workflows/solo/WORKFLOW.md +5 -1
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
replaceSelection,
|
|
39
39
|
selectionRange,
|
|
40
40
|
verticalOffset,
|
|
41
|
+
wordRangeAt,
|
|
41
42
|
} from '../input-editing.mjs';
|
|
42
43
|
|
|
43
44
|
function hintStyle(tone) {
|
|
@@ -52,6 +53,26 @@ function hintStyle(tone) {
|
|
|
52
53
|
// paddingX of 1, so the typing start can sit directly against that padding
|
|
53
54
|
// without an extra guard column.
|
|
54
55
|
const IME_LEFT_GUARD_COLUMNS = 0;
|
|
56
|
+
|
|
57
|
+
// Monotone voice indicator glyphs — right end of the prompt box. All frames
|
|
58
|
+
// are 1-cell-wide (no emoji, no string-width special-casing needed). Modes:
|
|
59
|
+
// 'off' — voice disabled and no install in flight: renders nothing,
|
|
60
|
+
// zero reserved columns.
|
|
61
|
+
// 'installing' — voice-runtime download in progress (progressHint active):
|
|
62
|
+
// braille spinner only.
|
|
63
|
+
// 'idle' — voice enabled, recorder idle: a slow "breathing" pulse
|
|
64
|
+
// (◎ ◍ ◉ ◍) so the indicator isn't a dead static glyph.
|
|
65
|
+
// 'recording' — steady ◉ + a braille spinner riding next to it (2 cells).
|
|
66
|
+
// 'transcribing' — braille spinner only (recording just stopped, awaiting
|
|
67
|
+
// the whisper-server round-trip).
|
|
68
|
+
// A single ~120ms interval drives BOTH the fast braille cycle and the slower
|
|
69
|
+
// idle pulse (idle advances one frame every VOICE_IDLE_FRAME_TICKS ticks) —
|
|
70
|
+
// one timer instead of two, and it only exists while the indicator is
|
|
71
|
+
// visible (mode !== 'off'); it's torn down otherwise, never a permanent timer.
|
|
72
|
+
const VOICE_BRAILLE_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧'];
|
|
73
|
+
const VOICE_IDLE_FRAMES = ['◎', '◍', '◉', '◍'];
|
|
74
|
+
const VOICE_TICK_MS = 120;
|
|
75
|
+
const VOICE_IDLE_FRAME_TICKS = 5; // ~600ms per idle frame (5 * 120ms)
|
|
55
76
|
// Coalesce prompt mouse-drag extend commits (SGR motion can fire faster than ink
|
|
56
77
|
// needs to immediate-render). Matches transcript selection paint cadence.
|
|
57
78
|
const MOUSE_EXTEND_COALESCE_MS = 24;
|
|
@@ -150,10 +171,12 @@ export function PromptInput({
|
|
|
150
171
|
onHistoryNavigate,
|
|
151
172
|
onPasteText,
|
|
152
173
|
onVoiceToggle,
|
|
174
|
+
voiceIndicatorMode = 'off',
|
|
153
175
|
selectionRef,
|
|
154
176
|
valueRef,
|
|
155
177
|
boxRectRef,
|
|
156
178
|
mouseSelectionRef,
|
|
179
|
+
suppressShiftNavRef,
|
|
157
180
|
}) {
|
|
158
181
|
const [draft, setDraft] = useState(() => {
|
|
159
182
|
const value = String(initialValue || '');
|
|
@@ -171,8 +194,31 @@ export function PromptInput({
|
|
|
171
194
|
const boxRef = useRef(null);
|
|
172
195
|
const cursorEnabledRef = useRef(false); // latest enabled state, read by the anchor fn at render time
|
|
173
196
|
const contentWidthRef = useRef(80);
|
|
197
|
+
// Cells reserved on the right for the voice indicator glyph(s) (0 when
|
|
198
|
+
// hidden, 1 idle/spinner-only, 2 recording's "◉"+braille). Read by the
|
|
199
|
+
// cursor-anchor closure below (installed ONCE, so it must read live state
|
|
200
|
+
// via a ref rather than close over a render-local variable) to keep the
|
|
201
|
+
// caret/wrap math in sync with the actual reserved column, same pattern as
|
|
202
|
+
// IME_LEFT_GUARD_COLUMNS on the left edge.
|
|
203
|
+
const voiceIndicatorWidthRef = useRef(0);
|
|
174
204
|
const preferredColumnRef = useRef(null);
|
|
175
205
|
const mouseExtendCoalesceRef = useRef({ pendingNext: null, timer: null, t: 0 });
|
|
206
|
+
// Voice indicator animation tick. Runs a single ~120ms interval ONLY while
|
|
207
|
+
// voiceIndicatorMode !== 'off' (installing/idle/recording/transcribing) —
|
|
208
|
+
// torn down the instant the mode goes 'off', so no permanent timer exists
|
|
209
|
+
// when voice is disabled and no install is in flight.
|
|
210
|
+
const [voiceTick, setVoiceTick] = useState(0);
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (voiceIndicatorMode === 'off') return undefined;
|
|
213
|
+
const timer = setInterval(() => setVoiceTick((t) => (t + 1) % 1000000), VOICE_TICK_MS);
|
|
214
|
+
timer.unref?.();
|
|
215
|
+
return () => clearInterval(timer);
|
|
216
|
+
}, [voiceIndicatorMode]);
|
|
217
|
+
// Undo/redo snapshot stack. Each entry is a { value, cursor, selectionAnchor }
|
|
218
|
+
// draft snapshot. Continuous typing coalesces (see UNDO_COALESCE_MS) so a run
|
|
219
|
+
// of characters collapses into one undo step; cursor-only moves are NOT
|
|
220
|
+
// snapshotted. Reset on submit / draftOverride.
|
|
221
|
+
const undoRef = useRef({ past: [], future: [], lastPushAt: 0, lastValue: null });
|
|
176
222
|
const { value, cursor } = draft;
|
|
177
223
|
draftRef.current = draft;
|
|
178
224
|
if (selectionRef) {
|
|
@@ -204,6 +250,7 @@ export function PromptInput({
|
|
|
204
250
|
const sameDraft = draftStateEqual(draftRef.current, next);
|
|
205
251
|
if (!options.keepPreferredColumn) preferredColumnRef.current = null;
|
|
206
252
|
if (sameDraft) return;
|
|
253
|
+
if (!options.skipHistory) recordUndoSnapshot(draftRef.current, next, options);
|
|
207
254
|
draftRef.current = next;
|
|
208
255
|
setDraft(next);
|
|
209
256
|
queueMicrotask(flushImmediate);
|
|
@@ -248,10 +295,19 @@ export function PromptInput({
|
|
|
248
295
|
const d = draftRef.current;
|
|
249
296
|
const w = yogaNode?.getComputedWidth?.() ?? 0;
|
|
250
297
|
const guardColumns = w > IME_LEFT_GUARD_COLUMNS ? IME_LEFT_GUARD_COLUMNS : 0;
|
|
251
|
-
|
|
298
|
+
// Reserve the voice-indicator's own cells (+1 gap column when visible)
|
|
299
|
+
// out of the SAME width the caret/wrap math uses, mirroring the left
|
|
300
|
+
// guard above — otherwise a visible indicator would silently overlap
|
|
301
|
+
// the caret/typed text at the right edge instead of pushing content in.
|
|
302
|
+
const voiceReserve = voiceIndicatorWidthRef.current > 0 ? voiceIndicatorWidthRef.current + 1 : 0;
|
|
303
|
+
const contentWidth = Math.max(1, (w ? w - guardColumns - voiceReserve : contentWidthRef.current) || 80);
|
|
252
304
|
contentWidthRef.current = contentWidth;
|
|
253
305
|
const caret = w > 0
|
|
254
|
-
|
|
306
|
+
// PromptInput renders a trailing space cell when the cursor is at
|
|
307
|
+
// end-of-input, so a caret flush on the last column there still has a
|
|
308
|
+
// following cell — pass hasTrailingContent=true so it rolls to row N+1
|
|
309
|
+
// exactly as ink wraps the trailing space.
|
|
310
|
+
? caretPosition(d.value, d.cursor, contentWidth, d.cursor >= d.value.length ? true : undefined)
|
|
255
311
|
: { row: 0, col: stringWidth(d.value.slice(0, d.cursor)) };
|
|
256
312
|
return w > 0
|
|
257
313
|
? { ...caret, col: caret.col + guardColumns }
|
|
@@ -264,6 +320,70 @@ export function PromptInput({
|
|
|
264
320
|
commitDraft(fn(draftRef.current), options);
|
|
265
321
|
};
|
|
266
322
|
|
|
323
|
+
// --- undo/redo -----------------------------------------------------------
|
|
324
|
+
// Continuous-typing coalesce window: successive value-changing edits within
|
|
325
|
+
// this window collapse into a single undo step.
|
|
326
|
+
const UNDO_COALESCE_MS = 500;
|
|
327
|
+
const UNDO_MAX = 100;
|
|
328
|
+
|
|
329
|
+
const snapshotOf = (d) => ({ value: d.value, cursor: d.cursor, selectionAnchor: d.selectionAnchor ?? null });
|
|
330
|
+
|
|
331
|
+
const resetUndo = () => {
|
|
332
|
+
undoRef.current = { past: [], future: [], lastPushAt: 0, lastValue: null };
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// Record a snapshot of the PREVIOUS state before applying `next`. Cursor-only
|
|
336
|
+
// moves (value unchanged) are never snapshotted. Consecutive value edits
|
|
337
|
+
// within UNDO_COALESCE_MS coalesce (we keep only the first snapshot of the
|
|
338
|
+
// run, so a single undo reverts the whole burst).
|
|
339
|
+
const recordUndoSnapshot = (prev, next, options = {}) => {
|
|
340
|
+
const stack = undoRef.current;
|
|
341
|
+
if (prev.value === next.value) {
|
|
342
|
+
// Cursor/selection-only move: don't snapshot, but BREAK the coalesce run
|
|
343
|
+
// so a following edit starts a fresh undo step (typing→move→typing must
|
|
344
|
+
// not collapse into one undo).
|
|
345
|
+
stack.lastPushAt = 0;
|
|
346
|
+
stack.lastValue = next.value;
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const now = Date.now();
|
|
350
|
+
const coalesce = !options.undoBreak
|
|
351
|
+
&& stack.past.length > 0
|
|
352
|
+
&& (now - stack.lastPushAt) < UNDO_COALESCE_MS
|
|
353
|
+
&& stack.lastValue === prev.value;
|
|
354
|
+
if (!coalesce) {
|
|
355
|
+
stack.past.push(snapshotOf(prev));
|
|
356
|
+
if (stack.past.length > UNDO_MAX) stack.past.shift();
|
|
357
|
+
}
|
|
358
|
+
stack.lastPushAt = now;
|
|
359
|
+
stack.lastValue = next.value;
|
|
360
|
+
stack.future = [];
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const performUndo = () => {
|
|
364
|
+
const stack = undoRef.current;
|
|
365
|
+
if (stack.past.length === 0) return false;
|
|
366
|
+
const prev = stack.past.pop();
|
|
367
|
+
stack.future.push(snapshotOf(draftRef.current));
|
|
368
|
+
stack.lastPushAt = 0;
|
|
369
|
+
stack.lastValue = prev.value;
|
|
370
|
+
commitDraft(prev, { skipHistory: true });
|
|
371
|
+
return true;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const performRedo = () => {
|
|
375
|
+
const stack = undoRef.current;
|
|
376
|
+
if (stack.future.length === 0) return false;
|
|
377
|
+
const next = stack.future.pop();
|
|
378
|
+
stack.past.push(snapshotOf(draftRef.current));
|
|
379
|
+
if (stack.past.length > UNDO_MAX) stack.past.shift();
|
|
380
|
+
stack.lastPushAt = 0;
|
|
381
|
+
stack.lastValue = next.value;
|
|
382
|
+
commitDraft(next, { skipHistory: true });
|
|
383
|
+
return true;
|
|
384
|
+
};
|
|
385
|
+
// -------------------------------------------------------------------------
|
|
386
|
+
|
|
267
387
|
const cancelMouseExtendCoalesce = () => {
|
|
268
388
|
const state = mouseExtendCoalesceRef.current;
|
|
269
389
|
if (state.timer) {
|
|
@@ -332,6 +452,28 @@ export function PromptInput({
|
|
|
332
452
|
const anchor = Number.isFinite(d.selectionAnchor) ? d.selectionAnchor : d.cursor;
|
|
333
453
|
queueMouseExtendCommit({ ...d, cursor: off, selectionAnchor: anchor }, immediate);
|
|
334
454
|
},
|
|
455
|
+
hasSelection: () => selectionRange(draftRef.current) != null,
|
|
456
|
+
// Double-click word select: pick the word/punctuation run under the
|
|
457
|
+
// clicked offset and set selectionAnchor/cursor to its bounds so it
|
|
458
|
+
// paints via the normal selection highlight. Triple-click line select
|
|
459
|
+
// uses lineStart/lineEnd instead (see selectLineAt).
|
|
460
|
+
selectWordAt: (offset) => {
|
|
461
|
+
cancelMouseExtendCoalesce();
|
|
462
|
+
const value = draftRef.current.value;
|
|
463
|
+
const off = Math.max(0, Math.min(value.length, Math.floor(Number(offset) || 0)));
|
|
464
|
+
const { start, end } = wordRangeAt(value, off);
|
|
465
|
+
commitDraft({ ...draftRef.current, cursor: end, selectionAnchor: start });
|
|
466
|
+
mouseExtendCoalesceRef.current.t = Date.now();
|
|
467
|
+
},
|
|
468
|
+
selectLineAt: (offset) => {
|
|
469
|
+
cancelMouseExtendCoalesce();
|
|
470
|
+
const value = draftRef.current.value;
|
|
471
|
+
const off = Math.max(0, Math.min(value.length, Math.floor(Number(offset) || 0)));
|
|
472
|
+
const start = lineStart(value, off);
|
|
473
|
+
const end = lineEnd(value, off);
|
|
474
|
+
commitDraft({ ...draftRef.current, cursor: end, selectionAnchor: start });
|
|
475
|
+
mouseExtendCoalesceRef.current.t = Date.now();
|
|
476
|
+
},
|
|
335
477
|
clear: () => {
|
|
336
478
|
cancelMouseExtendCoalesce();
|
|
337
479
|
if (selectionRange(draftRef.current)) commitDraft(clearSelection(draftRef.current));
|
|
@@ -405,7 +547,8 @@ export function PromptInput({
|
|
|
405
547
|
|
|
406
548
|
useEffect(() => {
|
|
407
549
|
if (!draftOverride || typeof draftOverride.value !== 'string') return;
|
|
408
|
-
commitDraft({ value: draftOverride.value, cursor: draftOverride.value.length, selectionAnchor: null });
|
|
550
|
+
commitDraft({ value: draftOverride.value, cursor: draftOverride.value.length, selectionAnchor: null }, { skipHistory: true });
|
|
551
|
+
resetUndo();
|
|
409
552
|
}, [draftOverride?.id]);
|
|
410
553
|
|
|
411
554
|
useEffect(() => () => {
|
|
@@ -419,7 +562,8 @@ export function PromptInput({
|
|
|
419
562
|
commitDraft(next);
|
|
420
563
|
return;
|
|
421
564
|
}
|
|
422
|
-
commitDraft({ value: '', cursor: 0, selectionAnchor: null });
|
|
565
|
+
commitDraft({ value: '', cursor: 0, selectionAnchor: null }, { skipHistory: true });
|
|
566
|
+
resetUndo();
|
|
423
567
|
};
|
|
424
568
|
|
|
425
569
|
const submitEnterChunk = (prefix = '') => {
|
|
@@ -450,6 +594,21 @@ export function PromptInput({
|
|
|
450
594
|
const rawInput = String(input ?? '');
|
|
451
595
|
const inputKey = rawInput.toLowerCase();
|
|
452
596
|
|
|
597
|
+
// App owns Shift+Arrow when a transcript/status ink-grid selection is live.
|
|
598
|
+
// Because the parent (App) useInput handler fires AFTER this child handler
|
|
599
|
+
// for the same event, a flag SET in App's handler is always one event stale.
|
|
600
|
+
// Instead call a synchronous predicate derived from dragRef at event time.
|
|
601
|
+
const gridSelectionActive = typeof suppressShiftNavRef === 'function'
|
|
602
|
+
? suppressShiftNavRef()
|
|
603
|
+
: (typeof suppressShiftNavRef?.current === 'function'
|
|
604
|
+
? suppressShiftNavRef.current()
|
|
605
|
+
: Boolean(suppressShiftNavRef?.current));
|
|
606
|
+
if (gridSelectionActive) {
|
|
607
|
+
const isShiftArrow = key.shift
|
|
608
|
+
&& (key.leftArrow || key.rightArrow || key.upArrow || key.downArrow || key.home || key.end);
|
|
609
|
+
if (isShiftArrow) return;
|
|
610
|
+
}
|
|
611
|
+
|
|
453
612
|
// Drop SGR mouse-tracking sequences (wheel/click). When app mouse tracking
|
|
454
613
|
// is explicitly enabled, App parses these off raw stdin itself;
|
|
455
614
|
// ink still forwards the bytes here as "input", which would otherwise type
|
|
@@ -482,7 +641,22 @@ export function PromptInput({
|
|
|
482
641
|
const rawShiftDown = rawInput === '\x1b[1;2B' || rawInput === '\x1b[b' || rawInput === '[1;2B';
|
|
483
642
|
const rawShiftRight = rawInput === '\x1b[1;2C' || rawInput === '\x1b[c' || rawInput === '[1;2C';
|
|
484
643
|
const rawShiftLeft = rawInput === '\x1b[1;2D' || rawInput === '\x1b[d' || rawInput === '[1;2D';
|
|
485
|
-
|
|
644
|
+
// Ctrl+Shift+Arrow modifier sequences: xterm mod=6 (1 + shift(1) + ctrl(4))
|
|
645
|
+
// arrives as `\x1b[1;6<dir>`; kitty keyboard protocol reports the same chord
|
|
646
|
+
// as `\x1b[<code>;6<dir>` (also mod=6). Ink decodes neither the `;6` for
|
|
647
|
+
// arrows, so the bytes arrive raw. Fold into a ctrlShiftHeld signal used to
|
|
648
|
+
// drive whole-word selection-extend below. `\x1b[1;6<dir>` covers both the
|
|
649
|
+
// classic xterm form and kitty's default (which emits the legacy arrow form
|
|
650
|
+
// with the CSI-u modifier param for arrow keys).
|
|
651
|
+
const rawCtrlShiftUp = rawInput === '\x1b[1;6A' || rawInput === '[1;6A';
|
|
652
|
+
const rawCtrlShiftDown = rawInput === '\x1b[1;6B' || rawInput === '[1;6B';
|
|
653
|
+
const rawCtrlShiftRight = rawInput === '\x1b[1;6C' || rawInput === '[1;6C';
|
|
654
|
+
const rawCtrlShiftLeft = rawInput === '\x1b[1;6D' || rawInput === '[1;6D';
|
|
655
|
+
const ctrlShiftHeld =
|
|
656
|
+
rawCtrlShiftUp || rawCtrlShiftDown || rawCtrlShiftLeft || rawCtrlShiftRight
|
|
657
|
+
|| (key.shift && key.ctrl && (key.leftArrow || key.rightArrow || key.upArrow || key.downArrow));
|
|
658
|
+
const shiftHeld = key.shift || rawShiftUp || rawShiftDown || rawShiftLeft || rawShiftRight
|
|
659
|
+
|| rawCtrlShiftUp || rawCtrlShiftDown || rawCtrlShiftLeft || rawCtrlShiftRight;
|
|
486
660
|
const lineBreakIndex = rawInput.search(/[\r\n]/);
|
|
487
661
|
const rawEnter = rawInput === '\r' || rawInput === '\n' || rawInput === '\r\n';
|
|
488
662
|
const trailingEnterPrefix = singleTrailingLineBreakPrefix(rawInput);
|
|
@@ -566,25 +740,58 @@ export function PromptInput({
|
|
|
566
740
|
return;
|
|
567
741
|
}
|
|
568
742
|
|
|
569
|
-
|
|
743
|
+
// Ctrl+Shift+Left/Right → extend selection whole-word. Kept before the plain
|
|
744
|
+
// shift-arrow branches so the ctrl+shift chord never falls through to a
|
|
745
|
+
// char-wise extend. (Up/Down ctrl+shift extend to line-relative vertical
|
|
746
|
+
// move with extend — same as shift alone; handled in the vertical branch.)
|
|
747
|
+
if (ctrlShiftHeld && (rawCtrlShiftLeft || (key.ctrl && key.shift && key.leftArrow))) {
|
|
748
|
+
if (!commandPaletteActive) {
|
|
749
|
+
updateDraft((d) => moveCursor(d, previousWordOffset(d.value, d.cursor), { extend: true }));
|
|
750
|
+
}
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (ctrlShiftHeld && (rawCtrlShiftRight || (key.ctrl && key.shift && key.rightArrow))) {
|
|
754
|
+
if (!commandPaletteActive) {
|
|
755
|
+
updateDraft((d) => moveCursor(d, nextWordOffset(d.value, d.cursor), { extend: true }));
|
|
756
|
+
}
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (key.upArrow || rawUpArrow || rawShiftUp || rawCtrlShiftUp) {
|
|
570
761
|
if (commandPaletteActive) {
|
|
571
762
|
onCommandPaletteNavigate?.(-1);
|
|
572
763
|
} else {
|
|
573
|
-
|
|
574
|
-
if
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
764
|
+
// A Shift-held Up is a SELECTION gesture, never history navigation:
|
|
765
|
+
// extend the selection up one visual line, and if already on the first
|
|
766
|
+
// line extend all the way to document start (offset 0). History
|
|
767
|
+
// navigation (restoreQueued / applyHistoryNavigation) MUST NOT fire.
|
|
768
|
+
if (shiftHeld) {
|
|
769
|
+
if (!moveDraftVertically(-1, { extend: true })) {
|
|
770
|
+
updateDraft((d) => moveCursor(d, 0, { extend: true }));
|
|
771
|
+
}
|
|
772
|
+
} else {
|
|
773
|
+
const hasDraftText = String(draftRef.current.value || '').trim().length > 0;
|
|
774
|
+
if (!hasDraftText) {
|
|
775
|
+
if (!restoreQueuedToDraft()) applyHistoryNavigation('up', { emptyDraft: true });
|
|
776
|
+
} else if (!moveDraftVertically(-1, { extend: false })) {
|
|
777
|
+
applyHistoryNavigation('up', { emptyDraft: false });
|
|
778
|
+
}
|
|
578
779
|
}
|
|
579
780
|
}
|
|
580
781
|
return;
|
|
581
782
|
}
|
|
582
783
|
|
|
583
|
-
if (key.downArrow || rawDownArrow || rawShiftDown) {
|
|
784
|
+
if (key.downArrow || rawDownArrow || rawShiftDown || rawCtrlShiftDown) {
|
|
584
785
|
if (commandPaletteActive) {
|
|
585
786
|
onCommandPaletteNavigate?.(1);
|
|
586
787
|
} else {
|
|
587
|
-
|
|
788
|
+
// Shift-held Down: extend selection down one line, or to document end
|
|
789
|
+
// (value.length) when already on the last line. Never history nav.
|
|
790
|
+
if (shiftHeld) {
|
|
791
|
+
if (!moveDraftVertically(1, { extend: true })) {
|
|
792
|
+
updateDraft((d) => moveCursor(d, d.value.length, { extend: true }));
|
|
793
|
+
}
|
|
794
|
+
} else if (!moveDraftVertically(1, { extend: false })) {
|
|
588
795
|
applyHistoryNavigation('down', { emptyDraft: String(draftRef.current.value || '').trim().length === 0 });
|
|
589
796
|
}
|
|
590
797
|
}
|
|
@@ -641,8 +848,8 @@ export function PromptInput({
|
|
|
641
848
|
commitDraft({ value: '', cursor: 0, selectionAnchor: null });
|
|
642
849
|
return;
|
|
643
850
|
}
|
|
644
|
-
// Active turn takes precedence over queue restore
|
|
645
|
-
//
|
|
851
|
+
// Active turn takes precedence over queue restore: Esc during a
|
|
852
|
+
// running turn interrupts the
|
|
646
853
|
// turn and leaves queued steering prompts intact to run afterward. Queued
|
|
647
854
|
// messages only pop back into the draft when the turn is idle.
|
|
648
855
|
if (interruptActive) {
|
|
@@ -702,6 +909,27 @@ export function PromptInput({
|
|
|
702
909
|
|
|
703
910
|
const editingKey = String(input || '').toLowerCase();
|
|
704
911
|
|
|
912
|
+
// Undo / redo. Covered encodings:
|
|
913
|
+
// • kitty protocol: ctrl+z → input 'z' + key.ctrl; ctrl+y → 'y' + key.ctrl;
|
|
914
|
+
// ctrl+shift+z → 'z' + key.ctrl + key.shift (redo).
|
|
915
|
+
// • legacy control bytes: Ctrl+Z is \x1a (SUB, 0x1A), Ctrl+Y is \x19 (EM,
|
|
916
|
+
// 0x19). ink may deliver these as raw input without key.ctrl on some
|
|
917
|
+
// terminals, so match the byte directly too.
|
|
918
|
+
const isCtrlZ = (key.ctrl && editingKey === 'z') || rawInput === '\x1a';
|
|
919
|
+
const isCtrlY = (key.ctrl && editingKey === 'y') || rawInput === '\x19';
|
|
920
|
+
if (isCtrlZ && (key.shift || shiftHeld)) {
|
|
921
|
+
performRedo();
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (isCtrlZ) {
|
|
925
|
+
performUndo();
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (isCtrlY) {
|
|
929
|
+
performRedo();
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
705
933
|
// ctrl+a selects all like a normal text box; ctrl+e keeps readline line-end.
|
|
706
934
|
if (key.ctrl && editingKey === 'a') {
|
|
707
935
|
updateDraft((d) => (d.value ? { ...d, cursor: d.value.length, selectionAnchor: 0 } : clearSelection(d)));
|
|
@@ -834,6 +1062,24 @@ export function PromptInput({
|
|
|
834
1062
|
const displayValue = mask ? value.replace(/[^\n]/g, '*') : value;
|
|
835
1063
|
const renderedValue = renderSelectedText(displayValue, selectionRange(draft), cursor === value.length);
|
|
836
1064
|
const hintMeta = hintStyle(hintTone);
|
|
1065
|
+
// Monotone voice indicator glyph text — zero-width (rendered as null) when
|
|
1066
|
+
// mode is 'off'. Braille frame drives installing/recording/transcribing;
|
|
1067
|
+
// the idle pulse advances one frame every VOICE_IDLE_FRAME_TICKS ticks of
|
|
1068
|
+
// the SAME interval (slower "breathing" cadence off one shared timer).
|
|
1069
|
+
const voiceBrailleFrame = VOICE_BRAILLE_FRAMES[voiceTick % VOICE_BRAILLE_FRAMES.length];
|
|
1070
|
+
const voiceIdleFrame = VOICE_IDLE_FRAMES[Math.floor(voiceTick / VOICE_IDLE_FRAME_TICKS) % VOICE_IDLE_FRAMES.length];
|
|
1071
|
+
const voiceIndicatorText = voiceIndicatorMode === 'installing' || voiceIndicatorMode === 'transcribing'
|
|
1072
|
+
? voiceBrailleFrame
|
|
1073
|
+
: voiceIndicatorMode === 'recording'
|
|
1074
|
+
? `◉${voiceBrailleFrame}`
|
|
1075
|
+
: voiceIndicatorMode === 'idle'
|
|
1076
|
+
? voiceIdleFrame
|
|
1077
|
+
: '';
|
|
1078
|
+
// stringWidth is unnecessary here — every glyph above is a single
|
|
1079
|
+
// 1-column codepoint (◎◍◉ and the braille frames), and 'recording'
|
|
1080
|
+
// concatenates exactly two 1-cell glyphs, so plain .length gives the
|
|
1081
|
+
// correct cell count without pulling in emoji-width special-casing.
|
|
1082
|
+
voiceIndicatorWidthRef.current = voiceIndicatorText.length;
|
|
837
1083
|
|
|
838
1084
|
return (
|
|
839
1085
|
<Box ref={boxRef} flexDirection="row" width="100%" flexGrow={1} flexShrink={1} backgroundColor={surfaceBackground()}>
|
|
@@ -844,6 +1090,17 @@ export function PromptInput({
|
|
|
844
1090
|
<Text color={hintMeta.textColor}>{hint}</Text>
|
|
845
1091
|
</Box>
|
|
846
1092
|
) : null}
|
|
1093
|
+
{voiceIndicatorText ? (
|
|
1094
|
+
<>
|
|
1095
|
+
{/* flexGrow spacer pins the indicator to the RIGHT edge of the row
|
|
1096
|
+
even when the typed text is short; without it the glyph would
|
|
1097
|
+
ride directly after the caret instead of the box edge. */}
|
|
1098
|
+
<Box flexGrow={1} backgroundColor={surfaceBackground()} />
|
|
1099
|
+
<Box flexShrink={0} width={voiceIndicatorText.length + 1} justifyContent="flex-end" backgroundColor={surfaceBackground()}>
|
|
1100
|
+
<Text color={theme.subtle}>{voiceIndicatorText}</Text>
|
|
1101
|
+
</Box>
|
|
1102
|
+
</>
|
|
1103
|
+
) : null}
|
|
847
1104
|
</Box>
|
|
848
1105
|
);
|
|
849
1106
|
}
|
|
@@ -571,7 +571,10 @@ function toolSearchLoadedSummary(resultText) {
|
|
|
571
571
|
try {
|
|
572
572
|
parsed = JSON.parse(String(resultText || ''));
|
|
573
573
|
} catch {
|
|
574
|
-
|
|
574
|
+
const text = String(resultText || '');
|
|
575
|
+
const match = /^Loaded deferred tools:\s*(.+)$/m.exec(text);
|
|
576
|
+
if (!match) return '';
|
|
577
|
+
return [...new Set(match[1].split(',').map((name) => name.trim()).filter(Boolean))].join(', ');
|
|
575
578
|
}
|
|
576
579
|
const tools = parsed?.selected?.tools;
|
|
577
580
|
if (!tools || typeof tools !== 'object') return '';
|
|
@@ -610,17 +613,21 @@ function clampFailureCount(errorCount, groupCount, isError) {
|
|
|
610
613
|
// Single source of truth for the tool-card dot (●) color. Both the aggregate
|
|
611
614
|
// and normal (single-tool) render paths must call this with a resolved
|
|
612
615
|
// `terminalStatus` — do not recompute color inline elsewhere.
|
|
613
|
-
// running/pending
|
|
614
|
-
//
|
|
615
|
-
// partial failure
|
|
616
|
-
// all failed
|
|
617
|
-
// cancelled
|
|
616
|
+
// running/pending -> theme.text (white; blink handled by caller)
|
|
617
|
+
// success -> theme.success
|
|
618
|
+
// partial failure -> mixdogOrange || warning (some, not all, of the group failed)
|
|
619
|
+
// all failed -> theme.error
|
|
620
|
+
// cancelled -> theme.warning
|
|
621
|
+
// Note: callers resolve terminalStatus to 'failed' whenever failedCount > 0
|
|
622
|
+
// (they cannot tell partial from total failure), so a 'failed' status must
|
|
623
|
+
// still fall through to the failedCount check below rather than short-circuit
|
|
624
|
+
// to error — otherwise a mixed-success batch (e.g. 1 of 3 failed) renders as
|
|
625
|
+
// full-failure red instead of partial orange.
|
|
618
626
|
function toolStatusColor({ pending, groupCount, failedCount, terminalStatus = '' }) {
|
|
619
|
-
if (pending) return theme.
|
|
627
|
+
if (pending) return theme.text;
|
|
620
628
|
const status = normalizeTerminalStatus(terminalStatus);
|
|
621
629
|
if (status === 'cancelled') return theme.warning;
|
|
622
|
-
if (status === 'failed'
|
|
623
|
-
if (failedCount <= 0) return theme.success;
|
|
630
|
+
if (failedCount <= 0) return status === 'failed' ? theme.error : theme.success;
|
|
624
631
|
if (groupCount > 1 && failedCount < groupCount) return theme.mixdogOrange || theme.warning;
|
|
625
632
|
return theme.error;
|
|
626
633
|
}
|
|
@@ -809,7 +816,10 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
809
816
|
// Aggregate cards intentionally omit elapsed time once grouped. A brief
|
|
810
817
|
// `Running · 1s` tick during the grouped→finished handoff reads as visual
|
|
811
818
|
// noise, and the grouped header already communicates that work is active.
|
|
812
|
-
|
|
819
|
+
// The placeholder tracks `pending` (real completion), NOT headerPending:
|
|
820
|
+
// the header verb stays active until the block seals, but the detail row
|
|
821
|
+
// must not keep saying "Running" after every call already resolved.
|
|
822
|
+
const pendingPlaceholder = pending
|
|
813
823
|
? 'Running'
|
|
814
824
|
: 'Finished';
|
|
815
825
|
const detailLines = showRawAggregate
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* components/tool-output-format.mjs — shared post-processing for EXPANDED tool
|
|
3
|
-
* result bodies (ctrl+o)
|
|
4
|
-
*
|
|
3
|
+
* result bodies (ctrl+o), so every tool surface renders the same way instead
|
|
4
|
+
* of a flat run of plain lines:
|
|
5
5
|
*
|
|
6
6
|
* - language inference from the read/grep path argument (file extension)
|
|
7
7
|
* - JSON auto pretty-print (precision-safe, size-capped)
|