klyro 0.1.52 → 0.1.53
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/dist/cli/repl.js +34 -8
- package/dist/tui/app.d.ts +2 -1
- package/dist/tui/app.js +31 -5
- package/dist/tui/app.test.js +38 -0
- package/dist/tui/measure.d.ts +2 -0
- package/dist/tui/measure.js +1 -1
- package/dist/tui/transcript.d.ts +7 -0
- package/package.json +1 -1
package/dist/cli/repl.js
CHANGED
|
@@ -142,6 +142,16 @@ export async function startRepl(opts = {}) {
|
|
|
142
142
|
else
|
|
143
143
|
pendingQueue.push({ kind: 'delta', text });
|
|
144
144
|
}
|
|
145
|
+
// Tool results patch the running start-item in place (App.updateTool) so a
|
|
146
|
+
// group resolves to done/error with its real latency instead of ticking
|
|
147
|
+
// forever. Falls back to a standalone item if the start item is gone.
|
|
148
|
+
const pendingToolIds = new Set();
|
|
149
|
+
function queuedToolUpdate(idCall, patch) {
|
|
150
|
+
if (isMounted && directHooks)
|
|
151
|
+
directHooks.updateTool(idCall, patch);
|
|
152
|
+
else
|
|
153
|
+
pendingQueue.push({ kind: 'toolupdate', idCall, patch });
|
|
154
|
+
}
|
|
145
155
|
function queuedStatus(s) {
|
|
146
156
|
lastStatus = { ...(lastStatus ?? { model: model ?? '', step: 0, maxSteps: opts.maxSteps ?? 30, usageInput: 0, usageOutput: 0, repairs: 0, status: 'idle' }), ...s };
|
|
147
157
|
if (isMounted && directHooks)
|
|
@@ -393,6 +403,8 @@ export async function startRepl(opts = {}) {
|
|
|
393
403
|
hooks.updatePlan(ev.plan);
|
|
394
404
|
else if (ev.kind === 'delta')
|
|
395
405
|
hooks.appendDelta(ev.text);
|
|
406
|
+
else if (ev.kind === 'toolupdate')
|
|
407
|
+
hooks.updateTool(ev.idCall, ev.patch);
|
|
396
408
|
else
|
|
397
409
|
hooks.append(ev.item);
|
|
398
410
|
}
|
|
@@ -511,6 +523,7 @@ export async function startRepl(opts = {}) {
|
|
|
511
523
|
activeCallArgs += ev.argsJson;
|
|
512
524
|
}
|
|
513
525
|
else if (ev.kind === 'tool_call_end') {
|
|
526
|
+
pendingToolIds.add(ev.id);
|
|
514
527
|
queuedAppend({
|
|
515
528
|
id: `tool-${ev.id}-${Date.now()}`,
|
|
516
529
|
kind: 'tool',
|
|
@@ -533,17 +546,30 @@ export async function startRepl(opts = {}) {
|
|
|
533
546
|
});
|
|
534
547
|
}
|
|
535
548
|
else if (ev.kind === 'tool_result') {
|
|
536
|
-
|
|
537
|
-
id: `tres-${ev.id}-${Date.now()}`,
|
|
538
|
-
kind: 'tool',
|
|
539
|
-
name: ev.name,
|
|
540
|
-
id_call: ev.id,
|
|
541
|
-
args: '',
|
|
549
|
+
const patch = {
|
|
542
550
|
result: typeof ev.output === 'string' ? ev.output : JSON.stringify(ev.output, null, 2),
|
|
543
551
|
isError: ev.isError,
|
|
544
552
|
latencyMs: ev.latencyMs,
|
|
545
|
-
status: ev.isError ? 'error' : 'done',
|
|
546
|
-
}
|
|
553
|
+
status: (ev.isError ? 'error' : 'done'),
|
|
554
|
+
};
|
|
555
|
+
if (pendingToolIds.delete(ev.id)) {
|
|
556
|
+
// Patch the running start-item in place — no stale spinner.
|
|
557
|
+
queuedToolUpdate(ev.id, patch);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
// Start item gone (e.g. transcript cleared) — keep a record.
|
|
561
|
+
queuedAppend({
|
|
562
|
+
id: `tres-${ev.id}-${Date.now()}`,
|
|
563
|
+
kind: 'tool',
|
|
564
|
+
name: ev.name,
|
|
565
|
+
id_call: ev.id,
|
|
566
|
+
args: '',
|
|
567
|
+
result: patch.result,
|
|
568
|
+
isError: patch.isError,
|
|
569
|
+
latencyMs: patch.latencyMs,
|
|
570
|
+
status: patch.status,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
547
573
|
}
|
|
548
574
|
else if (ev.kind === 'final_text') {
|
|
549
575
|
// streamingId is closed by status change; no extra handling needed
|
package/dist/tui/app.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import React from 'react';
|
|
6
6
|
import type { StatusSnapshot } from './status.js';
|
|
7
|
-
import type { TranscriptItem } from './transcript.js';
|
|
7
|
+
import type { TranscriptItem, ToolResultPatch } from './transcript.js';
|
|
8
8
|
import { TuiApprovalBridge } from './approval.js';
|
|
9
9
|
import type { PlanStep } from '../agent/runtime.js';
|
|
10
10
|
import { type TranscriptScrollHandle } from './transcript-commands.js';
|
|
@@ -28,6 +28,7 @@ export interface AppProps {
|
|
|
28
28
|
scrollHalfPage: (dir: -1 | 1) => void;
|
|
29
29
|
scrollToTop: () => void;
|
|
30
30
|
transcript: TranscriptScrollHandle;
|
|
31
|
+
updateTool: (idCall: string, patch: ToolResultPatch) => void;
|
|
31
32
|
}) => void;
|
|
32
33
|
version?: string;
|
|
33
34
|
isFullscreen?: boolean;
|
package/dist/tui/app.js
CHANGED
|
@@ -34,7 +34,7 @@ function verbForTool(name) {
|
|
|
34
34
|
return 'Listed';
|
|
35
35
|
if (name === 'grep' || name === 'glob' || name === 'find_files' || name === 'search_files' || name === 'recent_files')
|
|
36
36
|
return 'Searched';
|
|
37
|
-
if (name === 'shell_exec')
|
|
37
|
+
if (name === 'shell_exec' || name === 'run_verify')
|
|
38
38
|
return 'Ran';
|
|
39
39
|
if (name.startsWith('git_'))
|
|
40
40
|
return 'Checked git';
|
|
@@ -238,7 +238,13 @@ export function App(props) {
|
|
|
238
238
|
const gr = entry;
|
|
239
239
|
out.push({
|
|
240
240
|
key: gr.id,
|
|
241
|
-
desc: {
|
|
241
|
+
desc: {
|
|
242
|
+
kind: 'group',
|
|
243
|
+
count: gr.items.length,
|
|
244
|
+
expanded: expandedGroups.has(gr.id),
|
|
245
|
+
status: gr.status,
|
|
246
|
+
resultLen: gr.items.reduce((s, x) => s + (x.result?.length ?? 0), 0),
|
|
247
|
+
},
|
|
242
248
|
groupIndex: gi,
|
|
243
249
|
tail: null,
|
|
244
250
|
});
|
|
@@ -389,6 +395,20 @@ export function App(props) {
|
|
|
389
395
|
streamingIdRef.current = null; }, [status.status]);
|
|
390
396
|
const updateStatus = useCallback((s) => setStatus((p) => ({ ...p, ...s })), []);
|
|
391
397
|
const updatePlan = useCallback((p) => setPlan(p), []);
|
|
398
|
+
// Tool results patch the running start-item IN PLACE (no second item, so a
|
|
399
|
+
// group never stays 'running' forever showing a ticking elapsed timer).
|
|
400
|
+
const updateTool = useCallback((idCall, patch) => {
|
|
401
|
+
setTranscript((prev) => {
|
|
402
|
+
const idx = prev.findIndex((x) => x.kind === 'tool' &&
|
|
403
|
+
x.id_call === idCall &&
|
|
404
|
+
x.status === 'running');
|
|
405
|
+
if (idx === -1)
|
|
406
|
+
return prev;
|
|
407
|
+
const copy = [...prev];
|
|
408
|
+
copy[idx] = { ...copy[idx], ...patch };
|
|
409
|
+
return copy;
|
|
410
|
+
});
|
|
411
|
+
}, []);
|
|
392
412
|
const clearTranscript = useCallback(() => { streamingIdRef.current = null; setTranscript([]); setPlan([]); }, []);
|
|
393
413
|
// Scroll control for external drivers (mouse-wheel tap in repl.ts, §8.4).
|
|
394
414
|
// Stored in refs so the callbacks stay stable while acting on latest state.
|
|
@@ -417,7 +437,7 @@ export function App(props) {
|
|
|
417
437
|
}), []);
|
|
418
438
|
const onMountedRef = useRef(props.onMounted);
|
|
419
439
|
useEffect(() => { onMountedRef.current = props.onMounted; }, [props.onMounted]);
|
|
420
|
-
useEffect(() => { onMountedRef.current?.({ append, appendDelta, updateStatus, updatePlan, clearTranscript, scrollLines, scrollToBottom, scrollHalfPage, scrollToTop, transcript: transcriptHandle }); globalThis.__klyroAppAppend = append; globalThis.__klyroAppendDelta = appendDelta; globalThis.__klyroAppStatus = updateStatus; globalThis.__klyroAppPlan = updatePlan; return () => { delete globalThis.__klyroAppAppend; delete globalThis.__klyroAppendDelta; delete globalThis.__klyroAppStatus; delete globalThis.__klyroAppPlan; }; }, [append, appendDelta, updateStatus, updatePlan, clearTranscript, scrollLines, scrollToBottom, scrollHalfPage, scrollToTop, transcriptHandle]);
|
|
440
|
+
useEffect(() => { onMountedRef.current?.({ append, appendDelta, updateStatus, updatePlan, clearTranscript, scrollLines, scrollToBottom, scrollHalfPage, scrollToTop, transcript: transcriptHandle, updateTool }); globalThis.__klyroAppAppend = append; globalThis.__klyroAppendDelta = appendDelta; globalThis.__klyroAppStatus = updateStatus; globalThis.__klyroAppPlan = updatePlan; return () => { delete globalThis.__klyroAppAppend; delete globalThis.__klyroAppendDelta; delete globalThis.__klyroAppStatus; delete globalThis.__klyroAppPlan; }; }, [append, appendDelta, updateStatus, updatePlan, clearTranscript, scrollLines, scrollToBottom, scrollHalfPage, scrollToTop, transcriptHandle, updateTool]);
|
|
421
441
|
const toggleGroup = (id) => setExpandedGroups((prev) => { const n = new Set(prev); if (n.has(id))
|
|
422
442
|
n.delete(id);
|
|
423
443
|
else
|
|
@@ -616,9 +636,15 @@ export function App(props) {
|
|
|
616
636
|
const cost = (status.usageInput / 1000 * 0.003 + status.usageOutput / 1000 * 0.015);
|
|
617
637
|
const totalTokens = status.usageInput + status.usageOutput;
|
|
618
638
|
const ctxPct = totalTokens > 0 ? Math.round((totalTokens / 120_000) * 100) : 0;
|
|
619
|
-
|
|
639
|
+
// Narrow terminals: compact hints so the status bar never wraps mid-word.
|
|
640
|
+
const baseHints = width < 90
|
|
641
|
+
? status.status === 'running' ? 'ctrl+c stop · enter queue' : 'enter send · / commands'
|
|
642
|
+
: 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';
|
|
620
643
|
const hints = maxTop > 0 && isFullscreen ? `${baseHints} · PgUp/Dn scroll` : baseHints;
|
|
621
|
-
|
|
644
|
+
// I1 structural guard: the frame can never exceed terminal rows. Even if a
|
|
645
|
+
// child mis-measures, the root clips — the input/status stay on screen and
|
|
646
|
+
// Ink's cursor math can't corrupt into overlapping text.
|
|
647
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: isFullscreen ? height - 1 : undefined, overflow: isFullscreen ? 'hidden' : undefined, children: [_jsx(Header, { cwd: props.cwd, model: status.model, version: ver, width: width }), _jsxs(Box, { flexDirection: "row", flexGrow: isFullscreen ? 1 : 0, overflow: isFullscreen ? 'hidden' : undefined, children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: isFullscreen ? 'hidden' : undefined, paddingX: 0, children: [tiny ? (_jsxs(Text, { color: tokens.colors.warn, children: ["\u26A0 terminal too small (", width, "x", height, ") \u2014 transcript hidden"] })) : null, !tiny && grouped.length === 0 ? (_jsx(Text, { color: tokens.colors.dim, children: "Message Klyro..." })) : !tiny ? visibleGrouped.map((item) => {
|
|
622
648
|
if (item.verb) {
|
|
623
649
|
const gr = item;
|
|
624
650
|
const isExpanded = expandedGroups.has(gr.id);
|
package/dist/tui/app.test.js
CHANGED
|
@@ -356,6 +356,44 @@ describe('App', () => {
|
|
|
356
356
|
await new Promise((r) => setTimeout(r, 50));
|
|
357
357
|
expect(lastFrame() ?? '').toMatch(/MSG-24-tag/);
|
|
358
358
|
});
|
|
359
|
+
it('tool result patches the running item in place (no stale spinner)', async () => {
|
|
360
|
+
let hooks = null;
|
|
361
|
+
const { lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, onMounted: (h) => {
|
|
362
|
+
hooks = { append: h.append, updateTool: h.updateTool };
|
|
363
|
+
} }));
|
|
364
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
365
|
+
expect(hooks).not.toBeNull();
|
|
366
|
+
hooks.append({ id: 't1', kind: 'tool', name: 'read_file', id_call: 'c1', args: '{"path":"a.ts"}', status: 'running' });
|
|
367
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
368
|
+
expect(lastFrame() ?? '').toMatch(/Read/);
|
|
369
|
+
hooks.updateTool('c1', { result: 'ok', isError: false, latencyMs: 42, status: 'done' });
|
|
370
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
371
|
+
const frame = lastFrame() ?? '';
|
|
372
|
+
// Resolved with real latency — exactly one group (start item patched, no duplicate).
|
|
373
|
+
expect(frame).toMatch(/42ms/);
|
|
374
|
+
expect(frame.match(/Read/g)?.length ?? 0).toBeLessThanOrEqual(2);
|
|
375
|
+
});
|
|
376
|
+
it('heavy transcript: frame bounded, input and tail visible', async () => {
|
|
377
|
+
const items = [];
|
|
378
|
+
for (let i = 0; i < 30; i++) {
|
|
379
|
+
items.push({ id: `u-${i}`, kind: 'text', text: `user message number ${i} asking about stuff`, role: 'user' });
|
|
380
|
+
items.push({
|
|
381
|
+
id: `a-${i}`,
|
|
382
|
+
kind: 'text',
|
|
383
|
+
text: `## Answer ${i}\n\nAssistant answer **number ${i}** with a long explanation that wraps across multiple terminal lines.`,
|
|
384
|
+
role: 'assistant',
|
|
385
|
+
});
|
|
386
|
+
items.push({ id: `t-${i}`, kind: 'tool', name: 'read_file', id_call: `c-${i}`, args: '{"path":"a.ts"}', result: 'ok', latencyMs: 5, status: 'done' });
|
|
387
|
+
}
|
|
388
|
+
const { lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialTranscript: items }));
|
|
389
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
390
|
+
const frame = lastFrame() ?? '';
|
|
391
|
+
// I1: frame never exceeds terminal rows; input + tail always on screen.
|
|
392
|
+
expect(frame.split('\n').length).toBeLessThanOrEqual(32);
|
|
393
|
+
expect(frame).toContain('Message Klyro');
|
|
394
|
+
expect(frame).toContain('Answer 29');
|
|
395
|
+
expect(frame).not.toContain('user message number 0');
|
|
396
|
+
});
|
|
359
397
|
it('Shift+Up / Shift+Down scroll by one line', async () => {
|
|
360
398
|
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
|
|
361
399
|
await new Promise((r) => setTimeout(r, 50));
|
package/dist/tui/measure.d.ts
CHANGED
package/dist/tui/measure.js
CHANGED
|
@@ -102,7 +102,7 @@ export function blockSig(b) {
|
|
|
102
102
|
case 'assistant':
|
|
103
103
|
return `a:${b.text.length}:${b.text.slice(0, 16)}:${b.text.slice(-16)}`;
|
|
104
104
|
case 'group':
|
|
105
|
-
return `g:${b.count}:${b.expanded ? 1 : 0}`;
|
|
105
|
+
return `g:${b.count}:${b.expanded ? 1 : 0}:${b.status}:${b.resultLen}`;
|
|
106
106
|
case 'error':
|
|
107
107
|
return `e:${b.message.length}:${b.message.slice(-32)}`;
|
|
108
108
|
case 'policy':
|
package/dist/tui/transcript.d.ts
CHANGED
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { type DiffHunk } from './diff.js';
|
|
14
|
+
/** Patch applied to a running tool item when its result arrives. */
|
|
15
|
+
export interface ToolResultPatch {
|
|
16
|
+
result: string;
|
|
17
|
+
isError: boolean;
|
|
18
|
+
latencyMs: number;
|
|
19
|
+
status: 'done' | 'error';
|
|
20
|
+
}
|
|
14
21
|
export type TranscriptItem = {
|
|
15
22
|
id: string;
|
|
16
23
|
kind: 'text';
|
package/package.json
CHANGED