cvc-tui 0.4.4 → 0.4.7
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/NOTICES.md +13 -0
- package/dist/app/completion.js +102 -0
- package/dist/app/createGatewayEventHandler.js +508 -0
- package/dist/app/createSlashHandler.js +101 -0
- package/dist/app/delegationStore.js +51 -0
- package/dist/app/gatewayContext.js +17 -0
- package/dist/app/historyStore.js +123 -0
- package/dist/app/inputBuffer.js +120 -0
- package/dist/app/inputSelectionStore.js +8 -0
- package/dist/app/inputStore.js +28 -0
- package/dist/app/interfaces.js +6 -0
- package/dist/app/overlayStore.js +40 -0
- package/dist/app/promptStore.js +44 -0
- package/dist/app/queueStore.js +25 -0
- package/dist/app/scroll.js +44 -0
- package/dist/app/setupHandoff.js +28 -0
- package/dist/app/slash/commands/core.js +479 -0
- package/dist/app/slash/commands/debug.js +44 -0
- package/dist/app/slash/commands/ops.js +512 -0
- package/dist/app/slash/commands/session.js +431 -0
- package/dist/app/slash/commands/setup.js +20 -0
- package/dist/app/slash/commands/toggles.js +40 -0
- package/dist/app/slash/registry.js +18 -0
- package/dist/app/slash/types.js +1 -0
- package/dist/app/spawnHistoryStore.js +105 -0
- package/dist/app/turnController.js +650 -0
- package/dist/app/turnStore.js +48 -0
- package/dist/app/uiStore.js +36 -0
- package/dist/app/useComposerState.js +265 -0
- package/dist/app/useConfigSync.js +144 -0
- package/dist/app/useInputHandlers.js +403 -0
- package/dist/app/useLongRunToolCharms.js +50 -0
- package/dist/app/useMainApp.js +638 -0
- package/dist/app/useSessionLifecycle.js +175 -0
- package/dist/app/useSubmission.js +287 -0
- package/dist/app.js +15 -0
- package/dist/banner.js +63 -0
- package/dist/components/agentsOverlay.js +474 -0
- package/dist/components/appChrome.js +252 -0
- package/dist/components/appLayout.js +122 -0
- package/dist/components/appOverlays.js +65 -0
- package/dist/components/branding.js +97 -0
- package/dist/components/fpsOverlay.js +22 -0
- package/dist/components/helpHint.js +21 -0
- package/dist/components/markdown.js +501 -0
- package/dist/components/maskedPrompt.js +12 -0
- package/dist/components/messageLine.js +82 -0
- package/dist/components/modelPicker.js +254 -0
- package/dist/components/overlayControls.js +30 -0
- package/dist/components/overlays/confirmPrompt.js +25 -0
- package/dist/components/overlays/helpOverlay.js +76 -0
- package/dist/components/overlays/historySearch.js +49 -0
- package/dist/components/overlays/modelPicker.js +60 -0
- package/dist/components/overlays/overlayUtils.js +19 -0
- package/dist/components/overlays/secretPrompt.js +36 -0
- package/dist/components/overlays/sessionPicker.js +93 -0
- package/dist/components/overlays/skillsHub.js +71 -0
- package/dist/components/prompts.js +95 -0
- package/dist/components/queuedMessages.js +24 -0
- package/dist/components/sessionPicker.js +130 -0
- package/dist/components/skillsHub.js +165 -0
- package/dist/components/streamingAssistant.js +35 -0
- package/dist/components/streamingMarkdown.js +144 -0
- package/dist/components/textInput.js +794 -0
- package/dist/components/themed.js +12 -0
- package/dist/components/thinking.js +496 -0
- package/dist/components/todoPanel.js +40 -0
- package/dist/components/transcript.js +22 -0
- package/dist/config/env.js +18 -0
- package/dist/config/limits.js +22 -0
- package/dist/config/timing.js +25 -0
- package/dist/content/charms.js +5 -0
- package/dist/content/faces.js +21 -0
- package/dist/content/fortunes.js +29 -0
- package/dist/content/hotkeys.js +38 -0
- package/dist/content/placeholders.js +15 -0
- package/dist/content/setup.js +14 -0
- package/dist/content/verbs.js +41 -0
- package/dist/domain/details.js +53 -0
- package/dist/domain/messages.js +63 -0
- package/dist/domain/paths.js +16 -0
- package/dist/domain/providers.js +11 -0
- package/dist/domain/roles.js +6 -0
- package/dist/domain/slash.js +11 -0
- package/dist/domain/usage.js +1 -0
- package/dist/domain/viewport.js +33 -0
- package/dist/entry.js +64 -70236
- package/dist/gateway/client.js +312 -0
- package/dist/gatewayClient.js +574 -0
- package/dist/gatewayTypes.js +1 -0
- package/dist/hooks/useCompletion.js +86 -0
- package/dist/hooks/useGitBranch.js +58 -0
- package/dist/hooks/useInputHistory.js +12 -0
- package/dist/hooks/useQueue.js +57 -0
- package/dist/hooks/useVirtualHistory.js +401 -0
- package/dist/lib/circularBuffer.js +43 -0
- package/dist/lib/clipboard.js +126 -0
- package/dist/lib/editor.js +41 -0
- package/dist/lib/editor.test.js +58 -0
- package/dist/lib/emoji.js +49 -0
- package/dist/lib/externalCli.js +11 -0
- package/dist/lib/forceTruecolor.js +26 -0
- package/dist/lib/fpsStore.js +36 -0
- package/dist/lib/gracefulExit.js +29 -0
- package/dist/lib/history.js +69 -0
- package/dist/lib/inputMetrics.js +143 -0
- package/dist/lib/liveProgress.js +51 -0
- package/dist/lib/liveProgress.test.js +89 -0
- package/dist/lib/localSessionInfo.js +116 -0
- package/dist/lib/mathUnicode.js +685 -0
- package/dist/lib/memory.js +123 -0
- package/dist/lib/memoryMonitor.js +76 -0
- package/dist/lib/messages.js +3 -0
- package/dist/lib/messages.test.js +25 -0
- package/dist/lib/osc52.js +53 -0
- package/dist/lib/perfPane.js +94 -0
- package/dist/lib/platform.js +312 -0
- package/dist/lib/precisionWheel.js +25 -0
- package/dist/lib/react-devtools-stub.js +12 -0
- package/dist/lib/reasoning.js +39 -0
- package/dist/lib/rpc.js +26 -0
- package/dist/lib/subagentTree.js +287 -0
- package/dist/lib/syntax.js +89 -0
- package/dist/lib/terminalModes.js +46 -0
- package/dist/lib/terminalParity.js +48 -0
- package/dist/lib/terminalSetup.js +321 -0
- package/dist/lib/text.js +203 -0
- package/dist/lib/text.test.js +18 -0
- package/dist/lib/todo.js +2 -0
- package/dist/lib/todo.test.js +22 -0
- package/dist/lib/viewportStore.js +82 -0
- package/dist/lib/virtualHeights.js +61 -0
- package/dist/lib/wheelAccel.js +143 -0
- package/dist/protocol/interpolation.js +4 -0
- package/dist/protocol/paste.js +3 -0
- package/dist/theme.js +398 -0
- package/dist/types.js +1 -0
- package/dist/vendor/cvc-ink/dist/entry-exports.js +52737 -0
- package/dist/vendor/cvc-ink/index.js +1 -0
- package/dist/vendor/cvc-ink/package.json +9 -0
- package/dist/vendor/cvc-ink/text-input.js +1 -0
- package/package.json +9 -9
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Ported from CVC Agent (https://github.com/NousResearch/cvc)
|
|
4
|
+
// Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
|
|
5
|
+
import { forceRedraw, useInput } from '../vendor/cvc-ink/index.js';
|
|
6
|
+
import { useStore } from '@nanostores/react';
|
|
7
|
+
import { useEffect, useRef } from 'react';
|
|
8
|
+
import { TYPING_IDLE_MS } from '../config/timing.js';
|
|
9
|
+
import { isAction, isCopyShortcut, isMac, isVoiceToggleKey } from '../lib/platform.js';
|
|
10
|
+
import { computePrecisionWheelStep, initPrecisionWheel } from '../lib/precisionWheel.js';
|
|
11
|
+
import { computeWheelStep, initWheelAccelForHost } from '../lib/wheelAccel.js';
|
|
12
|
+
import { getInputSelection } from './inputSelectionStore.js';
|
|
13
|
+
import { $isBlocked, $overlayState, patchOverlayState } from './overlayStore.js';
|
|
14
|
+
import { turnController } from './turnController.js';
|
|
15
|
+
import { patchTurnState } from './turnStore.js';
|
|
16
|
+
import { getUiState } from './uiStore.js';
|
|
17
|
+
const isCtrl = (key, ch, target) => key.ctrl && ch.toLowerCase() === target;
|
|
18
|
+
export function applyVoiceRecordResponse(response, starting, voice, sys) {
|
|
19
|
+
if (!starting || response?.status === 'recording') {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
voice.setRecording(false);
|
|
23
|
+
if (response?.status === 'busy') {
|
|
24
|
+
voice.setProcessing(true);
|
|
25
|
+
sys('voice: still transcribing; try again shortly');
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
voice.setProcessing(false);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function useInputHandlers(ctx) {
|
|
32
|
+
const { actions, composer, gateway, terminal, voice, wheelStep } = ctx;
|
|
33
|
+
const { actions: cActions, refs: cRefs, state: cState } = composer;
|
|
34
|
+
const overlay = useStore($overlayState);
|
|
35
|
+
const isBlocked = useStore($isBlocked);
|
|
36
|
+
const pagerPageSize = Math.max(5, (terminal.stdout?.rows ?? 24) - 6);
|
|
37
|
+
const scrollIdleTimer = useRef(null);
|
|
38
|
+
// Wheel accel ported from claude-code: inter-event timing drives step size,
|
|
39
|
+
// direction flips reset. wheelStep (WHEEL_SCROLL_STEP) is the base; final
|
|
40
|
+
// rows = wheelStep × accelMult. State mutates in place across renders.
|
|
41
|
+
const wheelAccelRef = useRef(initWheelAccelForHost());
|
|
42
|
+
const precisionWheelRef = useRef(initPrecisionWheel());
|
|
43
|
+
useEffect(() => () => clearTimeout(scrollIdleTimer.current ?? undefined), []);
|
|
44
|
+
const scrollTranscript = (delta) => {
|
|
45
|
+
if (getUiState().busy) {
|
|
46
|
+
turnController.boostStreamingForScroll();
|
|
47
|
+
clearTimeout(scrollIdleTimer.current ?? undefined);
|
|
48
|
+
scrollIdleTimer.current = setTimeout(() => {
|
|
49
|
+
scrollIdleTimer.current = null;
|
|
50
|
+
turnController.relaxStreaming();
|
|
51
|
+
}, TYPING_IDLE_MS);
|
|
52
|
+
}
|
|
53
|
+
terminal.scrollWithSelection(delta);
|
|
54
|
+
};
|
|
55
|
+
const copySelection = () => {
|
|
56
|
+
// ink's copySelection() already calls setClipboard() which handles
|
|
57
|
+
// pbcopy (macOS), wl-copy/xclip (Linux), tmux, and OSC 52 fallback.
|
|
58
|
+
terminal.selection.copySelection();
|
|
59
|
+
};
|
|
60
|
+
const clearSelection = () => {
|
|
61
|
+
terminal.selection.clearSelection();
|
|
62
|
+
};
|
|
63
|
+
const cancelOverlayFromCtrlC = () => {
|
|
64
|
+
if (overlay.clarify) {
|
|
65
|
+
return actions.answerClarify('');
|
|
66
|
+
}
|
|
67
|
+
if (overlay.approval) {
|
|
68
|
+
return gateway
|
|
69
|
+
.rpc('approval.respond', { choice: 'deny', session_id: getUiState().sid })
|
|
70
|
+
.then(r => r && (patchOverlayState({ approval: null }), patchTurnState({ outcome: 'denied' })));
|
|
71
|
+
}
|
|
72
|
+
if (overlay.sudo) {
|
|
73
|
+
return gateway
|
|
74
|
+
.rpc('sudo.respond', { password: '', request_id: overlay.sudo.requestId })
|
|
75
|
+
.then(r => r && (patchOverlayState({ sudo: null }), actions.sys('sudo cancelled')));
|
|
76
|
+
}
|
|
77
|
+
if (overlay.secret) {
|
|
78
|
+
return gateway
|
|
79
|
+
.rpc('secret.respond', { request_id: overlay.secret.requestId, value: '' })
|
|
80
|
+
.then(r => r && (patchOverlayState({ secret: null }), actions.sys('secret entry cancelled')));
|
|
81
|
+
}
|
|
82
|
+
if (overlay.modelPicker) {
|
|
83
|
+
return patchOverlayState({ modelPicker: false });
|
|
84
|
+
}
|
|
85
|
+
if (overlay.skillsHub) {
|
|
86
|
+
return patchOverlayState({ skillsHub: false });
|
|
87
|
+
}
|
|
88
|
+
if (overlay.picker) {
|
|
89
|
+
return patchOverlayState({ picker: false });
|
|
90
|
+
}
|
|
91
|
+
if (overlay.agents) {
|
|
92
|
+
return patchOverlayState({ agents: false });
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const cycleQueue = (dir) => {
|
|
96
|
+
const len = cRefs.queueRef.current.length;
|
|
97
|
+
if (!len) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const index = cState.queueEditIdx === null ? (dir > 0 ? 0 : len - 1) : (cState.queueEditIdx + dir + len) % len;
|
|
101
|
+
cActions.setQueueEdit(index);
|
|
102
|
+
cActions.setHistoryIdx(null);
|
|
103
|
+
cActions.setInput(cRefs.queueRef.current[index] ?? '');
|
|
104
|
+
return true;
|
|
105
|
+
};
|
|
106
|
+
const cycleHistory = (dir) => {
|
|
107
|
+
const h = cRefs.historyRef.current;
|
|
108
|
+
const cur = cState.historyIdx;
|
|
109
|
+
if (dir < 0) {
|
|
110
|
+
if (!h.length) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (cur === null) {
|
|
114
|
+
cRefs.historyDraftRef.current = cState.input;
|
|
115
|
+
}
|
|
116
|
+
const index = cur === null ? h.length - 1 : Math.max(0, cur - 1);
|
|
117
|
+
cActions.setHistoryIdx(index);
|
|
118
|
+
cActions.setQueueEdit(null);
|
|
119
|
+
cActions.setInput(h[index] ?? '');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (cur === null) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const next = cur + 1;
|
|
126
|
+
if (next >= h.length) {
|
|
127
|
+
cActions.setHistoryIdx(null);
|
|
128
|
+
cActions.setInput(cRefs.historyDraftRef.current);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
cActions.setHistoryIdx(next);
|
|
132
|
+
cActions.setInput(h[next] ?? '');
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
// CLI parity: Ctrl+B toggles a VAD-bounded push-to-talk capture
|
|
136
|
+
// (NOT the voice-mode umbrella bit). The mode is enabled via /voice on;
|
|
137
|
+
// Ctrl+B while the mode is off sys-nudges the user. While the mode is
|
|
138
|
+
// on, the first press starts a single VAD-bounded capture
|
|
139
|
+
// (gateway -> start_continuous(auto_restart=false), VAD auto-stop ->
|
|
140
|
+
// transcribe -> idle), a subsequent press stops and transcribes it.
|
|
141
|
+
// The gateway publishes voice.status + voice.transcript events that
|
|
142
|
+
// createGatewayEventHandler turns into UI badges and composer injection.
|
|
143
|
+
const voiceRecordToggle = () => {
|
|
144
|
+
if (!voice.enabled) {
|
|
145
|
+
return actions.sys('voice: mode is off — enable with /voice on');
|
|
146
|
+
}
|
|
147
|
+
const starting = !voice.recording;
|
|
148
|
+
const action = starting ? 'start' : 'stop';
|
|
149
|
+
// Optimistic UI — flip the REC badge immediately so the user gets
|
|
150
|
+
// feedback while the RPC round-trips; the voice.status event is the
|
|
151
|
+
// authoritative source and may correct us.
|
|
152
|
+
if (starting) {
|
|
153
|
+
voice.setRecording(true);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
voice.setRecording(false);
|
|
157
|
+
voice.setProcessing(false);
|
|
158
|
+
}
|
|
159
|
+
gateway
|
|
160
|
+
.rpc('voice.record', { action, session_id: getUiState().sid })
|
|
161
|
+
.then(r => applyVoiceRecordResponse(r, starting, voice, actions.sys))
|
|
162
|
+
.catch((e) => {
|
|
163
|
+
// Revert optimistic UI on failure.
|
|
164
|
+
if (starting) {
|
|
165
|
+
voice.setRecording(false);
|
|
166
|
+
}
|
|
167
|
+
actions.sys(`voice error: ${e.message}`);
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
useInput((ch, key) => {
|
|
171
|
+
const live = getUiState();
|
|
172
|
+
if (isBlocked) {
|
|
173
|
+
// When approval/clarify/confirm overlays are active, their own useInput
|
|
174
|
+
// handlers must receive keystrokes (arrow keys, numbers, Enter). Only
|
|
175
|
+
// intercept Ctrl+C here so the user can deny/dismiss — all other keys
|
|
176
|
+
// fall through to the component-level handlers.
|
|
177
|
+
if (overlay.approval || overlay.clarify || overlay.confirm) {
|
|
178
|
+
if (isCtrl(key, ch, 'c')) {
|
|
179
|
+
cancelOverlayFromCtrlC();
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (overlay.pager) {
|
|
184
|
+
if (key.escape || isCtrl(key, ch, 'c') || ch === 'q') {
|
|
185
|
+
return patchOverlayState({ pager: null });
|
|
186
|
+
}
|
|
187
|
+
const move = (delta) => patchOverlayState(prev => {
|
|
188
|
+
if (!prev.pager) {
|
|
189
|
+
return prev;
|
|
190
|
+
}
|
|
191
|
+
const { lines, offset } = prev.pager;
|
|
192
|
+
const max = Math.max(0, lines.length - pagerPageSize);
|
|
193
|
+
const step = delta === 'top' ? -lines.length : delta === 'bottom' ? lines.length : delta;
|
|
194
|
+
const next = Math.max(0, Math.min(offset + step, max));
|
|
195
|
+
return next === offset ? prev : { ...prev, pager: { ...prev.pager, offset: next } };
|
|
196
|
+
});
|
|
197
|
+
if (key.upArrow || ch === 'k') {
|
|
198
|
+
return move(-1);
|
|
199
|
+
}
|
|
200
|
+
if (key.downArrow || ch === 'j') {
|
|
201
|
+
return move(1);
|
|
202
|
+
}
|
|
203
|
+
if (key.pageUp || ch === 'b') {
|
|
204
|
+
return move(-pagerPageSize);
|
|
205
|
+
}
|
|
206
|
+
if (ch === 'g') {
|
|
207
|
+
return move('top');
|
|
208
|
+
}
|
|
209
|
+
if (ch === 'G') {
|
|
210
|
+
return move('bottom');
|
|
211
|
+
}
|
|
212
|
+
if (key.return || ch === ' ' || key.pageDown) {
|
|
213
|
+
patchOverlayState(prev => {
|
|
214
|
+
if (!prev.pager) {
|
|
215
|
+
return prev;
|
|
216
|
+
}
|
|
217
|
+
const { lines, offset } = prev.pager;
|
|
218
|
+
const max = Math.max(0, lines.length - pagerPageSize);
|
|
219
|
+
// Auto-close only when already at the last page — otherwise clamp
|
|
220
|
+
// to `max` so the offset matches what the line/page-back handlers
|
|
221
|
+
// can reach (prevents a snap-back jump on the next ↑/↓/PgUp).
|
|
222
|
+
return offset >= max
|
|
223
|
+
? { ...prev, pager: null }
|
|
224
|
+
: { ...prev, pager: { ...prev.pager, offset: Math.min(offset + pagerPageSize, max) } };
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (isCtrl(key, ch, 'c')) {
|
|
230
|
+
cancelOverlayFromCtrlC();
|
|
231
|
+
}
|
|
232
|
+
else if (key.escape && overlay.picker) {
|
|
233
|
+
patchOverlayState({ picker: false });
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (cState.completions.length && cState.input && cState.historyIdx === null && (key.upArrow || key.downArrow)) {
|
|
238
|
+
const len = cState.completions.length;
|
|
239
|
+
cActions.setCompIdx(i => (key.upArrow ? (i - 1 + len) % len : (i + 1) % len));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (key.wheelUp || key.wheelDown) {
|
|
243
|
+
const dir = key.wheelUp ? -1 : 1;
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
// Modifier-held wheel = precision mode: one row per frame, no accel.
|
|
246
|
+
// Smooth mice / trackpads emit tiny same-frame bursts; coalesce those
|
|
247
|
+
// without the old 80ms throttle that made opt-scroll feel stepped.
|
|
248
|
+
// SGR/X10 mouse encoding only carries shift/meta/ctrl bits; Cmd on
|
|
249
|
+
// macOS is intercepted by the terminal, so we honor Option (meta) on
|
|
250
|
+
// Mac / Alt (meta) on Win+Linux / Ctrl as a portable fallback. Shift
|
|
251
|
+
// is reserved for selection extension.
|
|
252
|
+
const hasModifier = key.meta || key.ctrl;
|
|
253
|
+
const precision = computePrecisionWheelStep(precisionWheelRef.current, dir, hasModifier, now);
|
|
254
|
+
if (precision.active) {
|
|
255
|
+
// Entering precision mode must discard any accelerated wheel state;
|
|
256
|
+
// otherwise the next normal wheel event inherits stale momentum.
|
|
257
|
+
if (precision.entered) {
|
|
258
|
+
wheelAccelRef.current = initWheelAccelForHost();
|
|
259
|
+
}
|
|
260
|
+
return precision.rows ? scrollTranscript(dir * wheelStep) : undefined;
|
|
261
|
+
}
|
|
262
|
+
// 0 = direction-flip bounce deferred; skip the no-op scroll.
|
|
263
|
+
const rows = computeWheelStep(wheelAccelRef.current, dir, now);
|
|
264
|
+
return rows ? scrollTranscript(dir * rows * wheelStep) : undefined;
|
|
265
|
+
}
|
|
266
|
+
if (key.shift && key.upArrow) {
|
|
267
|
+
return scrollTranscript(-1);
|
|
268
|
+
}
|
|
269
|
+
if (key.shift && key.downArrow) {
|
|
270
|
+
return scrollTranscript(1);
|
|
271
|
+
}
|
|
272
|
+
if (key.pageUp || key.pageDown) {
|
|
273
|
+
// Half-viewport keeps 50% continuity and stays under Ink's
|
|
274
|
+
// `delta < innerHeight` DECSTBM fast-path threshold.
|
|
275
|
+
const viewport = terminal.scrollRef.current?.getViewportHeight() ?? Math.max(6, (terminal.stdout?.rows ?? 24) - 8);
|
|
276
|
+
const step = Math.max(4, Math.floor(viewport / 2));
|
|
277
|
+
return scrollTranscript(key.pageUp ? -step : step);
|
|
278
|
+
}
|
|
279
|
+
// Escape-based voice bindings (ctrl/alt/super+escape) must win before the
|
|
280
|
+
// generic Esc handlers below; otherwise queue-edit cancel / selection-clear
|
|
281
|
+
// would swallow the chord and /voice would advertise a shortcut that never
|
|
282
|
+
// actually toggles recording in those UI states.
|
|
283
|
+
if (key.escape && isVoiceToggleKey(key, ch, voice.recordKey)) {
|
|
284
|
+
return voiceRecordToggle();
|
|
285
|
+
}
|
|
286
|
+
// Queue-edit cancel beats selection-clear for plain Esc: the queue header
|
|
287
|
+
// explicitly promises "Esc cancel", so honoring it takes priority over the
|
|
288
|
+
// implicit selection-dismissal convention. Without an active edit, fall through.
|
|
289
|
+
if (key.escape && cState.queueEditIdx !== null) {
|
|
290
|
+
return cActions.clearIn();
|
|
291
|
+
}
|
|
292
|
+
if (key.escape && terminal.hasSelection) {
|
|
293
|
+
return clearSelection();
|
|
294
|
+
}
|
|
295
|
+
if (key.upArrow && !cState.inputBuf.length) {
|
|
296
|
+
const inputSel = getInputSelection();
|
|
297
|
+
const cursor = inputSel && inputSel.start === inputSel.end ? inputSel.start : null;
|
|
298
|
+
const noLineAbove = !cState.input || (cursor !== null && cState.input.lastIndexOf('\n', Math.max(0, cursor - 1)) < 0);
|
|
299
|
+
if (noLineAbove) {
|
|
300
|
+
cycleQueue(1) || cycleHistory(-1);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (key.downArrow && !cState.inputBuf.length) {
|
|
305
|
+
const inputSel = getInputSelection();
|
|
306
|
+
const cursor = inputSel && inputSel.start === inputSel.end ? inputSel.start : null;
|
|
307
|
+
const noLineBelow = !cState.input || (cursor !== null && cState.input.indexOf('\n', cursor) < 0);
|
|
308
|
+
if (noLineBelow || cState.historyIdx !== null) {
|
|
309
|
+
cycleQueue(-1) || cycleHistory(1);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (isCopyShortcut(key, ch)) {
|
|
314
|
+
if (terminal.hasSelection) {
|
|
315
|
+
return copySelection();
|
|
316
|
+
}
|
|
317
|
+
const inputSel = getInputSelection();
|
|
318
|
+
if (inputSel && inputSel.end > inputSel.start) {
|
|
319
|
+
inputSel.clear();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// On macOS, Cmd+C with no selection is a no-op (Ctrl+C below handles interrupt).
|
|
323
|
+
// On non-macOS, isAction uses Ctrl, so fall through to interrupt/clear/exit.
|
|
324
|
+
if (isMac) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (isCtrl(key, ch, 'x') && cState.queueEditIdx !== null) {
|
|
329
|
+
cActions.removeQueue(cState.queueEditIdx);
|
|
330
|
+
return cActions.clearIn();
|
|
331
|
+
}
|
|
332
|
+
if (key.ctrl && ch.toLowerCase() === 'c') {
|
|
333
|
+
if (live.busy && live.sid) {
|
|
334
|
+
return turnController.interruptTurn({
|
|
335
|
+
appendMessage: actions.appendMessage,
|
|
336
|
+
gw: gateway.gw,
|
|
337
|
+
sid: live.sid,
|
|
338
|
+
sys: actions.sys
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (cState.input || cState.inputBuf.length) {
|
|
342
|
+
return cActions.clearIn();
|
|
343
|
+
}
|
|
344
|
+
return actions.die();
|
|
345
|
+
}
|
|
346
|
+
if (isAction(key, ch, 'd')) {
|
|
347
|
+
return actions.die();
|
|
348
|
+
}
|
|
349
|
+
if (isAction(key, ch, 'l')) {
|
|
350
|
+
clearSelection();
|
|
351
|
+
forceRedraw(terminal.stdout ?? process.stdout);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (isVoiceToggleKey(key, ch, voice.recordKey)) {
|
|
355
|
+
return voiceRecordToggle();
|
|
356
|
+
}
|
|
357
|
+
// Cmd/Ctrl+G, plus Alt+G fallback for VSCode/Cursor (they bind the
|
|
358
|
+
// primary keystroke to "Find Next" before the TUI sees it; Alt+G
|
|
359
|
+
// arrives as meta+g across platforms).
|
|
360
|
+
if (ch.toLowerCase() === 'g' && (isAction(key, ch, 'g') || key.meta)) {
|
|
361
|
+
return void cActions.openEditor().catch((err) => {
|
|
362
|
+
actions.sys(err instanceof Error ? `failed to open editor: ${err.message}` : 'failed to open editor');
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
// shift-tab flips yolo without spending a turn (claude-code parity)
|
|
366
|
+
if (key.shift && key.tab && !cState.completions.length) {
|
|
367
|
+
if (!live.sid) {
|
|
368
|
+
return void actions.sys('yolo needs an active session');
|
|
369
|
+
}
|
|
370
|
+
// gateway.rpc swallows errors with its own sys() message and resolves to null,
|
|
371
|
+
// so we only speak when it came back with a real shape. null = rpc already spoke.
|
|
372
|
+
return void gateway.rpc('config.set', { key: 'yolo', session_id: live.sid }).then(r => {
|
|
373
|
+
if (r?.value === '1') {
|
|
374
|
+
return actions.sys('yolo on');
|
|
375
|
+
}
|
|
376
|
+
if (r?.value === '0') {
|
|
377
|
+
return actions.sys('yolo off');
|
|
378
|
+
}
|
|
379
|
+
if (r) {
|
|
380
|
+
actions.sys('failed to toggle yolo');
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
if (key.tab && cState.completions.length) {
|
|
385
|
+
const row = cState.completions[cState.compIdx];
|
|
386
|
+
if (row?.text) {
|
|
387
|
+
const text = cState.input.startsWith('/') && row.text.startsWith('/') && cState.compReplace > 0
|
|
388
|
+
? row.text.slice(1)
|
|
389
|
+
: row.text;
|
|
390
|
+
cActions.setInput(cState.input.slice(0, cState.compReplace) + text);
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (isAction(key, ch, 'k') && cRefs.queueRef.current.length && live.sid) {
|
|
395
|
+
const next = cActions.dequeue();
|
|
396
|
+
if (next) {
|
|
397
|
+
cActions.setQueueEdit(null);
|
|
398
|
+
actions.dispatchSubmission(next);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
return { pagerPageSize };
|
|
403
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Ported from CVC Agent (https://github.com/NousResearch/cvc)
|
|
4
|
+
// Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
|
|
5
|
+
import { useEffect, useRef } from 'react';
|
|
6
|
+
import { LONG_RUN_CHARMS } from '../content/charms.js';
|
|
7
|
+
import { pick, toolTrailLabel } from '../lib/text.js';
|
|
8
|
+
import { turnController } from './turnController.js';
|
|
9
|
+
import { useTurnSelector } from './turnStore.js';
|
|
10
|
+
import { getUiState } from './uiStore.js';
|
|
11
|
+
const DELAY_MS = 8_000;
|
|
12
|
+
const INTERVAL_MS = 10_000;
|
|
13
|
+
const MAX_CHARMS_PER_TOOL = 2;
|
|
14
|
+
export function useLongRunToolCharms() {
|
|
15
|
+
const tools = useTurnSelector(state => state.tools);
|
|
16
|
+
const slots = useRef(new Map());
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!getUiState().busy || !tools.length) {
|
|
19
|
+
slots.current.clear();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const tick = () => {
|
|
23
|
+
if (!getUiState().busy) {
|
|
24
|
+
slots.current.clear();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const liveIds = new Set(tools.map(t => t.id));
|
|
29
|
+
for (const key of Array.from(slots.current.keys())) {
|
|
30
|
+
if (!liveIds.has(key)) {
|
|
31
|
+
slots.current.delete(key);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (const tool of tools) {
|
|
35
|
+
if (!tool.startedAt || now - tool.startedAt < DELAY_MS) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const slot = slots.current.get(tool.id) ?? { count: 0, lastAt: 0 };
|
|
39
|
+
if (slot.count >= MAX_CHARMS_PER_TOOL || now - slot.lastAt < INTERVAL_MS) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
slots.current.set(tool.id, { count: slot.count + 1, lastAt: now });
|
|
43
|
+
turnController.pushActivity(`${pick(LONG_RUN_CHARMS)} (${toolTrailLabel(tool.name)} · ${Math.round((now - tool.startedAt) / 1000)}s)`);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
tick();
|
|
47
|
+
const id = setInterval(tick, 1000);
|
|
48
|
+
return () => clearInterval(id);
|
|
49
|
+
}, [tools]);
|
|
50
|
+
}
|