klyro 1.0.4 → 1.0.5
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/README.md +29 -0
- package/dist/agent/runtime.js +11 -2
- package/dist/cli/auth.js +11 -3
- package/dist/cli/completion.js +63 -10
- package/dist/cli/doctor.js +13 -0
- package/dist/cli/repl.js +116 -10
- package/dist/cli/run.js +14 -0
- package/dist/cli/slash/parser.d.ts +7 -1
- package/dist/cli/slash/parser.js +37 -10
- package/dist/cli/update.d.ts +8 -4
- package/dist/cli/update.js +50 -7
- package/dist/context/accounting.d.ts +8 -0
- package/dist/context/accounting.js +18 -1
- package/dist/context/compaction.d.ts +1 -0
- package/dist/context/compaction.js +2 -1
- package/dist/index.js +78 -8
- package/dist/mcp/config.d.ts +7 -0
- package/dist/mcp/config.js +45 -0
- package/dist/mcp/registry.js +6 -4
- package/dist/mcp/serve.d.ts +23 -0
- package/dist/mcp/serve.js +111 -0
- package/dist/policy/engine.d.ts +11 -1
- package/dist/policy/engine.js +14 -1
- package/dist/policy/secret-redactor.js +4 -1
- package/dist/shared/error-map.d.ts +19 -0
- package/dist/shared/error-map.js +58 -0
- package/dist/tools/lsp/diagnostics.d.ts +35 -4
- package/dist/tools/lsp/diagnostics.js +88 -9
- package/dist/tools/normalize.d.ts +3 -0
- package/dist/tools/normalize.js +8 -5
- package/dist/tools/search/dependencies.d.ts +2 -2
- package/dist/tools/shell/background.d.ts +6 -0
- package/dist/tools/shell/background.js +17 -0
- package/dist/tools/symbols/find-symbol.d.ts +1 -1
- package/dist/tools/symbols/find-symbol.js +8 -6
- package/dist/tools/types.d.ts +8 -1
- package/dist/tui/app.d.ts +2 -0
- package/dist/tui/app.js +324 -50
- package/dist/tui/app.test.js +42 -3
- package/dist/tui/markdown.js +9 -0
- package/dist/tui/mouse.d.ts +26 -1
- package/dist/tui/mouse.js +104 -6
- package/dist/tui/scroll-flow.test.js +3 -1
- package/dist/tui/tokens.d.ts +6 -6
- package/dist/tui/tokens.js +9 -6
- package/package.json +1 -1
package/dist/tui/app.js
CHANGED
|
@@ -13,6 +13,7 @@ import { tokens, g } from './tokens.js';
|
|
|
13
13
|
import { initialScroll, scrollReducer, resolveTopRow, maxTopFor, badgeLabel, } from './scroll-model.js';
|
|
14
14
|
import { buildIndex, itemAtRow, MeasureCache } from './measure.js';
|
|
15
15
|
import { renderMarkdownLines } from './markdown.js';
|
|
16
|
+
import { redact } from '../policy/secret-redactor.js';
|
|
16
17
|
import { readVersion } from '../version.js';
|
|
17
18
|
import { getTranscriptCommand, } from './transcript-commands.js';
|
|
18
19
|
import { estimateCost, getModelInfo } from '../providers/model-info.js';
|
|
@@ -85,6 +86,24 @@ function groupTools(items) {
|
|
|
85
86
|
flush();
|
|
86
87
|
return out;
|
|
87
88
|
}
|
|
89
|
+
// Input line with an explicit block cursor for vim normal mode.
|
|
90
|
+
// Same single-<Text> invariant as MarkdownText: nested inline parts only,
|
|
91
|
+
// so the row-direction parent never lays siblings out as columns.
|
|
92
|
+
function InputWithCursor({ input, cursor }) {
|
|
93
|
+
const dimColor = tokens.colors.dim;
|
|
94
|
+
if (!input) {
|
|
95
|
+
return (_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: dimColor, children: "Message Klyro..." }), "|"] }));
|
|
96
|
+
}
|
|
97
|
+
const safe = redact(input);
|
|
98
|
+
if (cursor === null) {
|
|
99
|
+
return _jsxs(Text, { wrap: "wrap", children: [safe, "|"] });
|
|
100
|
+
}
|
|
101
|
+
const pos = Math.max(0, Math.min(cursor, safe.length));
|
|
102
|
+
const before = safe.slice(0, pos);
|
|
103
|
+
const ch = safe.slice(pos, pos + 1) || ' ';
|
|
104
|
+
const after = safe.slice(pos + 1);
|
|
105
|
+
return (_jsxs(Text, { wrap: "wrap", children: [before, _jsx(Text, { inverse: true, children: ch }), after] }));
|
|
106
|
+
}
|
|
88
107
|
// design.md §23/§24 — terminal Markdown via tui/markdown.ts: headings,
|
|
89
108
|
// **bold**, *italic*, `code`, fences, links, lists. Ink wraps the text.
|
|
90
109
|
//
|
|
@@ -93,7 +112,9 @@ function groupTools(items) {
|
|
|
93
112
|
// out as side-by-side COLUMNS (garbled transcript) instead of lines.
|
|
94
113
|
function MarkdownText({ text, dim, width }) {
|
|
95
114
|
void width;
|
|
96
|
-
|
|
115
|
+
// Display boundary: scrub secret shapes before painting the screen.
|
|
116
|
+
// Execution data is untouched — this only affects rendered output.
|
|
117
|
+
const lines = useMemo(() => renderMarkdownLines(redact(text)), [text]);
|
|
97
118
|
const dimColor = tokens.colors.dim;
|
|
98
119
|
const softColor = tokens.colors.soft;
|
|
99
120
|
return (_jsx(Text, { wrap: "wrap", color: dim ? dimColor : undefined, children: lines.map((l, i) => (_jsxs(React.Fragment, { children: [i > 0 ? '\n' : null, l.parts.map((p, j) => {
|
|
@@ -227,6 +248,58 @@ export function App(props) {
|
|
|
227
248
|
const t = setTimeout(toBottom, 0);
|
|
228
249
|
return () => clearTimeout(t);
|
|
229
250
|
}, [submitKey]);
|
|
251
|
+
// Double-Esc convention: the first Esc while running only arms (with a
|
|
252
|
+
// status hint); a second Esc within 1500ms cancels the stream. Single-Esc
|
|
253
|
+
// immediate cancel was too easy to hit by accident next to Shift+Enter.
|
|
254
|
+
const escArmedAt = useRef(0);
|
|
255
|
+
const [escArmed, setEscArmed] = useState(false);
|
|
256
|
+
const escArmTimer = useRef(null);
|
|
257
|
+
const disarmEsc = useCallback(() => {
|
|
258
|
+
escArmedAt.current = 0;
|
|
259
|
+
setEscArmed(false);
|
|
260
|
+
if (escArmTimer.current) {
|
|
261
|
+
clearTimeout(escArmTimer.current);
|
|
262
|
+
escArmTimer.current = null;
|
|
263
|
+
}
|
|
264
|
+
}, []);
|
|
265
|
+
// Minimal vim input mode (/vim toggles live via directHooks.setVimMode).
|
|
266
|
+
// Normal mode: h/l/0/$ move, x deletes, i/a enter insert, j/k scroll a
|
|
267
|
+
// line; every other printable key is swallowed (never typed). The cursor
|
|
268
|
+
// is an explicit index (null = end); rendering clamps it.
|
|
269
|
+
const [vimMode, setVimMode] = useState('insert');
|
|
270
|
+
const [vimCursor, setVimCursor] = useState(null);
|
|
271
|
+
const setVimModeLive = useCallback((m) => {
|
|
272
|
+
setVimMode(m);
|
|
273
|
+
if (m === 'insert')
|
|
274
|
+
setVimCursor(null);
|
|
275
|
+
}, []);
|
|
276
|
+
// Cursor-aware editing: mirrors for synchronous use inside key handlers.
|
|
277
|
+
const inputRef = useRef('');
|
|
278
|
+
const vimCursorRef = useRef(null);
|
|
279
|
+
useEffect(() => { inputRef.current = input; }, [input]);
|
|
280
|
+
useEffect(() => { vimCursorRef.current = vimCursor; }, [vimCursor]);
|
|
281
|
+
const clampCursor = (v, c) => c === null ? v.length : Math.max(0, Math.min(c, v.length));
|
|
282
|
+
const insertAtCursor = useCallback((text) => {
|
|
283
|
+
const t = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
284
|
+
if (!t)
|
|
285
|
+
return;
|
|
286
|
+
const v = inputRef.current;
|
|
287
|
+
const pos = clampCursor(v, vimCursorRef.current);
|
|
288
|
+
const nv = v.slice(0, pos) + t + v.slice(pos);
|
|
289
|
+
setInput(nv);
|
|
290
|
+
setVimCursor(pos + t.length >= nv.length ? null : pos + t.length);
|
|
291
|
+
setHistIdx(null);
|
|
292
|
+
}, []);
|
|
293
|
+
const deleteBeforeCursor = useCallback(() => {
|
|
294
|
+
const v = inputRef.current;
|
|
295
|
+
const pos = clampCursor(v, vimCursorRef.current);
|
|
296
|
+
if (pos === 0)
|
|
297
|
+
return;
|
|
298
|
+
const nv = v.slice(0, pos - 1) + v.slice(pos);
|
|
299
|
+
setInput(nv);
|
|
300
|
+
setVimCursor(pos - 1 >= nv.length ? null : pos - 1);
|
|
301
|
+
setHistIdx(null);
|
|
302
|
+
}, []);
|
|
230
303
|
// Shift+Enter intent: explicit shift+return, kitty/CSI-u sequence, legacy
|
|
231
304
|
// ESC+CR pair, or Esc immediately followed by Return (75ms, non-empty input).
|
|
232
305
|
const escReturnAt = useRef(0);
|
|
@@ -239,6 +312,88 @@ export function App(props) {
|
|
|
239
312
|
top: () => { },
|
|
240
313
|
reset: () => { },
|
|
241
314
|
});
|
|
315
|
+
// Streaming render throttle: per-token setState copies the whole
|
|
316
|
+
// transcript array AND the growing string (O(n²) over long streams).
|
|
317
|
+
// Deltas accumulate in refs (the truth) and flush to state at most
|
|
318
|
+
// every STREAM_FLUSH_MS, plus on turn end / new items. Renders stay
|
|
319
|
+
// live (≤64ms stale) while array+string copies stay bounded.
|
|
320
|
+
// Declared early: queued-input and key handlers below call resetStream.
|
|
321
|
+
const STREAM_FLUSH_MS = 64;
|
|
322
|
+
const streamTextRef = useRef('');
|
|
323
|
+
const streamFlushAt = useRef(0);
|
|
324
|
+
const streamTimer = useRef(null);
|
|
325
|
+
const thinkingTextRef = useRef('');
|
|
326
|
+
const thinkingFlushAt = useRef(0);
|
|
327
|
+
const thinkingTimer = useRef(null);
|
|
328
|
+
// Declared before the flush callbacks that capture it.
|
|
329
|
+
const thinkingIdRef = useRef(null);
|
|
330
|
+
const flushStream = useCallback(() => {
|
|
331
|
+
const sid = streamingIdRef.current;
|
|
332
|
+
if (streamTimer.current) {
|
|
333
|
+
clearTimeout(streamTimer.current);
|
|
334
|
+
streamTimer.current = null;
|
|
335
|
+
}
|
|
336
|
+
if (!sid)
|
|
337
|
+
return;
|
|
338
|
+
const text = streamTextRef.current;
|
|
339
|
+
streamFlushAt.current = Date.now();
|
|
340
|
+
setTranscript((prev) => {
|
|
341
|
+
const idx = prev.findIndex((x) => x.id === sid);
|
|
342
|
+
if (idx === -1)
|
|
343
|
+
return [...prev, { id: sid, kind: 'text', text, role: 'assistant' }];
|
|
344
|
+
const cur = prev[idx];
|
|
345
|
+
if (cur.text === text)
|
|
346
|
+
return prev;
|
|
347
|
+
const copy = [...prev];
|
|
348
|
+
copy[idx] = { ...cur, text };
|
|
349
|
+
return copy;
|
|
350
|
+
});
|
|
351
|
+
}, []);
|
|
352
|
+
const flushThinking = useCallback(() => {
|
|
353
|
+
const tid = thinkingIdRef.current;
|
|
354
|
+
if (thinkingTimer.current) {
|
|
355
|
+
clearTimeout(thinkingTimer.current);
|
|
356
|
+
thinkingTimer.current = null;
|
|
357
|
+
}
|
|
358
|
+
if (!tid)
|
|
359
|
+
return;
|
|
360
|
+
const text = thinkingTextRef.current;
|
|
361
|
+
thinkingFlushAt.current = Date.now();
|
|
362
|
+
setTranscript((prev) => {
|
|
363
|
+
const idx = prev.findIndex((x) => x.id === tid);
|
|
364
|
+
if (idx === -1)
|
|
365
|
+
return [...prev, { id: tid, kind: 'thinking', text }];
|
|
366
|
+
const cur = prev[idx];
|
|
367
|
+
if (cur.text === text)
|
|
368
|
+
return prev;
|
|
369
|
+
const copy = [...prev];
|
|
370
|
+
copy[idx] = { ...cur, text };
|
|
371
|
+
return copy;
|
|
372
|
+
});
|
|
373
|
+
}, []);
|
|
374
|
+
const resetStream = useCallback(() => {
|
|
375
|
+
streamingIdRef.current = null;
|
|
376
|
+
streamTextRef.current = '';
|
|
377
|
+
if (streamTimer.current) {
|
|
378
|
+
clearTimeout(streamTimer.current);
|
|
379
|
+
streamTimer.current = null;
|
|
380
|
+
}
|
|
381
|
+
}, []);
|
|
382
|
+
const resetThinking = useCallback(() => {
|
|
383
|
+
thinkingIdRef.current = null;
|
|
384
|
+
thinkingTextRef.current = '';
|
|
385
|
+
if (thinkingTimer.current) {
|
|
386
|
+
clearTimeout(thinkingTimer.current);
|
|
387
|
+
thinkingTimer.current = null;
|
|
388
|
+
}
|
|
389
|
+
}, []);
|
|
390
|
+
// Flush pending tail before unmount so no trailing delta is lost.
|
|
391
|
+
useEffect(() => () => {
|
|
392
|
+
if (streamTimer.current)
|
|
393
|
+
clearTimeout(streamTimer.current);
|
|
394
|
+
if (thinkingTimer.current)
|
|
395
|
+
clearTimeout(thinkingTimer.current);
|
|
396
|
+
}, []);
|
|
242
397
|
const width = stdout?.columns ?? 100;
|
|
243
398
|
const height = stdout?.rows ?? 30;
|
|
244
399
|
const isFullscreen = props.isFullscreen ?? false;
|
|
@@ -394,60 +549,74 @@ export function App(props) {
|
|
|
394
549
|
const toSend = queuedInputs[0];
|
|
395
550
|
setQueuedInputs((prev) => prev.slice(1));
|
|
396
551
|
setTranscript((prev) => [...prev, { id: nextId('user'), kind: 'text', text: toSend, role: 'user' }]);
|
|
397
|
-
|
|
552
|
+
resetStream();
|
|
398
553
|
const cmd = parseSlash(toSend.trim());
|
|
399
554
|
if (cmd.kind === 'prompt')
|
|
400
555
|
void props.onPrompt(cmd.text);
|
|
401
556
|
else
|
|
402
557
|
void props.onSlash(cmd);
|
|
403
558
|
}
|
|
404
|
-
}, [queuedInputs, status.status, awaitingApproval]);
|
|
559
|
+
}, [queuedInputs, status.status, awaitingApproval, resetStream]);
|
|
405
560
|
useEffect(() => { if (status.status !== 'running')
|
|
406
561
|
return; const start = Date.now() - elapsed; const t = setInterval(() => setElapsed(Date.now() - start), 1000); return () => clearInterval(t); }, [status.status, elapsed]);
|
|
407
|
-
const append = useCallback((item) => {
|
|
408
|
-
|
|
562
|
+
const append = useCallback((item) => {
|
|
563
|
+
if (item.kind !== 'text' || item.role !== 'assistant') {
|
|
564
|
+
flushStream();
|
|
565
|
+
resetStream();
|
|
566
|
+
}
|
|
567
|
+
setTranscript((prev) => [...prev, item]);
|
|
568
|
+
}, [flushStream, resetStream]);
|
|
409
569
|
const appendDelta = useCallback((text) => {
|
|
410
570
|
if (!text)
|
|
411
571
|
return;
|
|
412
|
-
|
|
413
|
-
if (sid)
|
|
414
|
-
setTranscript((prev) => { const idx = prev.findIndex((x) => x.id === sid); if (idx === -1)
|
|
415
|
-
return [...prev, { id: sid, kind: 'text', text, role: 'assistant' }]; const cur = prev[idx]; const copy = [...prev]; copy[idx] = { ...cur, text: cur.text + text }; return copy; });
|
|
416
|
-
else {
|
|
572
|
+
if (!streamingIdRef.current) {
|
|
417
573
|
const id = nextId('stream');
|
|
418
574
|
streamingIdRef.current = id;
|
|
419
|
-
|
|
575
|
+
streamTextRef.current = '';
|
|
576
|
+
streamFlushAt.current = 0;
|
|
420
577
|
}
|
|
421
|
-
|
|
578
|
+
streamTextRef.current += text;
|
|
579
|
+
const now = Date.now();
|
|
580
|
+
if (now - streamFlushAt.current >= STREAM_FLUSH_MS)
|
|
581
|
+
flushStream();
|
|
582
|
+
else if (!streamTimer.current)
|
|
583
|
+
streamTimer.current = setTimeout(flushStream, STREAM_FLUSH_MS - (now - streamFlushAt.current));
|
|
584
|
+
}, [flushStream]);
|
|
422
585
|
// Ephemeral reasoning display: merges into one transient item (never in
|
|
423
586
|
// context/persistence); removed when the turn's answer completes.
|
|
424
|
-
const thinkingIdRef = useRef(null);
|
|
425
587
|
const appendThinkingDelta = useCallback((text) => {
|
|
426
588
|
if (!text)
|
|
427
589
|
return;
|
|
428
|
-
|
|
429
|
-
if (tid)
|
|
430
|
-
setTranscript((prev) => { const idx = prev.findIndex((x) => x.id === tid); if (idx === -1)
|
|
431
|
-
return [...prev, { id: tid, kind: 'thinking', text }]; const cur = prev[idx]; const copy = [...prev]; copy[idx] = { ...cur, text: cur.text + text }; return copy; });
|
|
432
|
-
else {
|
|
590
|
+
if (!thinkingIdRef.current) {
|
|
433
591
|
const id = nextId('thinking');
|
|
434
592
|
thinkingIdRef.current = id;
|
|
435
|
-
|
|
593
|
+
thinkingTextRef.current = '';
|
|
594
|
+
thinkingFlushAt.current = 0;
|
|
436
595
|
}
|
|
437
|
-
|
|
596
|
+
thinkingTextRef.current += text;
|
|
597
|
+
const now = Date.now();
|
|
598
|
+
if (now - thinkingFlushAt.current >= STREAM_FLUSH_MS)
|
|
599
|
+
flushThinking();
|
|
600
|
+
else if (!thinkingTimer.current)
|
|
601
|
+
thinkingTimer.current = setTimeout(flushThinking, STREAM_FLUSH_MS - (now - thinkingFlushAt.current));
|
|
602
|
+
}, [flushThinking]);
|
|
438
603
|
const clearThinking = useCallback(() => {
|
|
439
|
-
|
|
604
|
+
resetThinking();
|
|
440
605
|
setTranscript((prev) => (prev.some((x) => x.kind === 'thinking') ? prev.filter((x) => x.kind !== 'thinking') : prev));
|
|
441
|
-
}, []);
|
|
606
|
+
}, [resetThinking]);
|
|
442
607
|
useEffect(() => {
|
|
443
608
|
if (status.status !== 'running') {
|
|
444
|
-
|
|
445
|
-
|
|
609
|
+
// Turn end: flush any throttled tail first so the final text is
|
|
610
|
+
// complete, then drop the transient thinking block as before.
|
|
611
|
+
flushStream();
|
|
612
|
+
resetStream();
|
|
613
|
+
resetThinking();
|
|
614
|
+
disarmEsc();
|
|
446
615
|
// Runs that end without final_text (abort/cancel/error) must not leave
|
|
447
616
|
// stale thinking blocks behind — only the response may remain.
|
|
448
617
|
setTranscript((prev) => (prev.some((x) => x.kind === 'thinking') ? prev.filter((x) => x.kind !== 'thinking') : prev));
|
|
449
618
|
}
|
|
450
|
-
}, [status.status]);
|
|
619
|
+
}, [status.status, flushStream, resetStream, resetThinking, disarmEsc]);
|
|
451
620
|
const updateStatus = useCallback((s) => setStatus((p) => ({ ...p, ...s })), []);
|
|
452
621
|
const updatePlan = useCallback((p) => setPlan(p), []);
|
|
453
622
|
// Tool results patch the running start-item IN PLACE (no second item, so a
|
|
@@ -465,14 +634,14 @@ export function App(props) {
|
|
|
465
634
|
});
|
|
466
635
|
}, []);
|
|
467
636
|
const clearTranscript = useCallback(() => {
|
|
468
|
-
|
|
469
|
-
|
|
637
|
+
resetStream();
|
|
638
|
+
resetThinking();
|
|
470
639
|
setTranscript([]);
|
|
471
640
|
setPlan([]);
|
|
472
641
|
// Fresh content → fresh scroll (a pruned anchor would otherwise stick to
|
|
473
642
|
// the bottom with a stale newSinceUnstick badge count).
|
|
474
643
|
scrollCmdsRef.current.reset();
|
|
475
|
-
}, []);
|
|
644
|
+
}, [resetStream, resetThinking]);
|
|
476
645
|
// Scroll control for external drivers (mouse-wheel tap in repl.ts, §8.4).
|
|
477
646
|
// Stored in refs so the callbacks stay stable while acting on latest state.
|
|
478
647
|
const scrollLines = useCallback((delta) => { scrollCmdsRef.current.line(delta); }, []);
|
|
@@ -500,7 +669,12 @@ export function App(props) {
|
|
|
500
669
|
}), []);
|
|
501
670
|
const onMountedRef = useRef(props.onMounted);
|
|
502
671
|
useEffect(() => { onMountedRef.current = props.onMounted; }, [props.onMounted]);
|
|
503
|
-
|
|
672
|
+
// Bracketed-paste bulk insert: one atomic input append (no per-char
|
|
673
|
+
// autocomplete/history side effects) with newline normalization.
|
|
674
|
+
const pasteText = useCallback((text) => {
|
|
675
|
+
insertAtCursor(text);
|
|
676
|
+
}, [insertAtCursor]);
|
|
677
|
+
useEffect(() => { onMountedRef.current?.({ append, appendDelta, updateStatus, updatePlan, clearTranscript, scrollLines, scrollToBottom, scrollHalfPage, scrollToTop, transcript: transcriptHandle, updateTool, appendThinkingDelta, clearThinking, pasteText, setVimMode: setVimModeLive }); globalThis.__klyroAppAppend = append; globalThis.__klyroAppendDelta = appendDelta; globalThis.__klyroAppStatus = updateStatus; globalThis.__klyroAppPlan = updatePlan; globalThis.__klyroAppendThinking = appendThinkingDelta; globalThis.__klyroClearThinking = clearThinking; return () => { delete globalThis.__klyroAppAppend; delete globalThis.__klyroAppendDelta; delete globalThis.__klyroAppStatus; delete globalThis.__klyroAppPlan; delete globalThis.__klyroAppendThinking; delete globalThis.__klyroClearThinking; }; }, [append, appendDelta, updateStatus, updatePlan, clearTranscript, scrollLines, scrollToBottom, scrollHalfPage, scrollToTop, transcriptHandle, updateTool, appendThinkingDelta, clearThinking, pasteText, setVimModeLive]);
|
|
504
678
|
const toggleGroup = (id) => setExpandedGroups((prev) => { const n = new Set(prev); if (n.has(id))
|
|
505
679
|
n.delete(id);
|
|
506
680
|
else
|
|
@@ -512,12 +686,14 @@ export function App(props) {
|
|
|
512
686
|
useInput((inputStr, key) => {
|
|
513
687
|
if (key.escape && queuedInputs.length > 0) {
|
|
514
688
|
setQueuedInputs((prev) => prev.slice(1));
|
|
689
|
+
disarmEsc();
|
|
515
690
|
return;
|
|
516
691
|
}
|
|
517
692
|
// Tab completes the top slash suggestion (e.g. `/c` → `/clear `)
|
|
518
693
|
if ((key.tab || inputStr === '\t') && slashSuggest.length > 0) {
|
|
519
694
|
const top = slashSuggest[0];
|
|
520
695
|
setInput(`/${top.name} `);
|
|
696
|
+
setVimCursor(null);
|
|
521
697
|
return;
|
|
522
698
|
}
|
|
523
699
|
// Scroll keys (work in any mode, including while running).
|
|
@@ -581,10 +757,80 @@ export function App(props) {
|
|
|
581
757
|
inputStr === '\x1b\r' ||
|
|
582
758
|
inputStr === '\x1b[13;2u' ||
|
|
583
759
|
(key.return && input.trim() !== '' && now - escReturnAt.current < 75)) {
|
|
584
|
-
|
|
760
|
+
insertAtCursor('\n');
|
|
585
761
|
setHistIdx(null);
|
|
586
762
|
return;
|
|
587
763
|
}
|
|
764
|
+
// Vim normal mode: motion/editing keys act on the input buffer instead
|
|
765
|
+
// of typing. Return/arrows/tab/ctrl fall through to the shared logic
|
|
766
|
+
// below; every other printable key is swallowed.
|
|
767
|
+
if (vimMode === 'normal' && !awaitingApproval) {
|
|
768
|
+
// Legacy Shift+Enter partner: an Esc that just switched modes followed
|
|
769
|
+
// by Return within 75ms still means newline, not submit.
|
|
770
|
+
if (key.return && input.trim() !== '' && Date.now() - escReturnAt.current < 75) {
|
|
771
|
+
insertAtCursor('\n');
|
|
772
|
+
setVimMode('insert');
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const cur = vimCursor === null ? input.length : Math.max(0, Math.min(vimCursor, input.length));
|
|
776
|
+
const move = (d) => { setVimCursor(Math.max(0, Math.min(cur + d, input.length))); return true; };
|
|
777
|
+
if (!key.ctrl && !key.meta && !key.return && !key.tab && !key.upArrow && !key.downArrow && !key.leftArrow && !key.rightArrow) {
|
|
778
|
+
if (inputStr === 'i') {
|
|
779
|
+
setVimMode('insert');
|
|
780
|
+
setVimCursor(null);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (inputStr === 'a') {
|
|
784
|
+
setVimCursor(Math.min(cur + 1, input.length));
|
|
785
|
+
setVimMode('insert');
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (inputStr === 'h') {
|
|
789
|
+
move(-1);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (inputStr === 'l') {
|
|
793
|
+
move(1);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (inputStr === '0') {
|
|
797
|
+
setVimCursor(0);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (inputStr === '$') {
|
|
801
|
+
setVimCursor(input.length);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (inputStr === 'x') {
|
|
805
|
+
if (cur < input.length)
|
|
806
|
+
setInput(input.slice(0, cur) + input.slice(cur + 1));
|
|
807
|
+
setHistIdx(null);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (inputStr === 'j') {
|
|
811
|
+
if (isFullscreen && maxTop > 0)
|
|
812
|
+
commands.lineDown();
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (inputStr === 'k') {
|
|
816
|
+
if (isFullscreen && maxTop > 0)
|
|
817
|
+
commands.lineUp();
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
if (key.backspace || key.delete) {
|
|
821
|
+
move(-1);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (key.escape)
|
|
825
|
+
return; // already normal
|
|
826
|
+
if (inputStr)
|
|
827
|
+
return; // swallow everything else printable
|
|
828
|
+
}
|
|
829
|
+
else if (key.backspace || key.delete) {
|
|
830
|
+
move(-1);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
588
834
|
if (status.status === 'running') {
|
|
589
835
|
// design.md §18: first Ctrl+C cancels the run, second quits.
|
|
590
836
|
if (key.ctrl && inputStr === 'c') {
|
|
@@ -597,9 +843,26 @@ export function App(props) {
|
|
|
597
843
|
}
|
|
598
844
|
return;
|
|
599
845
|
}
|
|
600
|
-
// Esc
|
|
846
|
+
// Esc no longer cancels on first press: with a queued item it drops
|
|
847
|
+
// one; otherwise the first Esc arms and the second (≤1500ms) cancels.
|
|
601
848
|
if (key.escape) {
|
|
602
|
-
|
|
849
|
+
if (queuedInputs.length > 0) {
|
|
850
|
+
setQueuedInputs((prev) => prev.slice(1));
|
|
851
|
+
disarmEsc();
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const t = Date.now();
|
|
855
|
+
if (t - escArmedAt.current < 1500) {
|
|
856
|
+
disarmEsc();
|
|
857
|
+
void props.onSlash({ kind: 'cancel' });
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
860
|
+
escArmedAt.current = t;
|
|
861
|
+
setEscArmed(true);
|
|
862
|
+
if (escArmTimer.current)
|
|
863
|
+
clearTimeout(escArmTimer.current);
|
|
864
|
+
escArmTimer.current = setTimeout(disarmEsc, 1500);
|
|
865
|
+
}
|
|
603
866
|
return;
|
|
604
867
|
}
|
|
605
868
|
// Enter on empty input dismisses the badge (jump to bottom, §7.2)
|
|
@@ -614,16 +877,17 @@ export function App(props) {
|
|
|
614
877
|
return;
|
|
615
878
|
setQueuedInputs((prev) => [...prev, v]);
|
|
616
879
|
setInput('');
|
|
880
|
+
setVimCursor(null);
|
|
617
881
|
pushHistory(v);
|
|
618
882
|
setSubmitKey((k) => k + 1);
|
|
619
883
|
return;
|
|
620
884
|
}
|
|
621
885
|
if (key.backspace || key.delete) {
|
|
622
|
-
|
|
886
|
+
deleteBeforeCursor();
|
|
623
887
|
return;
|
|
624
888
|
}
|
|
625
889
|
if (!key.ctrl && !key.meta)
|
|
626
|
-
|
|
890
|
+
insertAtCursor(inputStr);
|
|
627
891
|
return;
|
|
628
892
|
}
|
|
629
893
|
// design.md §18: idle Ctrl+C exits.
|
|
@@ -639,6 +903,7 @@ export function App(props) {
|
|
|
639
903
|
const next = histIdx === null ? history.length - 1 : Math.max(0, histIdx - 1);
|
|
640
904
|
setHistIdx(next);
|
|
641
905
|
setInput(history[next] ?? '');
|
|
906
|
+
setVimCursor(null);
|
|
642
907
|
}
|
|
643
908
|
return;
|
|
644
909
|
}
|
|
@@ -658,6 +923,7 @@ export function App(props) {
|
|
|
658
923
|
setHistIdx(next);
|
|
659
924
|
setInput(history[next] ?? '');
|
|
660
925
|
}
|
|
926
|
+
setVimCursor(null);
|
|
661
927
|
return;
|
|
662
928
|
}
|
|
663
929
|
if (input.trim() !== '')
|
|
@@ -676,10 +942,11 @@ export function App(props) {
|
|
|
676
942
|
return;
|
|
677
943
|
}
|
|
678
944
|
setInput('');
|
|
945
|
+
setVimCursor(null);
|
|
679
946
|
setHistIdx(null);
|
|
680
947
|
pushHistory(v);
|
|
681
948
|
setTranscript((prev) => [...prev, { id: nextId('user'), kind: 'text', text: v, role: 'user' }]);
|
|
682
|
-
|
|
949
|
+
resetStream();
|
|
683
950
|
setSubmitKey((k) => k + 1);
|
|
684
951
|
const cmd = parseSlash(v);
|
|
685
952
|
if (cmd.kind === 'prompt')
|
|
@@ -689,13 +956,17 @@ export function App(props) {
|
|
|
689
956
|
return;
|
|
690
957
|
}
|
|
691
958
|
if (key.backspace || key.delete) {
|
|
692
|
-
|
|
693
|
-
setHistIdx(null);
|
|
959
|
+
deleteBeforeCursor();
|
|
694
960
|
return;
|
|
695
961
|
}
|
|
696
962
|
if (!key.ctrl && !key.meta) {
|
|
697
|
-
|
|
698
|
-
|
|
963
|
+
insertAtCursor(inputStr);
|
|
964
|
+
}
|
|
965
|
+
// Idle Esc enters vim normal mode (vimLive parity for keyboard-only users).
|
|
966
|
+
if (key.escape && vimMode === 'insert') {
|
|
967
|
+
setVimMode('normal');
|
|
968
|
+
setVimCursor(inputRef.current.length);
|
|
969
|
+
return;
|
|
699
970
|
}
|
|
700
971
|
});
|
|
701
972
|
// Single source of truth: package.json via version.ts — never hardcoded.
|
|
@@ -707,9 +978,12 @@ export function App(props) {
|
|
|
707
978
|
const ctxWindow = getModelInfo(status.model).contextWindow;
|
|
708
979
|
const ctxPct = totalTokens > 0 ? Math.round((totalTokens / ctxWindow) * 100) : 0;
|
|
709
980
|
// Narrow terminals: compact hints so the status bar never wraps mid-word.
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
981
|
+
// An armed first-Esc surfaces here so the operator knows the second cancels.
|
|
982
|
+
const baseHints = escArmed && status.status === 'running'
|
|
983
|
+
? 'press esc again to cancel'
|
|
984
|
+
: width < 90
|
|
985
|
+
? status.status === 'running' ? 'ctrl+c stop · enter queue' : 'enter send · / commands'
|
|
986
|
+
: status.status === 'running' ? 'ctrl+c to stop · enter to queue · ctrl+o expand' : transcript.length === 0 ? 'shift+tab to cycle · ↑/↓ for history · / for commands' : 'enter to send · shift+enter newline · @ to attach';
|
|
713
987
|
const hints = maxTop > 0 && isFullscreen ? `${baseHints} · PgUp/Dn scroll` : baseHints;
|
|
714
988
|
// I1 structural guard: the frame can never exceed terminal rows. Even if a
|
|
715
989
|
// child mis-measures, the root clips — the input/status stay on screen and
|
|
@@ -751,7 +1025,7 @@ export function App(props) {
|
|
|
751
1025
|
const marker = isExpanded ? g('expanded') : g('collapsed');
|
|
752
1026
|
const markerColor = gr.status === 'error' ? tokens.colors.err : gr.status === 'running' ? tokens.colors.warn : tokens.colors.ok;
|
|
753
1027
|
const running = gr.status === 'running';
|
|
754
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), running ? _jsxs(Text, { color: markerColor, children: [_jsx(Spinner, { type: "dots" }), " "] }) : null, _jsxs(Text, { color: markerColor, children: [marker, " ", verbLine] }), _jsxs(Text, { color: tokens.colors.dim, children: [" ", right] })] }), isExpanded ? (_jsxs(_Fragment, { children: [gr.items.slice(0, 12).map((it) => {
|
|
1028
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), running ? _jsxs(Text, { color: markerColor, children: [_jsx(Spinner, { type: "dots" }), " "] }) : null, _jsxs(Text, { color: markerColor, children: [marker, " ", redact(verbLine)] }), _jsxs(Text, { color: tokens.colors.dim, children: [" ", right] })] }), isExpanded ? (_jsxs(_Fragment, { children: [gr.items.slice(0, 12).map((it) => {
|
|
755
1029
|
let friendly = '';
|
|
756
1030
|
try {
|
|
757
1031
|
const a = JSON.parse(it.args);
|
|
@@ -769,12 +1043,12 @@ export function App(props) {
|
|
|
769
1043
|
catch {
|
|
770
1044
|
friendly = it.args.slice(0, 40);
|
|
771
1045
|
}
|
|
772
|
-
return (_jsxs(Box, { paddingLeft: 4, children: [_jsxs(Text, { color: tokens.colors.guide, children: [g('end'), " "] }), _jsx(Text, { color: tokens.colors.dim, children: friendly })] }, it.id));
|
|
1046
|
+
return (_jsxs(Box, { paddingLeft: 4, children: [_jsxs(Text, { color: tokens.colors.guide, children: [g('end'), " "] }), _jsx(Text, { color: tokens.colors.dim, children: redact(friendly) })] }, it.id));
|
|
773
1047
|
}), gr.items.length > 12 ? (_jsx(Box, { paddingLeft: 4, children: _jsxs(Text, { color: tokens.colors.dim, children: ["\u2026 ", gr.items.length - 12, " more"] }) })) : null] })) : null] }, gr.id));
|
|
774
1048
|
}
|
|
775
1049
|
const it = item;
|
|
776
1050
|
if (it.kind === 'text' && it.role === 'user') {
|
|
777
|
-
return _jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: tokens.colors.accent, bold: true, children: [g('prompt'), " "] }), _jsx(Text, { wrap: "wrap", children: it.text })] }, it.id);
|
|
1051
|
+
return _jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: tokens.colors.accent, bold: true, children: [g('prompt'), " "] }), _jsx(Text, { wrap: "wrap", children: redact(it.text) })] }, it.id);
|
|
778
1052
|
}
|
|
779
1053
|
if (it.kind === 'text') {
|
|
780
1054
|
// prose — render markdown, not raw **, with proper wrap and guide
|
|
@@ -782,15 +1056,15 @@ export function App(props) {
|
|
|
782
1056
|
}
|
|
783
1057
|
// Ephemeral reasoning: light-white while working, removed on response.
|
|
784
1058
|
if (it.kind === 'thinking')
|
|
785
|
-
return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsx(Text, { wrap: "wrap", color: tokens.colors.dim, children: it.text }) }, it.id);
|
|
1059
|
+
return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsx(Text, { wrap: "wrap", color: tokens.colors.dim, children: redact(it.text) }) }, it.id);
|
|
786
1060
|
if (it.kind === 'error')
|
|
787
|
-
return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.colors.err, children: [" ", g('guide'), " ", g('failure'), " ", it.message] }) }, it.id);
|
|
1061
|
+
return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.colors.err, children: [" ", g('guide'), " ", g('failure'), " ", redact(it.message)] }) }, it.id);
|
|
788
1062
|
if (it.kind === 'policy')
|
|
789
1063
|
return null;
|
|
790
1064
|
if (it.kind === 'file_changed')
|
|
791
|
-
return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.colors.dim, children: [" ", g('guide'), " ", g('editsBadge'), " ", it.path, " ", it.op] }) }, it.id);
|
|
1065
|
+
return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.colors.dim, children: [" ", g('guide'), " ", g('editsBadge'), " ", redact(it.path), " ", it.op] }) }, it.id);
|
|
792
1066
|
if (it.kind === 'diff')
|
|
793
|
-
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, color: tokens.colors.soft, children: it.summary ?? 'Diff' }), it.hunks.map((h, i) => (_jsxs(Box, { flexDirection: "column", marginTop: 0, children: [_jsx(Text, { color: tokens.colors.soft, children: h.path }), h.lines.map((l, j) => (_jsxs(Text, { wrap: "wrap", color: l.kind === 'add' ? tokens.colors.ok : l.kind === 'remove' ? tokens.colors.err : tokens.colors.dim, children: [l.kind === 'add' ? '+ ' : l.kind === 'remove' ? '- ' : ' ', l.text] }, j)))] }, i)))] }, it.id));
|
|
1067
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, color: tokens.colors.soft, children: it.summary ?? 'Diff' }), it.hunks.map((h, i) => (_jsxs(Box, { flexDirection: "column", marginTop: 0, children: [_jsx(Text, { color: tokens.colors.soft, children: h.path }), h.lines.map((l, j) => (_jsxs(Text, { wrap: "wrap", color: l.kind === 'add' ? tokens.colors.ok : l.kind === 'remove' ? tokens.colors.err : tokens.colors.dim, children: [l.kind === 'add' ? '+ ' : l.kind === 'remove' ? '- ' : ' ', redact(l.text)] }, j)))] }, i)))] }, it.id));
|
|
794
1068
|
return null;
|
|
795
|
-
}) : null, showThinking && status.status === 'running' && !streamingIdRef.current ? (_jsxs(Box, { paddingLeft: 2, marginBottom: 1, children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: tokens.colors.accent, children: [_jsx(Spinner, { type: "dots" }), " "] }), _jsx(Text, { color: tokens.colors.dim, children: "Thinking... (esc to cancel)" }), _jsxs(Text, { color: tokens.colors.dim, children: [" ", (elapsed / 1000).toFixed(1), "s"] })] })) : null, showPlan && plan.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginTop: 0, marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { bold: true, children: [g('todoPlan'), " Plan ", plan.filter((p) => p.status === 'done').length, "/", plan.length] })] }), plan.slice(0, 8).map((p, i) => (_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: p.status === 'done' ? tokens.colors.ok : p.status === 'in_progress' ? tokens.colors.accent : tokens.colors.dim, children: [p.status === 'done' ? g('todoDone') : p.status === 'in_progress' ? g('todoActive') : g('todoPending'), " ", i + 1, ". ", p.title] })] }, p.id)))] })) : null, showQueued && queuedInputs.length > 0 ? (_jsx(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: queuedInputs.map((q, i) => (_jsxs(Text, { color: tokens.colors.dim, children: ["queued: ", q.slice(0, 60), i === 0 ? ' esc to drop' : ''] }, i))) })) : null] }), isFullscreen && !atBottom ? (_jsx(Box, { flexDirection: "column", width: 1, marginLeft: 1, children: Array.from({ length: trackH }).map((_, i) => (_jsx(Text, { color: i === thumbPos ? tokens.colors.accent : tokens.colors.guide, children: i === thumbPos ? '●' : '│' }, i))) })) : null] }), pinned && badgeLabel(atBottom, pendingNew, status.status !== 'running') ? (_jsx(Box, { justifyContent: "center", flexShrink: 0, children: _jsxs(Text, { backgroundColor: tokens.colors.accentSoft, color: tokens.colors.accent, bold: true, children: [' ', badgeLabel(atBottom, pendingNew, status.status !== 'running'), ' '] }) })) : null, slashSuggest.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, flexShrink: 0, children: [slashSuggest.map((s, i) => (_jsxs(Text, { color: i === 0 ? tokens.colors.accent : tokens.colors.dim, children: [i === 0 ? '▸' : ' ', " /", s.name, " \u2014 ", s.hint] }, s.name))), _jsx(Text, { color: tokens.colors.dim, children: " tab to complete" })] })) : null, _jsx(ApprovalModal, { bridge: bridge }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(Text, { color: tokens.colors.guide, children: g('rule').repeat(Math.max(10, width - 2)) }), _jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.accent, bold: true, children: [g('prompt'), " "] }),
|
|
1069
|
+
}) : null, showThinking && status.status === 'running' && !streamingIdRef.current ? (_jsxs(Box, { paddingLeft: 2, marginBottom: 1, children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: tokens.colors.accent, children: [_jsx(Spinner, { type: "dots" }), " "] }), _jsx(Text, { color: tokens.colors.dim, children: "Thinking... (esc \u00D72 to cancel)" }), _jsxs(Text, { color: tokens.colors.dim, children: [" ", (elapsed / 1000).toFixed(1), "s"] })] })) : null, showPlan && plan.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginTop: 0, marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { bold: true, children: [g('todoPlan'), " Plan ", plan.filter((p) => p.status === 'done').length, "/", plan.length] })] }), plan.slice(0, 8).map((p, i) => (_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: p.status === 'done' ? tokens.colors.ok : p.status === 'in_progress' ? tokens.colors.accent : tokens.colors.dim, children: [p.status === 'done' ? g('todoDone') : p.status === 'in_progress' ? g('todoActive') : g('todoPending'), " ", i + 1, ". ", redact(p.title)] })] }, p.id)))] })) : null, showQueued && queuedInputs.length > 0 ? (_jsx(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: queuedInputs.map((q, i) => (_jsxs(Text, { color: tokens.colors.dim, children: ["queued: ", redact(q.slice(0, 60)), i === 0 ? ' esc to drop' : ''] }, i))) })) : null] }), isFullscreen && !atBottom ? (_jsx(Box, { flexDirection: "column", width: 1, marginLeft: 1, children: Array.from({ length: trackH }).map((_, i) => (_jsx(Text, { color: i === thumbPos ? tokens.colors.accent : tokens.colors.guide, children: i === thumbPos ? '●' : '│' }, i))) })) : null] }), pinned && badgeLabel(atBottom, pendingNew, status.status !== 'running') ? (_jsx(Box, { justifyContent: "center", flexShrink: 0, children: _jsxs(Text, { backgroundColor: tokens.colors.accentSoft, color: tokens.colors.accent, bold: true, children: [' ', badgeLabel(atBottom, pendingNew, status.status !== 'running'), ' '] }) })) : null, slashSuggest.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, flexShrink: 0, children: [slashSuggest.map((s, i) => (_jsxs(Text, { color: i === 0 ? tokens.colors.accent : tokens.colors.dim, children: [i === 0 ? '▸' : ' ', " /", s.name, " \u2014 ", s.hint] }, s.name))), _jsx(Text, { color: tokens.colors.dim, children: " tab to complete" })] })) : null, _jsx(ApprovalModal, { bridge: bridge }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(Text, { color: tokens.colors.guide, children: g('rule').repeat(Math.max(10, width - 2)) }), _jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.accent, bold: true, children: [g('prompt'), " "] }), vimMode === 'normal' ? _jsx(Text, { color: tokens.colors.warn, bold: true, children: "--NORMAL-- " }) : null, _jsx(InputWithCursor, { input: input, cursor: vimCursor })] }), _jsx(Text, { color: tokens.colors.guide, children: g('rule').repeat(Math.max(10, width - 2)) })] }), _jsxs(Box, { justifyContent: "space-between", flexShrink: 0, children: [_jsxs(Text, { color: tokens.colors.dim, children: [status.status === 'running' ? (_jsxs(Text, { color: tokens.colors.accent, children: [_jsx(Spinner, { type: "dots" }), " working \u00B7 "] })) : null, baseHints, maxTop > 0 && isFullscreen ? ' · PgUp/Dn scroll' : ''] }), _jsxs(Text, { color: tokens.colors.dim, children: [maxTop > 0 && isFullscreen ? `⇅ ${topRow}/${maxTop} · ` : '', cost > 0 ? `$${cost.toFixed(2)} · ` : '', ctxPct, "% ctx \u00B7 ", status.model, status.status === 'running' ? ' ●' : ''] })] })] }));
|
|
796
1070
|
}
|