klyro 1.0.3 → 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 +330 -52
- package/dist/tui/app.test.js +45 -6
- 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 +4 -2
- package/dist/tui/scroll-model.d.ts +3 -1
- package/dist/tui/scroll-model.js +6 -5
- 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
|
@@ -10,9 +10,10 @@ import { execFileSync } from 'node:child_process';
|
|
|
10
10
|
import { TuiApprovalBridge, ApprovalModal } from './approval.js';
|
|
11
11
|
import { parse as parseSlash, suggestCommands } from '../cli/slash/parser.js';
|
|
12
12
|
import { tokens, g } from './tokens.js';
|
|
13
|
-
import { initialScroll, scrollReducer, resolveTopRow, maxTopFor, } from './scroll-model.js';
|
|
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;
|
|
@@ -322,7 +477,7 @@ export function App(props) {
|
|
|
322
477
|
const blockKeys = useMemo(() => blocks.map((b) => b.key), [blocks]);
|
|
323
478
|
const blockHeights = useMemo(() => blocks.map((b) => cache.heightFor(b.key, b.desc, width)), [blocks, width, cache]);
|
|
324
479
|
const scroll = useChatScroll({ keys: blockKeys, heights: blockHeights, viewportH, width });
|
|
325
|
-
const { topRow, maxTop, pinned, pendingNew, commands } = scroll;
|
|
480
|
+
const { topRow, maxTop, pinned, pendingNew, commands, atBottom } = scroll;
|
|
326
481
|
const trackH = viewportH;
|
|
327
482
|
const thumbPos = maxTop === 0 ? 0 : Math.round((topRow / maxTop) * (trackH - 1));
|
|
328
483
|
// Slice *display lines* [topRow, topRow+viewportH): intersecting blocks only
|
|
@@ -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).
|
|
@@ -538,6 +714,10 @@ export function App(props) {
|
|
|
538
714
|
commands.jumpBottom();
|
|
539
715
|
return;
|
|
540
716
|
}
|
|
717
|
+
if (inputStr === ' ' && pinned && pendingNew > 0) {
|
|
718
|
+
commands.jumpBottom();
|
|
719
|
+
return;
|
|
720
|
+
} // Space → jump to unread tail
|
|
541
721
|
if (key.ctrl && inputStr === 'g') {
|
|
542
722
|
commands.jumpBottom();
|
|
543
723
|
return;
|
|
@@ -577,10 +757,80 @@ export function App(props) {
|
|
|
577
757
|
inputStr === '\x1b\r' ||
|
|
578
758
|
inputStr === '\x1b[13;2u' ||
|
|
579
759
|
(key.return && input.trim() !== '' && now - escReturnAt.current < 75)) {
|
|
580
|
-
|
|
760
|
+
insertAtCursor('\n');
|
|
581
761
|
setHistIdx(null);
|
|
582
762
|
return;
|
|
583
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
|
+
}
|
|
584
834
|
if (status.status === 'running') {
|
|
585
835
|
// design.md §18: first Ctrl+C cancels the run, second quits.
|
|
586
836
|
if (key.ctrl && inputStr === 'c') {
|
|
@@ -593,9 +843,26 @@ export function App(props) {
|
|
|
593
843
|
}
|
|
594
844
|
return;
|
|
595
845
|
}
|
|
596
|
-
// 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.
|
|
597
848
|
if (key.escape) {
|
|
598
|
-
|
|
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
|
+
}
|
|
599
866
|
return;
|
|
600
867
|
}
|
|
601
868
|
// Enter on empty input dismisses the badge (jump to bottom, §7.2)
|
|
@@ -610,16 +877,17 @@ export function App(props) {
|
|
|
610
877
|
return;
|
|
611
878
|
setQueuedInputs((prev) => [...prev, v]);
|
|
612
879
|
setInput('');
|
|
880
|
+
setVimCursor(null);
|
|
613
881
|
pushHistory(v);
|
|
614
882
|
setSubmitKey((k) => k + 1);
|
|
615
883
|
return;
|
|
616
884
|
}
|
|
617
885
|
if (key.backspace || key.delete) {
|
|
618
|
-
|
|
886
|
+
deleteBeforeCursor();
|
|
619
887
|
return;
|
|
620
888
|
}
|
|
621
889
|
if (!key.ctrl && !key.meta)
|
|
622
|
-
|
|
890
|
+
insertAtCursor(inputStr);
|
|
623
891
|
return;
|
|
624
892
|
}
|
|
625
893
|
// design.md §18: idle Ctrl+C exits.
|
|
@@ -635,6 +903,7 @@ export function App(props) {
|
|
|
635
903
|
const next = histIdx === null ? history.length - 1 : Math.max(0, histIdx - 1);
|
|
636
904
|
setHistIdx(next);
|
|
637
905
|
setInput(history[next] ?? '');
|
|
906
|
+
setVimCursor(null);
|
|
638
907
|
}
|
|
639
908
|
return;
|
|
640
909
|
}
|
|
@@ -654,6 +923,7 @@ export function App(props) {
|
|
|
654
923
|
setHistIdx(next);
|
|
655
924
|
setInput(history[next] ?? '');
|
|
656
925
|
}
|
|
926
|
+
setVimCursor(null);
|
|
657
927
|
return;
|
|
658
928
|
}
|
|
659
929
|
if (input.trim() !== '')
|
|
@@ -672,10 +942,11 @@ export function App(props) {
|
|
|
672
942
|
return;
|
|
673
943
|
}
|
|
674
944
|
setInput('');
|
|
945
|
+
setVimCursor(null);
|
|
675
946
|
setHistIdx(null);
|
|
676
947
|
pushHistory(v);
|
|
677
948
|
setTranscript((prev) => [...prev, { id: nextId('user'), kind: 'text', text: v, role: 'user' }]);
|
|
678
|
-
|
|
949
|
+
resetStream();
|
|
679
950
|
setSubmitKey((k) => k + 1);
|
|
680
951
|
const cmd = parseSlash(v);
|
|
681
952
|
if (cmd.kind === 'prompt')
|
|
@@ -685,13 +956,17 @@ export function App(props) {
|
|
|
685
956
|
return;
|
|
686
957
|
}
|
|
687
958
|
if (key.backspace || key.delete) {
|
|
688
|
-
|
|
689
|
-
setHistIdx(null);
|
|
959
|
+
deleteBeforeCursor();
|
|
690
960
|
return;
|
|
691
961
|
}
|
|
692
962
|
if (!key.ctrl && !key.meta) {
|
|
693
|
-
|
|
694
|
-
|
|
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;
|
|
695
970
|
}
|
|
696
971
|
});
|
|
697
972
|
// Single source of truth: package.json via version.ts — never hardcoded.
|
|
@@ -703,9 +978,12 @@ export function App(props) {
|
|
|
703
978
|
const ctxWindow = getModelInfo(status.model).contextWindow;
|
|
704
979
|
const ctxPct = totalTokens > 0 ? Math.round((totalTokens / ctxWindow) * 100) : 0;
|
|
705
980
|
// Narrow terminals: compact hints so the status bar never wraps mid-word.
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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';
|
|
709
987
|
const hints = maxTop > 0 && isFullscreen ? `${baseHints} · PgUp/Dn scroll` : baseHints;
|
|
710
988
|
// I1 structural guard: the frame can never exceed terminal rows. Even if a
|
|
711
989
|
// child mis-measures, the root clips — the input/status stay on screen and
|
|
@@ -747,7 +1025,7 @@ export function App(props) {
|
|
|
747
1025
|
const marker = isExpanded ? g('expanded') : g('collapsed');
|
|
748
1026
|
const markerColor = gr.status === 'error' ? tokens.colors.err : gr.status === 'running' ? tokens.colors.warn : tokens.colors.ok;
|
|
749
1027
|
const running = gr.status === 'running';
|
|
750
|
-
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) => {
|
|
751
1029
|
let friendly = '';
|
|
752
1030
|
try {
|
|
753
1031
|
const a = JSON.parse(it.args);
|
|
@@ -765,12 +1043,12 @@ export function App(props) {
|
|
|
765
1043
|
catch {
|
|
766
1044
|
friendly = it.args.slice(0, 40);
|
|
767
1045
|
}
|
|
768
|
-
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));
|
|
769
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));
|
|
770
1048
|
}
|
|
771
1049
|
const it = item;
|
|
772
1050
|
if (it.kind === 'text' && it.role === 'user') {
|
|
773
|
-
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);
|
|
774
1052
|
}
|
|
775
1053
|
if (it.kind === 'text') {
|
|
776
1054
|
// prose — render markdown, not raw **, with proper wrap and guide
|
|
@@ -778,15 +1056,15 @@ export function App(props) {
|
|
|
778
1056
|
}
|
|
779
1057
|
// Ephemeral reasoning: light-white while working, removed on response.
|
|
780
1058
|
if (it.kind === 'thinking')
|
|
781
|
-
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);
|
|
782
1060
|
if (it.kind === 'error')
|
|
783
|
-
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);
|
|
784
1062
|
if (it.kind === 'policy')
|
|
785
1063
|
return null;
|
|
786
1064
|
if (it.kind === 'file_changed')
|
|
787
|
-
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);
|
|
788
1066
|
if (it.kind === 'diff')
|
|
789
|
-
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));
|
|
790
1068
|
return null;
|
|
791
|
-
}) : 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 ? (_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 && pendingNew
|
|
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' ? ' ●' : ''] })] })] }));
|
|
792
1070
|
}
|