mixdog 0.9.18 → 0.9.19
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 +3 -2
- package/scripts/output-style-smoke.mjs +2 -2
- package/scripts/recall-bench-cases.json +11 -0
- package/scripts/recall-bench.mjs +91 -2
- package/scripts/tool-efficiency-diag.mjs +4 -1
- package/scripts/tool-smoke.mjs +101 -27
- package/src/agents/debugger/AGENT.md +3 -3
- package/src/agents/heavy-worker/AGENT.md +7 -10
- package/src/agents/maintainer/AGENT.md +1 -2
- package/src/agents/reviewer/AGENT.md +1 -2
- package/src/agents/worker/AGENT.md +5 -8
- package/src/defaults/agents.json +3 -0
- package/src/mixdog-session-runtime.mjs +23 -6
- package/src/rules/agent/00-core.md +4 -3
- package/src/rules/agent/30-explorer.md +53 -22
- package/src/rules/lead/lead-tool.md +3 -2
- package/src/rules/shared/01-tool.md +24 -29
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +24 -3
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +250 -35
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +198 -6
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +13 -15
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +45 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +195 -3
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +38 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +1 -1
- package/src/runtime/channels/backends/discord-gateway.mjs +14 -1
- package/src/runtime/channels/backends/discord.mjs +43 -1
- package/src/runtime/channels/index.mjs +145 -58
- package/src/runtime/channels/lib/inbound-routing.mjs +19 -12
- package/src/runtime/channels/lib/memory-client.mjs +32 -14
- package/src/runtime/channels/lib/output-forwarder.mjs +38 -7
- package/src/runtime/channels/lib/owner-heartbeat.mjs +13 -4
- package/src/runtime/channels/lib/runtime-paths.mjs +35 -1
- package/src/runtime/channels/lib/tool-dispatch.mjs +17 -7
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +8 -2
- package/src/runtime/memory/lib/memory-recall-store.mjs +182 -9
- package/src/runtime/memory/lib/query-handlers.mjs +36 -4
- package/src/runtime/memory/lib/recall-format.mjs +106 -6
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +10 -4
- package/src/runtime/shared/background-tasks.mjs +4 -2
- package/src/runtime/shared/tool-execution-contract.mjs +1 -1
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +30 -1
- package/src/session-runtime/config-lifecycle.mjs +1 -1
- package/src/session-runtime/cwd-plugins.mjs +46 -3
- package/src/session-runtime/mcp-glue.mjs +24 -3
- package/src/session-runtime/output-styles.mjs +44 -10
- package/src/session-runtime/workflow.mjs +16 -1
- package/src/standalone/channel-worker.mjs +74 -5
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/tui/App.jsx +57 -77
- package/src/tui/app/channel-pickers.mjs +45 -0
- package/src/tui/app/slash-commands.mjs +0 -1
- package/src/tui/app/slash-dispatch.mjs +0 -16
- package/src/tui/app/transcript-window.mjs +44 -1
- package/src/tui/app/use-mouse-input.mjs +9 -2
- package/src/tui/app/use-prompt-handlers.mjs +7 -94
- package/src/tui/app/use-transcript-scroll.mjs +5 -1
- package/src/tui/app/use-transcript-window.mjs +65 -5
- package/src/tui/components/PromptInput.jsx +33 -64
- package/src/tui/components/ToolExecution.jsx +2 -2
- package/src/tui/dist/index.mjs +5330 -5443
- package/src/tui/engine.mjs +49 -10
- package/src/tui/lib/voice-setup.mjs +166 -0
- package/src/tui/paste-attachments.mjs +12 -5
- package/src/tui/prompt-history-store.mjs +125 -12
- package/scripts/bench/cache-probe-tasks.json +0 -8
- package/scripts/bench/lead-review-tasks-r3.json +0 -20
- package/scripts/bench/lead-review-tasks.json +0 -20
- package/scripts/bench/r4-mixed-tasks.json +0 -20
- package/scripts/bench/r5-orchestrated-task.json +0 -7
- package/scripts/bench/review-tasks.json +0 -20
- package/scripts/bench/round-codex.json +0 -114
- package/scripts/bench/round-mixdog-lead-r3.json +0 -269
- package/scripts/bench/round-mixdog-lead.json +0 -269
- package/scripts/bench/round-mixdog.json +0 -126
- package/scripts/bench/round-r10-bigsample.json +0 -679
- package/scripts/bench/round-r11-codexalign.json +0 -257
- package/scripts/bench/round-r13-clientmeta.json +0 -464
- package/scripts/bench/round-r14-betafeatures.json +0 -466
- package/scripts/bench/round-r15-fulldefault.json +0 -462
- package/scripts/bench/round-r16-sessionid.json +0 -466
- package/scripts/bench/round-r17-wirebytes.json +0 -456
- package/scripts/bench/round-r18-prewarm.json +0 -468
- package/scripts/bench/round-r19-clean.json +0 -472
- package/scripts/bench/round-r20-prewarm-clean.json +0 -475
- package/scripts/bench/round-r21-delta-retry.json +0 -473
- package/scripts/bench/round-r22-full-probe.json +0 -693
- package/scripts/bench/round-r23-itemprobe.json +0 -701
- package/scripts/bench/round-r24-shapefix.json +0 -677
- package/scripts/bench/round-r25-serial.json +0 -464
- package/scripts/bench/round-r26-parallel3.json +0 -671
- package/scripts/bench/round-r27-parallel10.json +0 -894
- package/scripts/bench/round-r28-parallel10-stagger.json +0 -882
- package/scripts/bench/round-r29-parallel10-stagger166.json +0 -886
- package/scripts/bench/round-r30-instid.json +0 -253
- package/scripts/bench/round-r31-upgradeprobe.json +0 -256
- package/scripts/bench/round-r32-vs-codex-lead.json +0 -254
- package/scripts/bench/round-r33-vs-codex-codex.json +0 -115
- package/scripts/bench/round-r34-orchestrated.json +0 -120
- package/scripts/bench/round-r35-orchestrated-codex.json +0 -61
- package/scripts/bench/round-r36-orchestrated-capped.json +0 -128
- package/scripts/bench/round-r4-codex.json +0 -114
- package/scripts/bench/round-r4-mixed.json +0 -225
- package/scripts/bench/round-r5-gpt-lead.json +0 -259
- package/scripts/bench/round-r6-codex.json +0 -114
- package/scripts/bench/round-r6-solo.json +0 -257
- package/scripts/bench/round-r7-full.json +0 -254
- package/scripts/bench/round-r8-fulldefault.json +0 -255
- package/src/tui/components/prompt-input/voice-indicator.mjs +0 -39
- package/src/tui/lib/voice-recorder.mjs +0 -469
|
@@ -22,6 +22,9 @@ import {
|
|
|
22
22
|
setStreamingBottomPinned,
|
|
23
23
|
transcriptRenderWindow,
|
|
24
24
|
resolveAnchorScrollOffset,
|
|
25
|
+
streamingTailMountedGrowth,
|
|
26
|
+
streamingEstimateRows,
|
|
27
|
+
TRANSCRIPT_WINDOW_OVERSCAN_ROWS,
|
|
25
28
|
upperBound,
|
|
26
29
|
shiftSelectionRectY,
|
|
27
30
|
} from './transcript-window.mjs';
|
|
@@ -347,6 +350,37 @@ export function useTranscriptWindow({
|
|
|
347
350
|
contentHeight: lockViewRows,
|
|
348
351
|
floatingPanelRows: Number(floatingPanelRows) || 0,
|
|
349
352
|
};
|
|
353
|
+
// ── Same-frame streaming-tail growth compensation (scrolled-up only) ───────
|
|
354
|
+
// When the user reads older transcript, the row index freezes the trailing
|
|
355
|
+
// streaming item at its last Yoga-confirmed height (defer-growth), but the
|
|
356
|
+
// mounted Box lays out at the live-grown height THIS frame. renderScrollOffset
|
|
357
|
+
// was just resolved against that frozen row index, so the extra physical rows
|
|
358
|
+
// are unaccounted-for and the visible window jumps up one frame until the
|
|
359
|
+
// harvest bumps measuredRowsVersion. Fold the mounted growth delta into the
|
|
360
|
+
// offset NOW so marginBottom slides those extra rows off the bottom and the
|
|
361
|
+
// window holds still. Bottom-pinned/following is stable (flex-end owns it) and
|
|
362
|
+
// is excluded by the scrolledUp gate. No double-compensation next frame: once
|
|
363
|
+
// measuredRowsVersion bumps, the row index absorbs the growth (idEntry.rows ==
|
|
364
|
+
// the new measured height) and the live estimate matches it, so delta → 0.
|
|
365
|
+
if (scrolledUp && !followingRef.current) {
|
|
366
|
+
const growth = streamingTailMountedGrowth(items, frameColumns, toolOutputExpanded);
|
|
367
|
+
// Only compensate when the tail is actually mounted in the rendered slice
|
|
368
|
+
// (viewport + overscan). Off-slice it is represented by a row-index-sized
|
|
369
|
+
// bottom spacer that does NOT physically grow this frame, so shifting the
|
|
370
|
+
// offset there would itself introduce a jump. In-slice its Box grows now.
|
|
371
|
+
// Gate/clamp on the POST-compensation offset: transcriptRenderWindow drops
|
|
372
|
+
// the tail once effectiveScrollOffset >= tailRows + overscan, so adding the
|
|
373
|
+
// full delta could push the offset past that edge and unmount the very row
|
|
374
|
+
// we are compensating for (a reverse jump at the boundary). Cap the applied
|
|
375
|
+
// delta so the final offset keeps the tail mounted; any residual growth left
|
|
376
|
+
// uncompensated sits in the overscan below the viewport (not visible).
|
|
377
|
+
if (growth && growth.delta > 0) {
|
|
378
|
+
const maxOffsetKeepingTailMounted = growth.tailRows + TRANSCRIPT_WINDOW_OVERSCAN_ROWS - 1;
|
|
379
|
+
if (renderScrollOffset <= maxOffsetKeepingTailMounted) {
|
|
380
|
+
renderScrollOffset += Math.min(growth.delta, maxOffsetKeepingTailMounted - renderScrollOffset);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
350
384
|
const transcriptWindow = useMemo(() => transcriptRenderWindow(items, {
|
|
351
385
|
scrollOffset: renderScrollOffset,
|
|
352
386
|
viewportHeight: transcriptContentHeight,
|
|
@@ -367,8 +401,11 @@ export function useTranscriptWindow({
|
|
|
367
401
|
// CAPTURE for a missing/dirty anchor (above) reads this from the PREVIOUS
|
|
368
402
|
// frame to reconstruct the exact top-edge row that was on screen, so the
|
|
369
403
|
// capture matches what the user saw rather than the stale scrollOffset
|
|
370
|
-
// state.
|
|
371
|
-
|
|
404
|
+
// state. Publish the CLAMPED effective offset (transcriptRenderWindow clamps
|
|
405
|
+
// renderScrollOffset to maxScrollRows internally) so the reconstruction uses
|
|
406
|
+
// the value the window math actually rendered with, not the raw compensated
|
|
407
|
+
// offset. Bottom-relative, matching transcriptRenderWindow's window math.
|
|
408
|
+
renderOffset: Math.max(0, Number(transcriptWindow.effectiveScrollOffset) || 0),
|
|
372
409
|
suppressMeasuredRowHeights,
|
|
373
410
|
};
|
|
374
411
|
// The window memo is keyed on a structure signature that intentionally
|
|
@@ -456,8 +493,24 @@ export function useTranscriptWindow({
|
|
|
456
493
|
const variantKey = transcriptItemVariantKey(item);
|
|
457
494
|
if (isStreamingAssistant) {
|
|
458
495
|
const idPrev = streamingMeasuredRowsById.get(item.id);
|
|
496
|
+
// Baseline = the estimate for the text at THIS commit's measured state.
|
|
497
|
+
// Refresh it on EVERY harvest pass (not only on an id-store change): if
|
|
498
|
+
// the estimator's row count drifts while the Yoga height is unchanged
|
|
499
|
+
// (or a same-id text reset), a stale baseline would make
|
|
500
|
+
// streamingTailMountedGrowth compensate with no physical growth.
|
|
501
|
+
const estimateAtMeasure = streamingEstimateRows(item, frameColumns, toolOutputExpanded);
|
|
459
502
|
if (!idPrev || idPrev.rows !== measured || idPrev.columns !== frameColumns || idPrev.toolExpanded !== toolExpandedFlag) {
|
|
460
|
-
|
|
503
|
+
// Record the estimate for the EXACT text just measured alongside the
|
|
504
|
+
// real Yoga height, so streamingTailMountedGrowth can measure later
|
|
505
|
+
// growth in the estimate metric (live_now − estimateAtMeasure) and the
|
|
506
|
+
// estimator's steady bias cancels instead of leaking into a permanent
|
|
507
|
+
// over-compensation once measuredRowsVersion has absorbed this height.
|
|
508
|
+
streamingMeasuredRowsById.set(item.id, {
|
|
509
|
+
rows: measured,
|
|
510
|
+
columns: frameColumns,
|
|
511
|
+
toolExpanded: toolExpandedFlag,
|
|
512
|
+
estimateRows: estimateAtMeasure,
|
|
513
|
+
});
|
|
461
514
|
// ALWAYS bump on a real id-store change, bottom-pinned or not.
|
|
462
515
|
// estimateTranscriptItemRowsCached (transcript-window.mjs) now
|
|
463
516
|
// returns ONLY this id-store floor for a streaming item (defer-
|
|
@@ -470,6 +523,8 @@ export function useTranscriptWindow({
|
|
|
470
523
|
// stream settles — invisible growth for anything reading rowIndex
|
|
471
524
|
// (windowing, maxScrollRows, an anchor captured mid-stream).
|
|
472
525
|
changed = true;
|
|
526
|
+
} else {
|
|
527
|
+
idPrev.estimateRows = estimateAtMeasure;
|
|
473
528
|
}
|
|
474
529
|
}
|
|
475
530
|
const prev = transcriptMeasuredRowsCache.get(item);
|
|
@@ -662,13 +717,18 @@ export function useTranscriptWindow({
|
|
|
662
717
|
if (deltaY === 0) return;
|
|
663
718
|
const clippedRect = withSelectionClip(shiftSelectionRectY(dragRef.current.rect, deltaY));
|
|
664
719
|
dragRef.current = { ...dragRef.current, rect: clippedRect };
|
|
665
|
-
|
|
720
|
+
// rememberText:false — the shifted rect is viewport-clipped, so harvesting
|
|
721
|
+
// here would replace the full selection text remembered at drag-release
|
|
722
|
+
// with only the still-visible fragment (partial Ctrl+C after scrolling).
|
|
723
|
+
paintSelectionRect(clippedRect, { rememberText: false, immediate: true });
|
|
666
724
|
}, [transcriptContentHeight, transcriptWindow.totalRows, transcriptWindow.effectiveScrollOffset, withSelectionClip, paintSelectionRect]);
|
|
667
725
|
useEffect(() => {
|
|
668
726
|
if (!dragRef.current.rect) return;
|
|
669
727
|
const clippedRect = withSelectionClip(dragRef.current.rect);
|
|
670
728
|
dragRef.current = { ...dragRef.current, rect: clippedRect };
|
|
671
|
-
|
|
729
|
+
// Theme repaint: same cells, same text — no need to re-harvest (and a
|
|
730
|
+
// clipped rect would clobber the remembered full text with a fragment).
|
|
731
|
+
paintSelectionRect(clippedRect, { rememberText: false, immediate: true });
|
|
672
732
|
}, [themeEpoch, withSelectionClip, paintSelectionRect]);
|
|
673
733
|
useEffect(() => {
|
|
674
734
|
const maxRows = Math.max(0, Number(transcriptWindow.maxScrollRows) || 0);
|
|
@@ -49,10 +49,6 @@ import {
|
|
|
49
49
|
isModifiedEnterSequence,
|
|
50
50
|
isAnyModifiedEnterSequence,
|
|
51
51
|
} from './prompt-input/edit-helpers.mjs';
|
|
52
|
-
import {
|
|
53
|
-
VOICE_TICK_MS,
|
|
54
|
-
voiceIndicatorTextFor,
|
|
55
|
-
} from './prompt-input/voice-indicator.mjs';
|
|
56
52
|
|
|
57
53
|
// Windows Terminal IME composition can clip a glyph that starts exactly at the
|
|
58
54
|
// left edge of the editable text node. The rounded prompt box already adds a
|
|
@@ -100,8 +96,6 @@ export function PromptInput({
|
|
|
100
96
|
onRestoreQueued,
|
|
101
97
|
onHistoryNavigate,
|
|
102
98
|
onPasteText,
|
|
103
|
-
onVoiceToggle,
|
|
104
|
-
voiceIndicatorMode = 'off',
|
|
105
99
|
selectionRef,
|
|
106
100
|
valueRef,
|
|
107
101
|
boxRectRef,
|
|
@@ -129,27 +123,8 @@ export function PromptInput({
|
|
|
129
123
|
const boxRef = useRef(null);
|
|
130
124
|
const cursorEnabledRef = useRef(false); // latest enabled state, read by the anchor fn at render time
|
|
131
125
|
const contentWidthRef = useRef(80);
|
|
132
|
-
// Cells reserved on the right for the voice indicator glyph(s) (0 when
|
|
133
|
-
// hidden, 1 idle/spinner-only, 2 recording's "◉"+braille). Read by the
|
|
134
|
-
// cursor-anchor closure below (installed ONCE, so it must read live state
|
|
135
|
-
// via a ref rather than close over a render-local variable) to keep the
|
|
136
|
-
// caret/wrap math in sync with the actual reserved column, same pattern as
|
|
137
|
-
// IME_LEFT_GUARD_COLUMNS on the left edge.
|
|
138
|
-
const voiceIndicatorWidthRef = useRef(0);
|
|
139
126
|
const preferredColumnRef = useRef(null);
|
|
140
127
|
const mouseExtendCoalesceRef = useRef({ pendingNext: null, timer: null, t: 0 });
|
|
141
|
-
// Voice indicator animation tick. Runs a single ~120ms interval ONLY while
|
|
142
|
-
// an ANIMATED mode is active (installing/recording/transcribing) — 'idle'
|
|
143
|
-
// renders a static glyph and 'off' renders nothing, so neither keeps a
|
|
144
|
-
// timer alive (the old idle "breathing" pulse spun constantly whenever
|
|
145
|
-
// voice was merely enabled, reading as phantom activity).
|
|
146
|
-
const [voiceTick, setVoiceTick] = useState(0);
|
|
147
|
-
useEffect(() => {
|
|
148
|
-
if (voiceIndicatorMode === 'off' || voiceIndicatorMode === 'idle') return undefined;
|
|
149
|
-
const timer = setInterval(() => setVoiceTick((t) => (t + 1) % 1000000), VOICE_TICK_MS);
|
|
150
|
-
timer.unref?.();
|
|
151
|
-
return () => clearInterval(timer);
|
|
152
|
-
}, [voiceIndicatorMode]);
|
|
153
128
|
// Undo/redo snapshot stack. Each entry is a { value, cursor, selectionAnchor }
|
|
154
129
|
// draft snapshot. Continuous typing coalesces (see UNDO_COALESCE_MS) so a run
|
|
155
130
|
// of characters collapses into one undo step; cursor-only moves are NOT
|
|
@@ -171,6 +146,7 @@ export function PromptInput({
|
|
|
171
146
|
// the parent chain from our box and firing it flushes the new draft in the same
|
|
172
147
|
// tick. Guarded so a structure change in the ink fork degrades to throttled
|
|
173
148
|
// (slower) rendering, never a crash.
|
|
149
|
+
const flushThrottleRef = useRef({ lastAt: 0, timer: null });
|
|
174
150
|
const flushImmediate = () => {
|
|
175
151
|
let node = boxRef.current;
|
|
176
152
|
for (let i = 0; node && i < 64; i++) {
|
|
@@ -182,6 +158,30 @@ export function PromptInput({
|
|
|
182
158
|
}
|
|
183
159
|
};
|
|
184
160
|
|
|
161
|
+
// commitDraft fires on every keystroke; a raw queueMicrotask(flushImmediate)
|
|
162
|
+
// per commit bypassed ink's maxFps and forced an unthrottled render for each
|
|
163
|
+
// char (a burst of paste/held-key edits = a render storm). Coalesce to a
|
|
164
|
+
// frame budget: flush the first keystroke immediately (no caret lag) and
|
|
165
|
+
// batch the rest to one flush per ~16ms, with a trailing flush so the final
|
|
166
|
+
// char of a burst always lands.
|
|
167
|
+
const FLUSH_INTERVAL_MS = 16;
|
|
168
|
+
const scheduleImmediateFlush = () => {
|
|
169
|
+
const t = flushThrottleRef.current;
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
const elapsed = now - t.lastAt;
|
|
172
|
+
if (elapsed >= FLUSH_INTERVAL_MS) {
|
|
173
|
+
if (t.timer !== null) { clearTimeout(t.timer); t.timer = null; }
|
|
174
|
+
t.lastAt = now;
|
|
175
|
+
queueMicrotask(flushImmediate);
|
|
176
|
+
} else if (t.timer === null) {
|
|
177
|
+
t.timer = setTimeout(() => {
|
|
178
|
+
t.timer = null;
|
|
179
|
+
t.lastAt = Date.now();
|
|
180
|
+
flushImmediate();
|
|
181
|
+
}, FLUSH_INTERVAL_MS - elapsed);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
185
|
const commitDraft = (next, options = {}) => {
|
|
186
186
|
const sameDraft = draftStateEqual(draftRef.current, next);
|
|
187
187
|
if (!options.keepPreferredColumn) preferredColumnRef.current = null;
|
|
@@ -189,7 +189,7 @@ export function PromptInput({
|
|
|
189
189
|
if (!options.skipHistory) recordUndoSnapshot(draftRef.current, next, options);
|
|
190
190
|
draftRef.current = next;
|
|
191
191
|
setDraft(next);
|
|
192
|
-
|
|
192
|
+
scheduleImmediateFlush();
|
|
193
193
|
if (next.value !== lastReportedValueRef.current) {
|
|
194
194
|
lastReportedValueRef.current = next.value;
|
|
195
195
|
onDraftChange?.(next.value);
|
|
@@ -231,12 +231,7 @@ export function PromptInput({
|
|
|
231
231
|
const d = draftRef.current;
|
|
232
232
|
const w = yogaNode?.getComputedWidth?.() ?? 0;
|
|
233
233
|
const guardColumns = w > IME_LEFT_GUARD_COLUMNS ? IME_LEFT_GUARD_COLUMNS : 0;
|
|
234
|
-
|
|
235
|
-
// out of the SAME width the caret/wrap math uses, mirroring the left
|
|
236
|
-
// guard above — otherwise a visible indicator would silently overlap
|
|
237
|
-
// the caret/typed text at the right edge instead of pushing content in.
|
|
238
|
-
const voiceReserve = voiceIndicatorWidthRef.current > 0 ? voiceIndicatorWidthRef.current + 1 : 0;
|
|
239
|
-
const contentWidth = Math.max(1, (w ? w - guardColumns - voiceReserve : contentWidthRef.current) || 80);
|
|
234
|
+
const contentWidth = Math.max(1, (w ? w - guardColumns : contentWidthRef.current) || 80);
|
|
240
235
|
contentWidthRef.current = contentWidth;
|
|
241
236
|
const caret = w > 0
|
|
242
237
|
// PromptInput renders a trailing space cell when the cursor is at
|
|
@@ -962,21 +957,15 @@ export function PromptInput({
|
|
|
962
957
|
return;
|
|
963
958
|
}
|
|
964
959
|
|
|
965
|
-
// Ctrl+Space voice-record toggle. Two encodings observed across
|
|
966
|
-
// terminals: a lone NUL byte (legacy Ctrl+Space, many terminals send
|
|
967
|
-
// 0x00 directly) or the kitty CSI-u form `\x1b[32;5u` (codepoint 32
|
|
968
|
-
// = space, modifier 5 = 1+4 = ctrl). Consumed here — never typed into
|
|
969
|
-
// the prompt — and forwarded to the App-level recorder state machine
|
|
970
|
-
// via onVoiceToggle; a no-op when the prop isn't wired (e.g. embedded
|
|
971
|
-
// pickers) so this never throws.
|
|
972
|
-
if (rawInput === '\x00' || /^(?:\x1b)?\[32;5u$/.test(rawInput)) {
|
|
973
|
-
onVoiceToggle?.();
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
960
|
// Printable input (ignore other control keys). Strip any embedded SGR mouse
|
|
978
961
|
// sequences as a belt-and-suspenders guard (the early return above catches
|
|
979
962
|
// whole-sequence inputs; this removes partials that rode in with real text).
|
|
963
|
+
// Swallow Ctrl+Space raw encodings (lone NUL, or the kitty CSI-u form
|
|
964
|
+
// \x1b[32;5u) as a no-op so they never fall through into the prompt as
|
|
965
|
+
// garbage. No voice behavior — just discarded.
|
|
966
|
+
if (rawInput === '\x00' || /^(?:\x1b)?\[32;5u$/.test(rawInput)) {
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
980
969
|
const printable = rawInput
|
|
981
970
|
.replace(/(?:\x1b)?\[<\d+;\d+;\d+[Mm]/g, '')
|
|
982
971
|
.replace(/[\r\n]/g, '');
|
|
@@ -1019,15 +1008,6 @@ export function PromptInput({
|
|
|
1019
1008
|
const displayValue = mask ? value.replace(/[^\n]/g, '*') : value;
|
|
1020
1009
|
const renderedValue = renderSelectedText(displayValue, selectionRange(draft), cursor === value.length);
|
|
1021
1010
|
const hintMeta = hintStyle(hintTone);
|
|
1022
|
-
// Monotone voice indicator glyph text — zero-width (rendered as null) when
|
|
1023
|
-
// mode is 'off'. Braille frame drives installing/recording/transcribing;
|
|
1024
|
-
// 'idle' is a static ○ (no animation, no timer).
|
|
1025
|
-
const voiceIndicatorText = voiceIndicatorTextFor(voiceIndicatorMode, voiceTick);
|
|
1026
|
-
// stringWidth is unnecessary here — every glyph above is a single
|
|
1027
|
-
// 1-column codepoint (○ ● and the braille frames), and 'recording'
|
|
1028
|
-
// concatenates exactly two 1-cell glyphs, so plain .length gives the
|
|
1029
|
-
// correct cell count without pulling in emoji-width special-casing.
|
|
1030
|
-
voiceIndicatorWidthRef.current = voiceIndicatorText.length;
|
|
1031
1011
|
|
|
1032
1012
|
return (
|
|
1033
1013
|
<Box ref={boxRef} flexDirection="row" width="100%" flexGrow={1} flexShrink={1} backgroundColor={surfaceBackground()}>
|
|
@@ -1038,17 +1018,6 @@ export function PromptInput({
|
|
|
1038
1018
|
<Text color={hintMeta.textColor}>{hint}</Text>
|
|
1039
1019
|
</Box>
|
|
1040
1020
|
) : null}
|
|
1041
|
-
{voiceIndicatorText ? (
|
|
1042
|
-
<>
|
|
1043
|
-
{/* flexGrow spacer pins the indicator to the RIGHT edge of the row
|
|
1044
|
-
even when the typed text is short; without it the glyph would
|
|
1045
|
-
ride directly after the caret instead of the box edge. */}
|
|
1046
|
-
<Box flexGrow={1} backgroundColor={surfaceBackground()} />
|
|
1047
|
-
<Box flexShrink={0} width={voiceIndicatorText.length + 1} justifyContent="flex-end" backgroundColor={surfaceBackground()}>
|
|
1048
|
-
<Text color={theme.subtle}>{voiceIndicatorText}</Text>
|
|
1049
|
-
</Box>
|
|
1050
|
-
</>
|
|
1051
|
-
) : null}
|
|
1052
1021
|
</Box>
|
|
1053
1022
|
);
|
|
1054
1023
|
}
|
|
@@ -254,7 +254,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
254
254
|
// Keep the aggregate card at a fixed height (header + one detail row) for
|
|
255
255
|
// its whole lifecycle. Pending cards have no result yet, so reserve the
|
|
256
256
|
// detail row up front instead of growing from 1→2 rows when the summary
|
|
257
|
-
// lands on completion — that late row push is the "
|
|
257
|
+
// lands on completion — that late row push is the "line-jump" jump. The empty
|
|
258
258
|
// placeholder renders as a blank line under the ⎿ gutter; the final summary
|
|
259
259
|
// simply fills it in place. This matches estimateTranscriptItemRows (always
|
|
260
260
|
// 2 + resultRows), so windowing/scroll stay in lockstep too.
|
|
@@ -381,7 +381,7 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
381
381
|
// (collapsedDetail = ''), so the card rendered as a single header row. But
|
|
382
382
|
// estimateTranscriptItemRows() in App.jsx reserves 2 rows for a collapsed
|
|
383
383
|
// non-aggregate tool (1 only for skill surfaces). That left a 1-row gap that
|
|
384
|
-
|
|
384
|
+
// closed the instant the result landed — the surviving "line-jump". Reserve the same
|
|
385
385
|
// dim placeholder detail row the aggregate card uses (`Running`) for the whole
|
|
386
386
|
// pending lifecycle so the height stays fixed at header + one detail row and
|
|
387
387
|
// the final summary just fills in place. Skill surfaces collapse to a single
|