klyro 1.0.4 → 1.0.6
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/custom-agents.d.ts +3 -0
- package/dist/agent/custom-agents.js +96 -0
- package/dist/agent/orchestrator.d.ts +22 -2
- package/dist/agent/orchestrator.js +30 -4
- package/dist/agent/runtime.d.ts +5 -0
- package/dist/agent/runtime.js +174 -51
- package/dist/checkpoints/store.d.ts +9 -0
- package/dist/checkpoints/store.js +20 -0
- package/dist/cli/auth.js +11 -3
- package/dist/cli/completion.js +63 -10
- package/dist/cli/config.d.ts +4 -4
- package/dist/cli/doctor.js +13 -0
- package/dist/cli/eval.d.ts +15 -1
- package/dist/cli/eval.js +34 -2
- package/dist/cli/hooks.d.ts +54 -5
- package/dist/cli/hooks.js +85 -6
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.js +60 -0
- package/dist/cli/repl.js +261 -34
- package/dist/cli/run.d.ts +7 -1
- package/dist/cli/run.js +97 -41
- package/dist/cli/slash/custom.d.ts +25 -0
- package/dist/cli/slash/custom.js +166 -0
- package/dist/cli/slash/parser.d.ts +16 -2
- package/dist/cli/slash/parser.js +67 -18
- 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/context/memory.js +18 -1
- package/dist/eval/harness.d.ts +21 -3
- package/dist/eval/harness.js +31 -3
- package/dist/eval/judge.d.ts +32 -0
- package/dist/eval/judge.js +63 -0
- package/dist/eval/tasks.js +134 -0
- package/dist/index.js +225 -132
- package/dist/mcp/client.d.ts +15 -0
- package/dist/mcp/client.js +42 -2
- package/dist/mcp/config.d.ts +10 -1
- package/dist/mcp/config.js +64 -1
- package/dist/mcp/registry.d.ts +13 -0
- package/dist/mcp/registry.js +47 -5
- package/dist/mcp/remote.d.ts +29 -0
- package/dist/mcp/remote.js +153 -0
- package/dist/mcp/serve.d.ts +23 -0
- package/dist/mcp/serve.js +111 -0
- package/dist/policy/approval.d.ts +15 -1
- package/dist/policy/approval.js +8 -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/providers/endpoints.d.ts +43 -0
- package/dist/providers/endpoints.js +104 -0
- package/dist/providers.js +13 -10
- 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 +454 -53
- package/dist/tui/app.test.js +66 -3
- package/dist/tui/approval.js +53 -1
- 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
|
@@ -9,10 +9,12 @@ import Spinner from 'ink-spinner';
|
|
|
9
9
|
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
|
+
import { loadCustomCommands, listCompletableFiles } from '../cli/slash/custom.js';
|
|
12
13
|
import { tokens, g } from './tokens.js';
|
|
13
14
|
import { initialScroll, scrollReducer, resolveTopRow, maxTopFor, badgeLabel, } from './scroll-model.js';
|
|
14
15
|
import { buildIndex, itemAtRow, MeasureCache } from './measure.js';
|
|
15
16
|
import { renderMarkdownLines } from './markdown.js';
|
|
17
|
+
import { redact } from '../policy/secret-redactor.js';
|
|
16
18
|
import { readVersion } from '../version.js';
|
|
17
19
|
import { getTranscriptCommand, } from './transcript-commands.js';
|
|
18
20
|
import { estimateCost, getModelInfo } from '../providers/model-info.js';
|
|
@@ -85,6 +87,24 @@ function groupTools(items) {
|
|
|
85
87
|
flush();
|
|
86
88
|
return out;
|
|
87
89
|
}
|
|
90
|
+
// Input line with an explicit block cursor for vim normal mode.
|
|
91
|
+
// Same single-<Text> invariant as MarkdownText: nested inline parts only,
|
|
92
|
+
// so the row-direction parent never lays siblings out as columns.
|
|
93
|
+
function InputWithCursor({ input, cursor }) {
|
|
94
|
+
const dimColor = tokens.colors.dim;
|
|
95
|
+
if (!input) {
|
|
96
|
+
return (_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: dimColor, children: "Message Klyro..." }), "|"] }));
|
|
97
|
+
}
|
|
98
|
+
const safe = redact(input);
|
|
99
|
+
if (cursor === null) {
|
|
100
|
+
return _jsxs(Text, { wrap: "wrap", children: [safe, "|"] });
|
|
101
|
+
}
|
|
102
|
+
const pos = Math.max(0, Math.min(cursor, safe.length));
|
|
103
|
+
const before = safe.slice(0, pos);
|
|
104
|
+
const ch = safe.slice(pos, pos + 1) || ' ';
|
|
105
|
+
const after = safe.slice(pos + 1);
|
|
106
|
+
return (_jsxs(Text, { wrap: "wrap", children: [before, _jsx(Text, { inverse: true, children: ch }), after] }));
|
|
107
|
+
}
|
|
88
108
|
// design.md §23/§24 — terminal Markdown via tui/markdown.ts: headings,
|
|
89
109
|
// **bold**, *italic*, `code`, fences, links, lists. Ink wraps the text.
|
|
90
110
|
//
|
|
@@ -93,7 +113,9 @@ function groupTools(items) {
|
|
|
93
113
|
// out as side-by-side COLUMNS (garbled transcript) instead of lines.
|
|
94
114
|
function MarkdownText({ text, dim, width }) {
|
|
95
115
|
void width;
|
|
96
|
-
|
|
116
|
+
// Display boundary: scrub secret shapes before painting the screen.
|
|
117
|
+
// Execution data is untouched — this only affects rendered output.
|
|
118
|
+
const lines = useMemo(() => renderMarkdownLines(redact(text)), [text]);
|
|
97
119
|
const dimColor = tokens.colors.dim;
|
|
98
120
|
const softColor = tokens.colors.soft;
|
|
99
121
|
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 +249,64 @@ export function App(props) {
|
|
|
227
249
|
const t = setTimeout(toBottom, 0);
|
|
228
250
|
return () => clearTimeout(t);
|
|
229
251
|
}, [submitKey]);
|
|
252
|
+
// Double-Esc convention: the first Esc while running only arms (with a
|
|
253
|
+
// status hint); a second Esc within 1500ms cancels the stream. Single-Esc
|
|
254
|
+
// immediate cancel was too easy to hit by accident next to Shift+Enter.
|
|
255
|
+
const escArmedAt = useRef(0);
|
|
256
|
+
const [escArmed, setEscArmed] = useState(false);
|
|
257
|
+
const escArmTimer = useRef(null);
|
|
258
|
+
const disarmEsc = useCallback(() => {
|
|
259
|
+
escArmedAt.current = 0;
|
|
260
|
+
setEscArmed(false);
|
|
261
|
+
if (escArmTimer.current) {
|
|
262
|
+
clearTimeout(escArmTimer.current);
|
|
263
|
+
escArmTimer.current = null;
|
|
264
|
+
}
|
|
265
|
+
}, []);
|
|
266
|
+
// Minimal vim input mode (/vim toggles live via directHooks.setVimMode).
|
|
267
|
+
// Normal mode: h/l/0/$ move, x deletes, i/a enter insert, j/k scroll a
|
|
268
|
+
// line; every other printable key is swallowed (never typed). The cursor
|
|
269
|
+
// is an explicit index (null = end); rendering clamps it.
|
|
270
|
+
const [vimMode, setVimMode] = useState('insert');
|
|
271
|
+
const [vimCursor, setVimCursor] = useState(null);
|
|
272
|
+
// Count prefix (3h) and pending operator (d for dd) — refs, not state:
|
|
273
|
+
// they never render on their own, only through the next command.
|
|
274
|
+
const vimCountRef = useRef(0);
|
|
275
|
+
const vimPendingOp = useRef(null);
|
|
276
|
+
const setVimModeLive = useCallback((m) => {
|
|
277
|
+
setVimMode(m);
|
|
278
|
+
vimCountRef.current = 0;
|
|
279
|
+
vimPendingOp.current = null;
|
|
280
|
+
if (m === 'insert')
|
|
281
|
+
setVimCursor(null);
|
|
282
|
+
}, []);
|
|
283
|
+
// Cursor-aware editing: mirrors for synchronous use inside key handlers.
|
|
284
|
+
const inputRef = useRef('');
|
|
285
|
+
const vimCursorRef = useRef(null);
|
|
286
|
+
useEffect(() => { inputRef.current = input; }, [input]);
|
|
287
|
+
useEffect(() => { vimCursorRef.current = vimCursor; }, [vimCursor]);
|
|
288
|
+
const clampCursor = (v, c) => c === null ? v.length : Math.max(0, Math.min(c, v.length));
|
|
289
|
+
const insertAtCursor = useCallback((text) => {
|
|
290
|
+
const t = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
291
|
+
if (!t)
|
|
292
|
+
return;
|
|
293
|
+
const v = inputRef.current;
|
|
294
|
+
const pos = clampCursor(v, vimCursorRef.current);
|
|
295
|
+
const nv = v.slice(0, pos) + t + v.slice(pos);
|
|
296
|
+
setInput(nv);
|
|
297
|
+
setVimCursor(pos + t.length >= nv.length ? null : pos + t.length);
|
|
298
|
+
setHistIdx(null);
|
|
299
|
+
}, []);
|
|
300
|
+
const deleteBeforeCursor = useCallback(() => {
|
|
301
|
+
const v = inputRef.current;
|
|
302
|
+
const pos = clampCursor(v, vimCursorRef.current);
|
|
303
|
+
if (pos === 0)
|
|
304
|
+
return;
|
|
305
|
+
const nv = v.slice(0, pos - 1) + v.slice(pos);
|
|
306
|
+
setInput(nv);
|
|
307
|
+
setVimCursor(pos - 1 >= nv.length ? null : pos - 1);
|
|
308
|
+
setHistIdx(null);
|
|
309
|
+
}, []);
|
|
230
310
|
// Shift+Enter intent: explicit shift+return, kitty/CSI-u sequence, legacy
|
|
231
311
|
// ESC+CR pair, or Esc immediately followed by Return (75ms, non-empty input).
|
|
232
312
|
const escReturnAt = useRef(0);
|
|
@@ -239,6 +319,88 @@ export function App(props) {
|
|
|
239
319
|
top: () => { },
|
|
240
320
|
reset: () => { },
|
|
241
321
|
});
|
|
322
|
+
// Streaming render throttle: per-token setState copies the whole
|
|
323
|
+
// transcript array AND the growing string (O(n²) over long streams).
|
|
324
|
+
// Deltas accumulate in refs (the truth) and flush to state at most
|
|
325
|
+
// every STREAM_FLUSH_MS, plus on turn end / new items. Renders stay
|
|
326
|
+
// live (≤64ms stale) while array+string copies stay bounded.
|
|
327
|
+
// Declared early: queued-input and key handlers below call resetStream.
|
|
328
|
+
const STREAM_FLUSH_MS = 64;
|
|
329
|
+
const streamTextRef = useRef('');
|
|
330
|
+
const streamFlushAt = useRef(0);
|
|
331
|
+
const streamTimer = useRef(null);
|
|
332
|
+
const thinkingTextRef = useRef('');
|
|
333
|
+
const thinkingFlushAt = useRef(0);
|
|
334
|
+
const thinkingTimer = useRef(null);
|
|
335
|
+
// Declared before the flush callbacks that capture it.
|
|
336
|
+
const thinkingIdRef = useRef(null);
|
|
337
|
+
const flushStream = useCallback(() => {
|
|
338
|
+
const sid = streamingIdRef.current;
|
|
339
|
+
if (streamTimer.current) {
|
|
340
|
+
clearTimeout(streamTimer.current);
|
|
341
|
+
streamTimer.current = null;
|
|
342
|
+
}
|
|
343
|
+
if (!sid)
|
|
344
|
+
return;
|
|
345
|
+
const text = streamTextRef.current;
|
|
346
|
+
streamFlushAt.current = Date.now();
|
|
347
|
+
setTranscript((prev) => {
|
|
348
|
+
const idx = prev.findIndex((x) => x.id === sid);
|
|
349
|
+
if (idx === -1)
|
|
350
|
+
return [...prev, { id: sid, kind: 'text', text, role: 'assistant' }];
|
|
351
|
+
const cur = prev[idx];
|
|
352
|
+
if (cur.text === text)
|
|
353
|
+
return prev;
|
|
354
|
+
const copy = [...prev];
|
|
355
|
+
copy[idx] = { ...cur, text };
|
|
356
|
+
return copy;
|
|
357
|
+
});
|
|
358
|
+
}, []);
|
|
359
|
+
const flushThinking = useCallback(() => {
|
|
360
|
+
const tid = thinkingIdRef.current;
|
|
361
|
+
if (thinkingTimer.current) {
|
|
362
|
+
clearTimeout(thinkingTimer.current);
|
|
363
|
+
thinkingTimer.current = null;
|
|
364
|
+
}
|
|
365
|
+
if (!tid)
|
|
366
|
+
return;
|
|
367
|
+
const text = thinkingTextRef.current;
|
|
368
|
+
thinkingFlushAt.current = Date.now();
|
|
369
|
+
setTranscript((prev) => {
|
|
370
|
+
const idx = prev.findIndex((x) => x.id === tid);
|
|
371
|
+
if (idx === -1)
|
|
372
|
+
return [...prev, { id: tid, kind: 'thinking', text }];
|
|
373
|
+
const cur = prev[idx];
|
|
374
|
+
if (cur.text === text)
|
|
375
|
+
return prev;
|
|
376
|
+
const copy = [...prev];
|
|
377
|
+
copy[idx] = { ...cur, text };
|
|
378
|
+
return copy;
|
|
379
|
+
});
|
|
380
|
+
}, []);
|
|
381
|
+
const resetStream = useCallback(() => {
|
|
382
|
+
streamingIdRef.current = null;
|
|
383
|
+
streamTextRef.current = '';
|
|
384
|
+
if (streamTimer.current) {
|
|
385
|
+
clearTimeout(streamTimer.current);
|
|
386
|
+
streamTimer.current = null;
|
|
387
|
+
}
|
|
388
|
+
}, []);
|
|
389
|
+
const resetThinking = useCallback(() => {
|
|
390
|
+
thinkingIdRef.current = null;
|
|
391
|
+
thinkingTextRef.current = '';
|
|
392
|
+
if (thinkingTimer.current) {
|
|
393
|
+
clearTimeout(thinkingTimer.current);
|
|
394
|
+
thinkingTimer.current = null;
|
|
395
|
+
}
|
|
396
|
+
}, []);
|
|
397
|
+
// Flush pending tail before unmount so no trailing delta is lost.
|
|
398
|
+
useEffect(() => () => {
|
|
399
|
+
if (streamTimer.current)
|
|
400
|
+
clearTimeout(streamTimer.current);
|
|
401
|
+
if (thinkingTimer.current)
|
|
402
|
+
clearTimeout(thinkingTimer.current);
|
|
403
|
+
}, []);
|
|
242
404
|
const width = stdout?.columns ?? 100;
|
|
243
405
|
const height = stdout?.rows ?? 30;
|
|
244
406
|
const isFullscreen = props.isFullscreen ?? false;
|
|
@@ -394,60 +556,74 @@ export function App(props) {
|
|
|
394
556
|
const toSend = queuedInputs[0];
|
|
395
557
|
setQueuedInputs((prev) => prev.slice(1));
|
|
396
558
|
setTranscript((prev) => [...prev, { id: nextId('user'), kind: 'text', text: toSend, role: 'user' }]);
|
|
397
|
-
|
|
559
|
+
resetStream();
|
|
398
560
|
const cmd = parseSlash(toSend.trim());
|
|
399
561
|
if (cmd.kind === 'prompt')
|
|
400
562
|
void props.onPrompt(cmd.text);
|
|
401
563
|
else
|
|
402
564
|
void props.onSlash(cmd);
|
|
403
565
|
}
|
|
404
|
-
}, [queuedInputs, status.status, awaitingApproval]);
|
|
566
|
+
}, [queuedInputs, status.status, awaitingApproval, resetStream]);
|
|
405
567
|
useEffect(() => { if (status.status !== 'running')
|
|
406
568
|
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
|
-
|
|
569
|
+
const append = useCallback((item) => {
|
|
570
|
+
if (item.kind !== 'text' || item.role !== 'assistant') {
|
|
571
|
+
flushStream();
|
|
572
|
+
resetStream();
|
|
573
|
+
}
|
|
574
|
+
setTranscript((prev) => [...prev, item]);
|
|
575
|
+
}, [flushStream, resetStream]);
|
|
409
576
|
const appendDelta = useCallback((text) => {
|
|
410
577
|
if (!text)
|
|
411
578
|
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 {
|
|
579
|
+
if (!streamingIdRef.current) {
|
|
417
580
|
const id = nextId('stream');
|
|
418
581
|
streamingIdRef.current = id;
|
|
419
|
-
|
|
582
|
+
streamTextRef.current = '';
|
|
583
|
+
streamFlushAt.current = 0;
|
|
420
584
|
}
|
|
421
|
-
|
|
585
|
+
streamTextRef.current += text;
|
|
586
|
+
const now = Date.now();
|
|
587
|
+
if (now - streamFlushAt.current >= STREAM_FLUSH_MS)
|
|
588
|
+
flushStream();
|
|
589
|
+
else if (!streamTimer.current)
|
|
590
|
+
streamTimer.current = setTimeout(flushStream, STREAM_FLUSH_MS - (now - streamFlushAt.current));
|
|
591
|
+
}, [flushStream]);
|
|
422
592
|
// Ephemeral reasoning display: merges into one transient item (never in
|
|
423
593
|
// context/persistence); removed when the turn's answer completes.
|
|
424
|
-
const thinkingIdRef = useRef(null);
|
|
425
594
|
const appendThinkingDelta = useCallback((text) => {
|
|
426
595
|
if (!text)
|
|
427
596
|
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 {
|
|
597
|
+
if (!thinkingIdRef.current) {
|
|
433
598
|
const id = nextId('thinking');
|
|
434
599
|
thinkingIdRef.current = id;
|
|
435
|
-
|
|
600
|
+
thinkingTextRef.current = '';
|
|
601
|
+
thinkingFlushAt.current = 0;
|
|
436
602
|
}
|
|
437
|
-
|
|
603
|
+
thinkingTextRef.current += text;
|
|
604
|
+
const now = Date.now();
|
|
605
|
+
if (now - thinkingFlushAt.current >= STREAM_FLUSH_MS)
|
|
606
|
+
flushThinking();
|
|
607
|
+
else if (!thinkingTimer.current)
|
|
608
|
+
thinkingTimer.current = setTimeout(flushThinking, STREAM_FLUSH_MS - (now - thinkingFlushAt.current));
|
|
609
|
+
}, [flushThinking]);
|
|
438
610
|
const clearThinking = useCallback(() => {
|
|
439
|
-
|
|
611
|
+
resetThinking();
|
|
440
612
|
setTranscript((prev) => (prev.some((x) => x.kind === 'thinking') ? prev.filter((x) => x.kind !== 'thinking') : prev));
|
|
441
|
-
}, []);
|
|
613
|
+
}, [resetThinking]);
|
|
442
614
|
useEffect(() => {
|
|
443
615
|
if (status.status !== 'running') {
|
|
444
|
-
|
|
445
|
-
|
|
616
|
+
// Turn end: flush any throttled tail first so the final text is
|
|
617
|
+
// complete, then drop the transient thinking block as before.
|
|
618
|
+
flushStream();
|
|
619
|
+
resetStream();
|
|
620
|
+
resetThinking();
|
|
621
|
+
disarmEsc();
|
|
446
622
|
// Runs that end without final_text (abort/cancel/error) must not leave
|
|
447
623
|
// stale thinking blocks behind — only the response may remain.
|
|
448
624
|
setTranscript((prev) => (prev.some((x) => x.kind === 'thinking') ? prev.filter((x) => x.kind !== 'thinking') : prev));
|
|
449
625
|
}
|
|
450
|
-
}, [status.status]);
|
|
626
|
+
}, [status.status, flushStream, resetStream, resetThinking, disarmEsc]);
|
|
451
627
|
const updateStatus = useCallback((s) => setStatus((p) => ({ ...p, ...s })), []);
|
|
452
628
|
const updatePlan = useCallback((p) => setPlan(p), []);
|
|
453
629
|
// Tool results patch the running start-item IN PLACE (no second item, so a
|
|
@@ -465,14 +641,14 @@ export function App(props) {
|
|
|
465
641
|
});
|
|
466
642
|
}, []);
|
|
467
643
|
const clearTranscript = useCallback(() => {
|
|
468
|
-
|
|
469
|
-
|
|
644
|
+
resetStream();
|
|
645
|
+
resetThinking();
|
|
470
646
|
setTranscript([]);
|
|
471
647
|
setPlan([]);
|
|
472
648
|
// Fresh content → fresh scroll (a pruned anchor would otherwise stick to
|
|
473
649
|
// the bottom with a stale newSinceUnstick badge count).
|
|
474
650
|
scrollCmdsRef.current.reset();
|
|
475
|
-
}, []);
|
|
651
|
+
}, [resetStream, resetThinking]);
|
|
476
652
|
// Scroll control for external drivers (mouse-wheel tap in repl.ts, §8.4).
|
|
477
653
|
// Stored in refs so the callbacks stay stable while acting on latest state.
|
|
478
654
|
const scrollLines = useCallback((delta) => { scrollCmdsRef.current.line(delta); }, []);
|
|
@@ -500,24 +676,60 @@ export function App(props) {
|
|
|
500
676
|
}), []);
|
|
501
677
|
const onMountedRef = useRef(props.onMounted);
|
|
502
678
|
useEffect(() => { onMountedRef.current = props.onMounted; }, [props.onMounted]);
|
|
503
|
-
|
|
679
|
+
// Bracketed-paste bulk insert: one atomic input append (no per-char
|
|
680
|
+
// autocomplete/history side effects) with newline normalization.
|
|
681
|
+
const pasteText = useCallback((text) => {
|
|
682
|
+
insertAtCursor(text);
|
|
683
|
+
}, [insertAtCursor]);
|
|
684
|
+
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
685
|
const toggleGroup = (id) => setExpandedGroups((prev) => { const n = new Set(prev); if (n.has(id))
|
|
505
686
|
n.delete(id);
|
|
506
687
|
else
|
|
507
688
|
n.add(id); return n; });
|
|
508
|
-
// `/` autocomplete — top 6
|
|
689
|
+
// `/` autocomplete — fuzzy top 6 over builtins + custom command files
|
|
690
|
+
// while completing a command name (no space yet)
|
|
691
|
+
const customCmds = useMemo(() => {
|
|
692
|
+
try {
|
|
693
|
+
return loadCustomCommands(props.cwd).map((c) => ({ name: c.name, hint: c.description || 'custom command' }));
|
|
694
|
+
}
|
|
695
|
+
catch {
|
|
696
|
+
return [];
|
|
697
|
+
}
|
|
698
|
+
}, [props.cwd]);
|
|
509
699
|
const slashSuggest = input.startsWith('/') && !input.slice(1).includes(' ') && !awaitingApproval
|
|
510
|
-
? suggestCommands(input, 6)
|
|
700
|
+
? suggestCommands(input, 6, customCmds)
|
|
701
|
+
: [];
|
|
702
|
+
// `@` file completion — fuzzy over the workspace file index while typing
|
|
703
|
+
// a mention token (no space yet). Directories carry trailing `/`.
|
|
704
|
+
const fileIndex = useMemo(() => {
|
|
705
|
+
try {
|
|
706
|
+
return listCompletableFiles(props.cwd);
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
return [];
|
|
710
|
+
}
|
|
711
|
+
}, [props.cwd]);
|
|
712
|
+
const atToken = /^@(\S*)$/.exec(input);
|
|
713
|
+
const pathSuggest = atToken && !awaitingApproval
|
|
714
|
+
? suggestCommands(`/${atToken[1] ?? ''}`, 6, fileIndex.map((f) => ({ name: f, hint: 'file' }))).map((d) => ({ ...d, name: `@${d.name}` }))
|
|
511
715
|
: [];
|
|
512
716
|
useInput((inputStr, key) => {
|
|
513
717
|
if (key.escape && queuedInputs.length > 0) {
|
|
514
718
|
setQueuedInputs((prev) => prev.slice(1));
|
|
719
|
+
disarmEsc();
|
|
515
720
|
return;
|
|
516
721
|
}
|
|
517
|
-
// Tab completes the top slash
|
|
722
|
+
// Tab completes the top slash or @-path suggestion
|
|
518
723
|
if ((key.tab || inputStr === '\t') && slashSuggest.length > 0) {
|
|
519
724
|
const top = slashSuggest[0];
|
|
520
725
|
setInput(`/${top.name} `);
|
|
726
|
+
setVimCursor(null);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if ((key.tab || inputStr === '\t') && pathSuggest.length > 0) {
|
|
730
|
+
const top = pathSuggest[0];
|
|
731
|
+
setInput(`${top.name} `);
|
|
732
|
+
setVimCursor(null);
|
|
521
733
|
return;
|
|
522
734
|
}
|
|
523
735
|
// Scroll keys (work in any mode, including while running).
|
|
@@ -581,10 +793,171 @@ export function App(props) {
|
|
|
581
793
|
inputStr === '\x1b\r' ||
|
|
582
794
|
inputStr === '\x1b[13;2u' ||
|
|
583
795
|
(key.return && input.trim() !== '' && now - escReturnAt.current < 75)) {
|
|
584
|
-
|
|
796
|
+
insertAtCursor('\n');
|
|
585
797
|
setHistIdx(null);
|
|
586
798
|
return;
|
|
587
799
|
}
|
|
800
|
+
// Vim normal mode: motion/editing keys act on the input buffer instead
|
|
801
|
+
// of typing. Return/arrows/tab/ctrl fall through to the shared logic
|
|
802
|
+
// below; every other printable key is swallowed. Supports counts
|
|
803
|
+
// (3h/2x), word motions (w/b), line ops (dd/D) — no operators/visual.
|
|
804
|
+
if (vimMode === 'normal' && !awaitingApproval) {
|
|
805
|
+
// Legacy Shift+Enter partner: an Esc that just switched modes followed
|
|
806
|
+
// by Return within 75ms still means newline, not submit.
|
|
807
|
+
if (key.return && input.trim() !== '' && Date.now() - escReturnAt.current < 75) {
|
|
808
|
+
insertAtCursor('\n');
|
|
809
|
+
setVimMode('insert');
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
const cur = vimCursor === null ? input.length : Math.max(0, Math.min(vimCursor, input.length));
|
|
813
|
+
const move = (d) => { setVimCursor(Math.max(0, Math.min(cur + d, input.length))); return true; };
|
|
814
|
+
const isWord = (c) => /[A-Za-z0-9_]/.test(c);
|
|
815
|
+
const wordFwd = (pos, n) => {
|
|
816
|
+
let p = pos;
|
|
817
|
+
for (let k = 0; k < n && p < input.length; k++) {
|
|
818
|
+
while (p < input.length && isWord(input[p]))
|
|
819
|
+
p++;
|
|
820
|
+
while (p < input.length && !isWord(input[p]))
|
|
821
|
+
p++;
|
|
822
|
+
}
|
|
823
|
+
return Math.min(p, input.length);
|
|
824
|
+
};
|
|
825
|
+
const wordBack = (pos, n) => {
|
|
826
|
+
let p = pos;
|
|
827
|
+
for (let k = 0; k < n && p > 0; k++) {
|
|
828
|
+
while (p > 0 && !isWord(input[p - 1]))
|
|
829
|
+
p--;
|
|
830
|
+
while (p > 0 && isWord(input[p - 1]))
|
|
831
|
+
p--;
|
|
832
|
+
}
|
|
833
|
+
return Math.max(0, p);
|
|
834
|
+
};
|
|
835
|
+
const takeCount = () => {
|
|
836
|
+
const n = vimCountRef.current <= 1 ? 1 : vimCountRef.current;
|
|
837
|
+
vimCountRef.current = 0;
|
|
838
|
+
return n;
|
|
839
|
+
};
|
|
840
|
+
if (!key.ctrl && !key.meta && !key.return && !key.tab && !key.upArrow && !key.downArrow && !key.leftArrow && !key.rightArrow) {
|
|
841
|
+
// Pending `d` operator: second `d` deletes the current line.
|
|
842
|
+
if (vimPendingOp.current === 'd') {
|
|
843
|
+
vimPendingOp.current = null;
|
|
844
|
+
if (inputStr === 'd') {
|
|
845
|
+
const start = input.lastIndexOf('\n', cur - 1) + 1;
|
|
846
|
+
const nl = input.indexOf('\n', cur);
|
|
847
|
+
const end = nl === -1 ? input.length : nl + 1;
|
|
848
|
+
setInput(input.slice(0, start) + input.slice(end));
|
|
849
|
+
setVimCursor(Math.min(start, Math.max(0, input.slice(0, start).length)));
|
|
850
|
+
setHistIdx(null);
|
|
851
|
+
vimCountRef.current = 0;
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
// Only dd is supported — anything else cancels the operator.
|
|
855
|
+
vimCountRef.current = 0;
|
|
856
|
+
}
|
|
857
|
+
if (inputStr === 'i') {
|
|
858
|
+
setVimMode('insert');
|
|
859
|
+
setVimCursor(null);
|
|
860
|
+
vimCountRef.current = 0;
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if (inputStr === 'a') {
|
|
864
|
+
setVimCursor(Math.min(cur + 1, input.length));
|
|
865
|
+
setVimMode('insert');
|
|
866
|
+
vimCountRef.current = 0;
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (/^[1-9]$/.test(inputStr)) {
|
|
870
|
+
vimCountRef.current = vimCountRef.current * 10 + Number(inputStr);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (inputStr === 'h') {
|
|
874
|
+
const n = takeCount();
|
|
875
|
+
move(-n);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (inputStr === 'l') {
|
|
879
|
+
const n = takeCount();
|
|
880
|
+
move(n);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (inputStr === '0' && vimCountRef.current === 0) {
|
|
884
|
+
setVimCursor(0);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (inputStr === '0') {
|
|
888
|
+
const n = takeCount();
|
|
889
|
+
move(-n);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (inputStr === '$') {
|
|
893
|
+
takeCount();
|
|
894
|
+
setVimCursor(input.length);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (inputStr === 'x') {
|
|
898
|
+
const n = takeCount();
|
|
899
|
+
if (cur < input.length)
|
|
900
|
+
setInput(input.slice(0, cur) + input.slice(cur + n));
|
|
901
|
+
setHistIdx(null);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
if (inputStr === 'w') {
|
|
905
|
+
const n = takeCount();
|
|
906
|
+
setVimCursor(wordFwd(cur, n));
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (inputStr === 'b') {
|
|
910
|
+
const n = takeCount();
|
|
911
|
+
setVimCursor(wordBack(cur, n));
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
if (inputStr === 'd') {
|
|
915
|
+
vimPendingOp.current = 'd';
|
|
916
|
+
vimCountRef.current = 0;
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (inputStr === 'D') {
|
|
920
|
+
// Delete to end of current line (keep the newline).
|
|
921
|
+
const nl = input.indexOf('\n', cur);
|
|
922
|
+
const end = nl === -1 ? input.length : nl;
|
|
923
|
+
setInput(input.slice(0, cur) + input.slice(end));
|
|
924
|
+
setHistIdx(null);
|
|
925
|
+
vimCountRef.current = 0;
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (inputStr === 'j') {
|
|
929
|
+
if (isFullscreen && maxTop > 0)
|
|
930
|
+
commands.lineDown();
|
|
931
|
+
vimCountRef.current = 0;
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (inputStr === 'k') {
|
|
935
|
+
if (isFullscreen && maxTop > 0)
|
|
936
|
+
commands.lineUp();
|
|
937
|
+
vimCountRef.current = 0;
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
if (key.backspace || key.delete) {
|
|
941
|
+
move(-1);
|
|
942
|
+
vimCountRef.current = 0;
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (key.escape) {
|
|
946
|
+
vimCountRef.current = 0;
|
|
947
|
+
vimPendingOp.current = null;
|
|
948
|
+
return;
|
|
949
|
+
} // already normal
|
|
950
|
+
if (inputStr) {
|
|
951
|
+
vimCountRef.current = 0;
|
|
952
|
+
vimPendingOp.current = null;
|
|
953
|
+
return;
|
|
954
|
+
} // swallow everything else printable
|
|
955
|
+
}
|
|
956
|
+
else if (key.backspace || key.delete) {
|
|
957
|
+
move(-1);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
588
961
|
if (status.status === 'running') {
|
|
589
962
|
// design.md §18: first Ctrl+C cancels the run, second quits.
|
|
590
963
|
if (key.ctrl && inputStr === 'c') {
|
|
@@ -597,9 +970,26 @@ export function App(props) {
|
|
|
597
970
|
}
|
|
598
971
|
return;
|
|
599
972
|
}
|
|
600
|
-
// Esc
|
|
973
|
+
// Esc no longer cancels on first press: with a queued item it drops
|
|
974
|
+
// one; otherwise the first Esc arms and the second (≤1500ms) cancels.
|
|
601
975
|
if (key.escape) {
|
|
602
|
-
|
|
976
|
+
if (queuedInputs.length > 0) {
|
|
977
|
+
setQueuedInputs((prev) => prev.slice(1));
|
|
978
|
+
disarmEsc();
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const t = Date.now();
|
|
982
|
+
if (t - escArmedAt.current < 1500) {
|
|
983
|
+
disarmEsc();
|
|
984
|
+
void props.onSlash({ kind: 'cancel' });
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
escArmedAt.current = t;
|
|
988
|
+
setEscArmed(true);
|
|
989
|
+
if (escArmTimer.current)
|
|
990
|
+
clearTimeout(escArmTimer.current);
|
|
991
|
+
escArmTimer.current = setTimeout(disarmEsc, 1500);
|
|
992
|
+
}
|
|
603
993
|
return;
|
|
604
994
|
}
|
|
605
995
|
// Enter on empty input dismisses the badge (jump to bottom, §7.2)
|
|
@@ -614,16 +1004,17 @@ export function App(props) {
|
|
|
614
1004
|
return;
|
|
615
1005
|
setQueuedInputs((prev) => [...prev, v]);
|
|
616
1006
|
setInput('');
|
|
1007
|
+
setVimCursor(null);
|
|
617
1008
|
pushHistory(v);
|
|
618
1009
|
setSubmitKey((k) => k + 1);
|
|
619
1010
|
return;
|
|
620
1011
|
}
|
|
621
1012
|
if (key.backspace || key.delete) {
|
|
622
|
-
|
|
1013
|
+
deleteBeforeCursor();
|
|
623
1014
|
return;
|
|
624
1015
|
}
|
|
625
1016
|
if (!key.ctrl && !key.meta)
|
|
626
|
-
|
|
1017
|
+
insertAtCursor(inputStr);
|
|
627
1018
|
return;
|
|
628
1019
|
}
|
|
629
1020
|
// design.md §18: idle Ctrl+C exits.
|
|
@@ -639,6 +1030,7 @@ export function App(props) {
|
|
|
639
1030
|
const next = histIdx === null ? history.length - 1 : Math.max(0, histIdx - 1);
|
|
640
1031
|
setHistIdx(next);
|
|
641
1032
|
setInput(history[next] ?? '');
|
|
1033
|
+
setVimCursor(null);
|
|
642
1034
|
}
|
|
643
1035
|
return;
|
|
644
1036
|
}
|
|
@@ -658,6 +1050,7 @@ export function App(props) {
|
|
|
658
1050
|
setHistIdx(next);
|
|
659
1051
|
setInput(history[next] ?? '');
|
|
660
1052
|
}
|
|
1053
|
+
setVimCursor(null);
|
|
661
1054
|
return;
|
|
662
1055
|
}
|
|
663
1056
|
if (input.trim() !== '')
|
|
@@ -676,10 +1069,11 @@ export function App(props) {
|
|
|
676
1069
|
return;
|
|
677
1070
|
}
|
|
678
1071
|
setInput('');
|
|
1072
|
+
setVimCursor(null);
|
|
679
1073
|
setHistIdx(null);
|
|
680
1074
|
pushHistory(v);
|
|
681
1075
|
setTranscript((prev) => [...prev, { id: nextId('user'), kind: 'text', text: v, role: 'user' }]);
|
|
682
|
-
|
|
1076
|
+
resetStream();
|
|
683
1077
|
setSubmitKey((k) => k + 1);
|
|
684
1078
|
const cmd = parseSlash(v);
|
|
685
1079
|
if (cmd.kind === 'prompt')
|
|
@@ -689,13 +1083,17 @@ export function App(props) {
|
|
|
689
1083
|
return;
|
|
690
1084
|
}
|
|
691
1085
|
if (key.backspace || key.delete) {
|
|
692
|
-
|
|
693
|
-
setHistIdx(null);
|
|
1086
|
+
deleteBeforeCursor();
|
|
694
1087
|
return;
|
|
695
1088
|
}
|
|
696
1089
|
if (!key.ctrl && !key.meta) {
|
|
697
|
-
|
|
698
|
-
|
|
1090
|
+
insertAtCursor(inputStr);
|
|
1091
|
+
}
|
|
1092
|
+
// Idle Esc enters vim normal mode (vimLive parity for keyboard-only users).
|
|
1093
|
+
if (key.escape && vimMode === 'insert') {
|
|
1094
|
+
setVimMode('normal');
|
|
1095
|
+
setVimCursor(inputRef.current.length);
|
|
1096
|
+
return;
|
|
699
1097
|
}
|
|
700
1098
|
});
|
|
701
1099
|
// Single source of truth: package.json via version.ts — never hardcoded.
|
|
@@ -707,9 +1105,12 @@ export function App(props) {
|
|
|
707
1105
|
const ctxWindow = getModelInfo(status.model).contextWindow;
|
|
708
1106
|
const ctxPct = totalTokens > 0 ? Math.round((totalTokens / ctxWindow) * 100) : 0;
|
|
709
1107
|
// Narrow terminals: compact hints so the status bar never wraps mid-word.
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1108
|
+
// An armed first-Esc surfaces here so the operator knows the second cancels.
|
|
1109
|
+
const baseHints = escArmed && status.status === 'running'
|
|
1110
|
+
? 'press esc again to cancel'
|
|
1111
|
+
: width < 90
|
|
1112
|
+
? status.status === 'running' ? 'ctrl+c stop · enter queue' : 'enter send · / commands'
|
|
1113
|
+
: 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
1114
|
const hints = maxTop > 0 && isFullscreen ? `${baseHints} · PgUp/Dn scroll` : baseHints;
|
|
714
1115
|
// I1 structural guard: the frame can never exceed terminal rows. Even if a
|
|
715
1116
|
// child mis-measures, the root clips — the input/status stay on screen and
|
|
@@ -751,7 +1152,7 @@ export function App(props) {
|
|
|
751
1152
|
const marker = isExpanded ? g('expanded') : g('collapsed');
|
|
752
1153
|
const markerColor = gr.status === 'error' ? tokens.colors.err : gr.status === 'running' ? tokens.colors.warn : tokens.colors.ok;
|
|
753
1154
|
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) => {
|
|
1155
|
+
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
1156
|
let friendly = '';
|
|
756
1157
|
try {
|
|
757
1158
|
const a = JSON.parse(it.args);
|
|
@@ -769,12 +1170,12 @@ export function App(props) {
|
|
|
769
1170
|
catch {
|
|
770
1171
|
friendly = it.args.slice(0, 40);
|
|
771
1172
|
}
|
|
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));
|
|
1173
|
+
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
1174
|
}), 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
1175
|
}
|
|
775
1176
|
const it = item;
|
|
776
1177
|
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);
|
|
1178
|
+
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
1179
|
}
|
|
779
1180
|
if (it.kind === 'text') {
|
|
780
1181
|
// prose — render markdown, not raw **, with proper wrap and guide
|
|
@@ -782,15 +1183,15 @@ export function App(props) {
|
|
|
782
1183
|
}
|
|
783
1184
|
// Ephemeral reasoning: light-white while working, removed on response.
|
|
784
1185
|
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);
|
|
1186
|
+
return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsx(Text, { wrap: "wrap", color: tokens.colors.dim, children: redact(it.text) }) }, it.id);
|
|
786
1187
|
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);
|
|
1188
|
+
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
1189
|
if (it.kind === 'policy')
|
|
789
1190
|
return null;
|
|
790
1191
|
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);
|
|
1192
|
+
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
1193
|
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));
|
|
1194
|
+
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
1195
|
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'), " "] }),
|
|
1196
|
+
}) : 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, slashSuggest.length === 0 && pathSuggest.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, flexShrink: 0, children: [pathSuggest.map((s, i) => (_jsxs(Text, { color: i === 0 ? tokens.colors.accent : tokens.colors.dim, children: [i === 0 ? '▸' : ' ', " ", s.name] }, 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
1197
|
}
|