mixdog 0.9.18 → 0.9.20
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/LICENSE +21 -0
- package/README.md +93 -29
- package/package.json +5 -3
- package/scripts/build-runtime-windows.ps1 +242 -242
- package/scripts/build-tui.mjs +6 -0
- package/scripts/hook-bus-test.mjs +8 -0
- package/scripts/log-writer-guard-smoke.mjs +131 -0
- package/scripts/output-style-smoke.mjs +2 -2
- package/scripts/reactive-compact-persist-smoke.mjs +22 -6
- package/scripts/recall-bench-cases.json +10 -0
- package/scripts/recall-bench.mjs +91 -2
- package/scripts/recall-quality-cases.json +1 -2
- package/scripts/recall-usecase-cases.json +1 -1
- package/scripts/session-ingest-compaction-smoke.mjs +241 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -106
- package/scripts/tool-efficiency-diag.mjs +5 -2
- package/scripts/tool-smoke.mjs +251 -72
- 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/help.mjs +2 -5
- package/src/lib/mixdog-debug.cjs +13 -0
- package/src/mixdog-session-runtime.mjs +7 -3311
- package/src/rules/agent/00-core.md +4 -3
- package/src/rules/agent/30-explorer.md +53 -22
- package/src/rules/lead/02-channels.md +3 -3
- package/src/rules/lead/lead-tool.md +3 -2
- package/src/rules/shared/01-tool.md +24 -29
- package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
- 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/agent-loop.mjs +663 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +250 -35
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
- package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
- package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
- package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +6 -4
- package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
- 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/shell-job-paths.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
- 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/patch/orchestrator.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
- package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
- 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 +6 -2096
- package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
- package/src/runtime/channels/lib/inbound-routing.mjs +19 -12
- package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
- package/src/runtime/channels/lib/memory-client.mjs +32 -14
- package/src/runtime/channels/lib/network-retry.mjs +23 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +38 -7
- package/src/runtime/channels/lib/owned-runtime.mjs +547 -0
- 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/channels/lib/transcript-binding.mjs +336 -0
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
- package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
- package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
- package/src/runtime/channels/lib/worker-main.mjs +771 -0
- package/src/runtime/memory/index.mjs +73 -1725
- package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
- package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
- package/src/runtime/memory/lib/http-router.mjs +772 -0
- package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
- package/src/runtime/memory/lib/memory-embed.mjs +28 -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 +38 -6
- package/src/runtime/memory/lib/recall-format.mjs +106 -6
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
- 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/boot-profile.mjs +36 -0
- package/src/session-runtime/channel-config-api.mjs +70 -0
- package/src/session-runtime/config-lifecycle.mjs +1 -1
- package/src/session-runtime/context-status.mjs +181 -0
- package/src/session-runtime/cwd-plugins.mjs +46 -3
- package/src/session-runtime/env.mjs +17 -0
- package/src/session-runtime/lifecycle-api.mjs +242 -0
- package/src/session-runtime/mcp-glue.mjs +24 -3
- package/src/session-runtime/model-route-api.mjs +198 -0
- package/src/session-runtime/output-styles.mjs +44 -10
- package/src/session-runtime/provider-auth-api.mjs +135 -0
- package/src/session-runtime/resource-api.mjs +282 -0
- package/src/session-runtime/runtime-core.mjs +2046 -0
- package/src/session-runtime/session-turn-api.mjs +274 -0
- package/src/session-runtime/tool-catalog.mjs +18 -264
- package/src/session-runtime/tool-defs.mjs +2 -2
- package/src/session-runtime/workflow-agents-api.mjs +238 -0
- package/src/session-runtime/workflow.mjs +16 -1
- package/src/standalone/agent-tool.mjs +2 -2
- package/src/standalone/channel-worker.mjs +78 -5
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/standalone/memory-runtime-proxy.mjs +56 -6
- package/src/tui/App.jsx +88 -97
- package/src/tui/app/channel-pickers.mjs +45 -0
- package/src/tui/app/core-memory-picker.mjs +1 -1
- 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 +15 -2
- package/src/tui/app/use-prompt-handlers.mjs +7 -94
- package/src/tui/app/use-transcript-scroll.mjs +82 -4
- 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 +7908 -7558
- package/src/tui/engine/context-state.mjs +145 -0
- package/src/tui/engine/prompt-history.mjs +27 -0
- package/src/tui/engine/session-api-ext.mjs +478 -0
- package/src/tui/engine/session-api.mjs +545 -0
- package/src/tui/engine/session-flow.mjs +485 -0
- package/src/tui/engine/turn.mjs +1078 -0
- package/src/tui/engine.mjs +69 -2582
- package/src/tui/index.jsx +7 -0
- 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/vendor/ink/build/ink.js +16 -1
- package/vendor/ink/build/output.js +30 -4
- package/vendor/ink/build/render.js +5 -0
- 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
- package/src/workflows/sequential/WORKFLOW.md +0 -51
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* use-prompt-handlers.mjs — the PROMPT HANDLER cluster extracted from App.jsx
|
|
3
3
|
* (pass-7 split).
|
|
4
4
|
*
|
|
5
|
-
* Exports usePromptHandlers(), which owns the
|
|
6
|
-
* PromptInput:
|
|
7
|
-
* /
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* Exports usePromptHandlers(), which owns the four useCallback handlers wired to
|
|
6
|
+
* PromptInput: handlePromptPaste / handlePromptHistoryNavigate / handlePromptEscape
|
|
7
|
+
* / handlePromptInterrupt. Every ref, setter, store value and derived value the
|
|
8
|
+
* handlers close over is threaded in explicitly so the deps arrays stay
|
|
9
|
+
* byte-identical to the original inline hooks. Import-level helpers (paste
|
|
10
|
+
* attachments) are imported directly here.
|
|
11
11
|
*/
|
|
12
12
|
import { useCallback } from 'react';
|
|
13
13
|
import {
|
|
@@ -18,13 +18,6 @@ import {
|
|
|
18
18
|
import {
|
|
19
19
|
shouldFoldPastedText,
|
|
20
20
|
} from '../paste-attachments.mjs';
|
|
21
|
-
import {
|
|
22
|
-
isVoiceEnabled,
|
|
23
|
-
getRecorderState,
|
|
24
|
-
startRecording,
|
|
25
|
-
stopRecording,
|
|
26
|
-
cancelRecording,
|
|
27
|
-
} from '../lib/voice-recorder.mjs';
|
|
28
21
|
import { promptHistoryKey } from '../prompt-history-store.mjs';
|
|
29
22
|
|
|
30
23
|
export function usePromptHandlers({
|
|
@@ -42,10 +35,8 @@ export function usePromptHandlers({
|
|
|
42
35
|
setPastedImages,
|
|
43
36
|
setPastedTexts,
|
|
44
37
|
setPromptDraftOverride,
|
|
45
|
-
setVoiceRecPhase,
|
|
46
38
|
setContextPanel,
|
|
47
39
|
// derived / helper values + callbacks
|
|
48
|
-
setVoiceStatusHint,
|
|
49
40
|
syncPromptLayoutRows,
|
|
50
41
|
showPromptHint,
|
|
51
42
|
clearPromptHint,
|
|
@@ -63,70 +54,6 @@ export function usePromptHandlers({
|
|
|
63
54
|
clearPastedTextsSnapshot,
|
|
64
55
|
registerPastedText,
|
|
65
56
|
}) {
|
|
66
|
-
// Ctrl+Space handler wired to PromptInput's onVoiceToggle. Dispatches on the
|
|
67
|
-
// recorder's CURRENT state (idle/recording/transcribing) — see
|
|
68
|
-
// src/tui/lib/voice-recorder.mjs for the state machine itself.
|
|
69
|
-
const handleVoiceToggle = useCallback(async () => {
|
|
70
|
-
if (!isVoiceEnabled()) {
|
|
71
|
-
store.pushNotice('Voice is off — run /voice to turn it on', 'warn');
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
const recState = getRecorderState();
|
|
75
|
-
if (recState === 'transcribing') {
|
|
76
|
-
store.pushNotice('Voice: already transcribing…', 'info');
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (recState === 'recording') {
|
|
80
|
-
setVoiceStatusHint('… transcribing', 'info');
|
|
81
|
-
setVoiceRecPhase('transcribing');
|
|
82
|
-
let result;
|
|
83
|
-
try {
|
|
84
|
-
result = await stopRecording();
|
|
85
|
-
} catch (e) {
|
|
86
|
-
result = { ok: false, reason: e?.message || String(e) };
|
|
87
|
-
}
|
|
88
|
-
setVoiceStatusHint('', 'info');
|
|
89
|
-
setVoiceRecPhase('idle');
|
|
90
|
-
if (!result) return; // race: recorder was no longer RECORDING
|
|
91
|
-
if (!result.ok) {
|
|
92
|
-
store.pushNotice(`Voice: ${result.reason || 'transcription failed'}`, 'error');
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
const text = String(result.text || '').trim();
|
|
96
|
-
if (!text) {
|
|
97
|
-
store.pushNotice('Voice: no speech detected', 'warn');
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
// End-of-draft insertion via promptDraftOverride (approved design —
|
|
101
|
-
// no cursor-position insert; see gamerscroll skill's out-of-scope note
|
|
102
|
-
// on PromptInput's imperative surface).
|
|
103
|
-
// Med-4: promptValueRef.current is read HERE — after `await
|
|
104
|
-
// stopRecording()` has already resolved — not captured earlier before
|
|
105
|
-
// the await. PromptInput keeps valueRef.current live on every
|
|
106
|
-
// keystroke (commitDraft), so this always reflects whatever the user
|
|
107
|
-
// typed during the recording+transcribe window; nothing typed in that
|
|
108
|
-
// gap is overwritten.
|
|
109
|
-
const current = promptValueRef.current || '';
|
|
110
|
-
const next = current ? `${current}${/\s$/.test(current) ? '' : ' '}${text}` : text;
|
|
111
|
-
syncPromptLayoutRows(next);
|
|
112
|
-
setPromptDraftOverride({ id: Date.now(), value: next });
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
// idle -> recording
|
|
116
|
-
let result;
|
|
117
|
-
try {
|
|
118
|
-
result = await startRecording();
|
|
119
|
-
} catch (e) {
|
|
120
|
-
result = { ok: false, reason: e?.message || String(e) };
|
|
121
|
-
}
|
|
122
|
-
if (!result?.ok) {
|
|
123
|
-
store.pushNotice(`Voice: ${result?.reason || 'failed to start recording'}`, 'error');
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
setVoiceStatusHint('● REC', 'error');
|
|
127
|
-
setVoiceRecPhase('recording');
|
|
128
|
-
}, [store, setVoiceStatusHint, syncPromptLayoutRows]);
|
|
129
|
-
|
|
130
57
|
const handlePromptPaste = useCallback((text, meta = {}) => {
|
|
131
58
|
const source = String(meta?.source || 'paste');
|
|
132
59
|
const value = String(text ?? '');
|
|
@@ -256,19 +183,6 @@ export function usePromptHandlers({
|
|
|
256
183
|
// the active turn on the same Esc press.
|
|
257
184
|
// - empty prompt + active turn interrupts the active turn.
|
|
258
185
|
const handlePromptEscape = useCallback((text = '', meta = {}) => {
|
|
259
|
-
// Recording takes priority over every other Esc branch (usage/context
|
|
260
|
-
// panels, slash-clear, queue-restore, turn-interrupt): a live mic capture
|
|
261
|
-
// must never silently keep running because Esc was consumed by something
|
|
262
|
-
// else first. Only consumes Esc while actually RECORDING — transcribing
|
|
263
|
-
// (already stopped, awaiting the HTTP round-trip) and idle fall through
|
|
264
|
-
// to the existing branches unchanged.
|
|
265
|
-
if (getRecorderState() === 'recording') {
|
|
266
|
-
cancelRecording();
|
|
267
|
-
setVoiceStatusHint('', 'info');
|
|
268
|
-
setVoiceRecPhase('idle');
|
|
269
|
-
store.pushNotice('Voice: recording cancelled', 'plain');
|
|
270
|
-
return true;
|
|
271
|
-
}
|
|
272
186
|
if (usagePanel) { closeUsagePanel(); return true; }
|
|
273
187
|
if (contextPanel) { setContextPanel(null); return true; }
|
|
274
188
|
|
|
@@ -283,7 +197,7 @@ export function usePromptHandlers({
|
|
|
283
197
|
// Idle + empty + nothing to restore: nothing (double-press from empty
|
|
284
198
|
// opens message selector, but we don't have that feature yet).
|
|
285
199
|
return false;
|
|
286
|
-
}, [contextPanel, usagePanel, closeUsagePanel, restoreQueuedToPrompt, clearPromptHint, clearPastedImagesSnapshot,
|
|
200
|
+
}, [contextPanel, usagePanel, closeUsagePanel, restoreQueuedToPrompt, clearPromptHint, clearPastedImagesSnapshot, store]);
|
|
287
201
|
|
|
288
202
|
const handlePromptInterrupt = useCallback((currentText = '') => {
|
|
289
203
|
const result = store.abort?.();
|
|
@@ -301,7 +215,6 @@ export function usePromptHandlers({
|
|
|
301
215
|
}, [store, clearPromptHint, installPastedImages, clearPastedImagesSnapshot]);
|
|
302
216
|
|
|
303
217
|
return {
|
|
304
|
-
handleVoiceToggle,
|
|
305
218
|
handlePromptPaste,
|
|
306
219
|
handlePromptHistoryNavigate,
|
|
307
220
|
handlePromptEscape,
|
|
@@ -44,6 +44,71 @@ export function useTranscriptScroll({
|
|
|
44
44
|
// SCROLL_COALESCE_MS). Both call sites accumulate into pendingRows.
|
|
45
45
|
const scrollCoalesceRef = useRef({ pendingRows: 0, timer: null });
|
|
46
46
|
const selectionTextTimerRef = useRef(null);
|
|
47
|
+
// Stitch buffer: accumulates harvested transcript selection rows across scroll
|
|
48
|
+
// positions so Ctrl+C copies the FULL drag even after it auto-scrolled past the
|
|
49
|
+
// viewport. Keyed by scroll-invariant content row = screenY - scrollTarget at
|
|
50
|
+
// harvest time; value = row text. Only transcript-region drags accumulate.
|
|
51
|
+
const stitchBufferRef = useRef(new Map());
|
|
52
|
+
const stitchHarvestTimerRef = useRef(null);
|
|
53
|
+
// Scroll offset captured at the SCHEDULE (paint) time of the pending harvest —
|
|
54
|
+
// the deferred timer must key rows by the frame's scroll, not by whatever
|
|
55
|
+
// scrollTargetRef holds when the timer eventually fires (a scroll in between
|
|
56
|
+
// would mis-key the rows). Latest schedule wins (latest paint = latest frame).
|
|
57
|
+
const stitchHarvestScrollRef = useRef(0);
|
|
58
|
+
|
|
59
|
+
const clearStitchBuffer = useCallback(() => {
|
|
60
|
+
stitchBufferRef.current.clear();
|
|
61
|
+
if (stitchHarvestTimerRef.current) {
|
|
62
|
+
clearTimeout(stitchHarvestTimerRef.current);
|
|
63
|
+
stitchHarvestTimerRef.current = null;
|
|
64
|
+
}
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
// Deferred (like rememberSelectionTextSoon) harvest of the currently visible
|
|
68
|
+
// selection rows into the stitch buffer. Runs on EVERY transcript selection
|
|
69
|
+
// paint AND on scroll-shift repaints (the rememberText:false path) so rows
|
|
70
|
+
// revealed only mid-scroll are captured. Later harvest of a key overwrites,
|
|
71
|
+
// handling partial↔full endpoint rows on retraction.
|
|
72
|
+
const harvestStitchRowsSoon = useCallback(() => {
|
|
73
|
+
if (dragRef.current.region !== 'transcript') return;
|
|
74
|
+
// Capture the scroll offset for THIS paint (schedule time). If a timer is
|
|
75
|
+
// already pending, only refresh the captured offset to the latest frame and
|
|
76
|
+
// reuse the existing timer.
|
|
77
|
+
stitchHarvestScrollRef.current = Number(scrollTargetRef.current) || 0;
|
|
78
|
+
if (stitchHarvestTimerRef.current) return;
|
|
79
|
+
stitchHarvestTimerRef.current = setTimeout(() => {
|
|
80
|
+
stitchHarvestTimerRef.current = null;
|
|
81
|
+
if (dragRef.current.region !== 'transcript') return;
|
|
82
|
+
const rows = store.getRenderSelectionRows?.();
|
|
83
|
+
if (!Array.isArray(rows)) return;
|
|
84
|
+
const scroll = stitchHarvestScrollRef.current;
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
if (!row || typeof row.y !== 'number') continue;
|
|
87
|
+
stitchBufferRef.current.set(row.y - scroll, typeof row.text === 'string' ? row.text : '');
|
|
88
|
+
}
|
|
89
|
+
}, 0);
|
|
90
|
+
}, [store]);
|
|
91
|
+
|
|
92
|
+
// Map the CURRENT rect + current scrollTarget onto the content-key range and
|
|
93
|
+
// join buffered rows sorted by key with '\n' (skipping missing keys). Returns
|
|
94
|
+
// '' when unusable so callers can fall back to render/remembered text.
|
|
95
|
+
const getStitchedSelectionText = useCallback(() => {
|
|
96
|
+
const buf = stitchBufferRef.current;
|
|
97
|
+
if (!buf.size) return '';
|
|
98
|
+
if (dragRef.current.region !== 'transcript') return '';
|
|
99
|
+
const rect = dragRef.current.rect;
|
|
100
|
+
if (!rect) return '';
|
|
101
|
+
const y1 = Number(rect.y1);
|
|
102
|
+
const y2 = Number(rect.y2);
|
|
103
|
+
if (!Number.isFinite(y1) || !Number.isFinite(y2)) return '';
|
|
104
|
+
const scroll = Number(scrollTargetRef.current) || 0;
|
|
105
|
+
const lo = Math.min(y1, y2) - scroll;
|
|
106
|
+
const hi = Math.max(y1, y2) - scroll;
|
|
107
|
+
const keys = [...buf.keys()].filter((k) => k >= lo && k <= hi).sort((a, b) => a - b);
|
|
108
|
+
if (!keys.length) return '';
|
|
109
|
+
const text = keys.map((k) => buf.get(k)).filter((t) => t != null).join('\n');
|
|
110
|
+
return text.trim() ? text : '';
|
|
111
|
+
}, []);
|
|
47
112
|
|
|
48
113
|
const stopSmoothScroll = useCallback(() => {
|
|
49
114
|
if (!scrollAnimationRef.current) return;
|
|
@@ -145,19 +210,24 @@ export function useTranscriptScroll({
|
|
|
145
210
|
store.setRenderSelection?.(nextRect, { immediate: true });
|
|
146
211
|
}
|
|
147
212
|
if (needsCapture) rememberSelectionTextSoon();
|
|
213
|
+
if (nextRect) harvestStitchRowsSoon();
|
|
148
214
|
return true;
|
|
149
215
|
}
|
|
150
216
|
state.rect = nextRect;
|
|
151
217
|
state.t = Date.now();
|
|
152
218
|
store.setRenderSelection?.(nextRect, immediate ? { immediate: true } : undefined);
|
|
153
219
|
if (nextRect && rememberText && nextRect.captureText !== false) rememberSelectionTextSoon();
|
|
220
|
+
if (nextRect) harvestStitchRowsSoon();
|
|
154
221
|
return true;
|
|
155
|
-
}, [store, rememberSelectionTextSoon]);
|
|
222
|
+
}, [store, rememberSelectionTextSoon, harvestStitchRowsSoon]);
|
|
156
223
|
|
|
157
224
|
const applySelectionRect = useCallback((rect) => {
|
|
158
225
|
const clippedRect = withSelectionClip(rect);
|
|
159
226
|
dragRef.current.rect = clippedRect || null;
|
|
160
|
-
if (!clippedRect)
|
|
227
|
+
if (!clippedRect) {
|
|
228
|
+
selectionTextRef.current = '';
|
|
229
|
+
clearStitchBuffer();
|
|
230
|
+
}
|
|
161
231
|
const state = selectionPaintRef.current;
|
|
162
232
|
if (state.timer) {
|
|
163
233
|
clearTimeout(state.timer);
|
|
@@ -165,7 +235,7 @@ export function useTranscriptScroll({
|
|
|
165
235
|
state.pending = null;
|
|
166
236
|
}
|
|
167
237
|
paintSelectionRect(clippedRect, { rememberText: true, immediate: true });
|
|
168
|
-
}, [paintSelectionRect, withSelectionClip]);
|
|
238
|
+
}, [paintSelectionRect, withSelectionClip, clearStitchBuffer]);
|
|
169
239
|
|
|
170
240
|
const applySelectionRectThrottled = useCallback((rect) => {
|
|
171
241
|
const clippedRect = withSelectionClip(rect, { captureText: false });
|
|
@@ -274,6 +344,8 @@ export function useTranscriptScroll({
|
|
|
274
344
|
paintState.pending = null;
|
|
275
345
|
if (selectionTextTimerRef.current) clearTimeout(selectionTextTimerRef.current);
|
|
276
346
|
selectionTextTimerRef.current = null;
|
|
347
|
+
if (stitchHarvestTimerRef.current) clearTimeout(stitchHarvestTimerRef.current);
|
|
348
|
+
stitchHarvestTimerRef.current = null;
|
|
277
349
|
const coalesceState = scrollCoalesceRef.current;
|
|
278
350
|
if (coalesceState.timer) clearTimeout(coalesceState.timer);
|
|
279
351
|
coalesceState.timer = null;
|
|
@@ -341,7 +413,11 @@ export function useTranscriptScroll({
|
|
|
341
413
|
}
|
|
342
414
|
const clippedRect = withSelectionClip(rect);
|
|
343
415
|
dragRef.current = { ...dragRef.current, rect: clippedRect };
|
|
344
|
-
|
|
416
|
+
// Never re-harvest selection text from a scroll-shifted rect: the shift
|
|
417
|
+
// clips the rect to the viewport, so a harvest here would OVERWRITE the
|
|
418
|
+
// full text remembered at drag-release with only the still-visible rows
|
|
419
|
+
// (Ctrl+C after scrolling then copied just that fragment).
|
|
420
|
+
paintSelectionRect(clippedRect, { rememberText: false });
|
|
345
421
|
}
|
|
346
422
|
if (options.smooth) {
|
|
347
423
|
startSmoothScroll();
|
|
@@ -508,5 +584,7 @@ export function useTranscriptScroll({
|
|
|
508
584
|
scrollTranscriptRows,
|
|
509
585
|
queueScrollCoalesced,
|
|
510
586
|
moveSelectionFocus,
|
|
587
|
+
getStitchedSelectionText,
|
|
588
|
+
clearStitchBuffer,
|
|
511
589
|
};
|
|
512
590
|
}
|
|
@@ -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
|