klyro 0.1.55 → 0.1.56
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/agent/anthropic-adapter.js +12 -2
- package/dist/agent/provider-adapter.d.ts +3 -0
- package/dist/agent/provider-adapter.js +8 -0
- package/dist/agent/runtime.d.ts +3 -0
- package/dist/agent/runtime.js +7 -0
- package/dist/cli/repl.js +31 -0
- package/dist/tui/app.d.ts +2 -0
- package/dist/tui/app.js +31 -3
- package/dist/tui/app.test.js +52 -24
- package/dist/tui/measure.d.ts +3 -0
- package/dist/tui/measure.js +4 -0
- package/dist/tui/transcript.d.ts +4 -0
- package/package.json +1 -1
|
@@ -115,6 +115,8 @@ async function* streamAnthropic(req, opts) {
|
|
|
115
115
|
const toolBuffers = new Map();
|
|
116
116
|
// Map content_block index → tool_use id (persists after tool completes to handle late deltas)
|
|
117
117
|
const indexToToolId = new Map();
|
|
118
|
+
// Active thinking-block index (Anthropic reasoning channel).
|
|
119
|
+
const thinkingState = { idx: null };
|
|
118
120
|
// message_stop already yields message_end — don't emit a second one at EOF.
|
|
119
121
|
let sawMessageEnd = false;
|
|
120
122
|
try {
|
|
@@ -153,7 +155,7 @@ async function* streamAnthropic(req, opts) {
|
|
|
153
155
|
catch {
|
|
154
156
|
continue;
|
|
155
157
|
}
|
|
156
|
-
const out = translateSse(e.event, parsed, toolBuffers, indexToToolId);
|
|
158
|
+
const out = translateSse(e.event, parsed, toolBuffers, indexToToolId, thinkingState);
|
|
157
159
|
for (const ev of out) {
|
|
158
160
|
if (ev.kind === 'message_end')
|
|
159
161
|
sawMessageEnd = true;
|
|
@@ -173,7 +175,7 @@ async function* streamAnthropic(req, opts) {
|
|
|
173
175
|
if (!sawMessageEnd)
|
|
174
176
|
yield { kind: 'message_end', finishReason: 'stop' };
|
|
175
177
|
}
|
|
176
|
-
function translateSse(event, parsed, toolBuffers, indexToToolId) {
|
|
178
|
+
function translateSse(event, parsed, toolBuffers, indexToToolId, thinking) {
|
|
177
179
|
const out = [];
|
|
178
180
|
switch (event) {
|
|
179
181
|
case 'content_block_start': {
|
|
@@ -185,6 +187,9 @@ function translateSse(event, parsed, toolBuffers, indexToToolId) {
|
|
|
185
187
|
indexToToolId.set(idx, block.id);
|
|
186
188
|
out.push({ kind: 'tool_call_start', id: block.id, name: block.name });
|
|
187
189
|
}
|
|
190
|
+
else if ((block?.type === 'thinking' || block?.type === 'redacted_thinking') && thinking && idx !== undefined) {
|
|
191
|
+
thinking.idx = idx;
|
|
192
|
+
}
|
|
188
193
|
return out;
|
|
189
194
|
}
|
|
190
195
|
case 'content_block_delta': {
|
|
@@ -193,6 +198,9 @@ function translateSse(event, parsed, toolBuffers, indexToToolId) {
|
|
|
193
198
|
if (delta?.type === 'text_delta' && typeof delta.text === 'string') {
|
|
194
199
|
out.push({ kind: 'text_delta', text: delta.text });
|
|
195
200
|
}
|
|
201
|
+
else if (delta?.type === 'thinking_delta' && typeof delta.thinking === 'string' && delta.thinking) {
|
|
202
|
+
out.push({ kind: 'thinking_delta', text: delta.thinking });
|
|
203
|
+
}
|
|
196
204
|
else if (delta?.type === 'input_json_delta' && typeof delta.partial_json === 'string') {
|
|
197
205
|
const id = findToolIdByIndex(index, toolBuffers, indexToToolId);
|
|
198
206
|
if (id) {
|
|
@@ -207,6 +215,8 @@ function translateSse(event, parsed, toolBuffers, indexToToolId) {
|
|
|
207
215
|
}
|
|
208
216
|
case 'content_block_stop': {
|
|
209
217
|
const index = parsed.index;
|
|
218
|
+
if (thinking && index !== undefined && index === thinking.idx)
|
|
219
|
+
thinking.idx = null;
|
|
210
220
|
const id = findToolIdByIndex(index, toolBuffers, indexToToolId);
|
|
211
221
|
if (id) {
|
|
212
222
|
toolBuffers.delete(id);
|
|
@@ -239,6 +239,14 @@ async function* streamChatCompletions(url, opts, req, fetchImpl) {
|
|
|
239
239
|
if (typeof text === 'string' && text) {
|
|
240
240
|
yield { kind: 'text_delta', text };
|
|
241
241
|
}
|
|
242
|
+
// Reasoning channel (DeepSeek-R1 / OpenRouter / vLLM et al. send
|
|
243
|
+
// `reasoning_content`; some proxies use `reasoning`). Shown dimmed
|
|
244
|
+
// while working, discarded when the answer completes.
|
|
245
|
+
const thinking = delta?.reasoning_content ??
|
|
246
|
+
delta?.reasoning;
|
|
247
|
+
if (typeof thinking === 'string' && thinking) {
|
|
248
|
+
yield { kind: 'thinking_delta', text: thinking };
|
|
249
|
+
}
|
|
242
250
|
for (const tc of choice.delta.tool_calls ?? []) {
|
|
243
251
|
if (tc.id && tc.function?.name) {
|
|
244
252
|
toolIds.set(tc.index, tc.id);
|
package/dist/agent/runtime.d.ts
CHANGED
package/dist/agent/runtime.js
CHANGED
|
@@ -236,6 +236,9 @@ export async function run(opts, deps) {
|
|
|
236
236
|
};
|
|
237
237
|
const events = deps.adapter.stream(req);
|
|
238
238
|
let textBuf = '';
|
|
239
|
+
// Thinking is ephemeral: streamed to the UI live, never stored in the
|
|
240
|
+
// transcript, and cleared when the turn's answer completes.
|
|
241
|
+
let thinkingBuf = '';
|
|
239
242
|
const pendingToolCalls = new Map();
|
|
240
243
|
let lastFinishReason;
|
|
241
244
|
for await (const ev of events) {
|
|
@@ -245,6 +248,10 @@ export async function run(opts, deps) {
|
|
|
245
248
|
textBuf += ev.text;
|
|
246
249
|
emit?.({ kind: 'text_delta', text: ev.text });
|
|
247
250
|
}
|
|
251
|
+
else if (ev.kind === 'thinking_delta') {
|
|
252
|
+
thinkingBuf += ev.text;
|
|
253
|
+
emit?.({ kind: 'thinking_delta', text: ev.text });
|
|
254
|
+
}
|
|
248
255
|
else if (ev.kind === 'tool_call_start') {
|
|
249
256
|
pendingToolCalls.set(ev.id, { id: ev.id, name: ev.name, argsJson: '' });
|
|
250
257
|
emit?.({ kind: 'tool_call_start', id: ev.id, name: ev.name });
|
package/dist/cli/repl.js
CHANGED
|
@@ -144,6 +144,23 @@ export async function startRepl(opts = {}) {
|
|
|
144
144
|
else
|
|
145
145
|
pendingQueue.push({ kind: 'delta', text });
|
|
146
146
|
}
|
|
147
|
+
// Ephemeral reasoning display (light-white while working, gone on response).
|
|
148
|
+
function queuedThinking(text) {
|
|
149
|
+
if (!text)
|
|
150
|
+
return;
|
|
151
|
+
if (isMounted && directHooks)
|
|
152
|
+
directHooks.appendThinkingDelta(text);
|
|
153
|
+
else
|
|
154
|
+
pendingQueue.push({ kind: 'thinking', text });
|
|
155
|
+
}
|
|
156
|
+
function clearThinking() {
|
|
157
|
+
for (let i = pendingQueue.length - 1; i >= 0; i--) {
|
|
158
|
+
if (pendingQueue[i]?.kind === 'thinking')
|
|
159
|
+
pendingQueue.splice(i, 1);
|
|
160
|
+
}
|
|
161
|
+
if (isMounted && directHooks)
|
|
162
|
+
directHooks.clearThinking();
|
|
163
|
+
}
|
|
147
164
|
// Tool results patch the running start-item in place (App.updateTool) so a
|
|
148
165
|
// group resolves to done/error with its real latency instead of ticking
|
|
149
166
|
// forever. Falls back to a standalone item if the start item is gone.
|
|
@@ -404,10 +421,13 @@ export async function startRepl(opts = {}) {
|
|
|
404
421
|
text += ev.text;
|
|
405
422
|
queuedDelta(ev.text);
|
|
406
423
|
}
|
|
424
|
+
else if (ev.kind === 'thinking_delta')
|
|
425
|
+
queuedThinking(ev.text);
|
|
407
426
|
else if (ev.kind === 'error')
|
|
408
427
|
throw new Error(ev.message);
|
|
409
428
|
}
|
|
410
429
|
lastAssistantText = text;
|
|
430
|
+
clearThinking();
|
|
411
431
|
queuedStatus({ status: 'done' });
|
|
412
432
|
return text;
|
|
413
433
|
}
|
|
@@ -454,6 +474,8 @@ export async function startRepl(opts = {}) {
|
|
|
454
474
|
hooks.appendDelta(ev.text);
|
|
455
475
|
else if (ev.kind === 'toolupdate')
|
|
456
476
|
hooks.updateTool(ev.idCall, ev.patch);
|
|
477
|
+
else if (ev.kind === 'thinking')
|
|
478
|
+
hooks.appendThinkingDelta(ev.text);
|
|
457
479
|
else
|
|
458
480
|
hooks.append(ev.item);
|
|
459
481
|
}
|
|
@@ -523,10 +545,13 @@ export async function startRepl(opts = {}) {
|
|
|
523
545
|
simpleText += ev.text;
|
|
524
546
|
queuedDelta(ev.text);
|
|
525
547
|
}
|
|
548
|
+
else if (ev.kind === 'thinking_delta')
|
|
549
|
+
queuedThinking(ev.text);
|
|
526
550
|
else if (ev.kind === 'error')
|
|
527
551
|
throw new Error(ev.message);
|
|
528
552
|
}
|
|
529
553
|
lastAssistantText = simpleText;
|
|
554
|
+
clearThinking();
|
|
530
555
|
queuedStatus({ status: 'done' });
|
|
531
556
|
return;
|
|
532
557
|
}
|
|
@@ -553,11 +578,15 @@ export async function startRepl(opts = {}) {
|
|
|
553
578
|
onEvent: (ev) => {
|
|
554
579
|
if (ev.kind === 'step_start') {
|
|
555
580
|
queuedStatus({ step: ev.step });
|
|
581
|
+
clearThinking(); // fresh reasoning display per step
|
|
556
582
|
}
|
|
557
583
|
else if (ev.kind === 'text_delta') {
|
|
558
584
|
// single appendDelta path — App merges into one assistant item (Q→A order, no duplication)
|
|
559
585
|
queuedDelta(ev.text);
|
|
560
586
|
}
|
|
587
|
+
else if (ev.kind === 'thinking_delta') {
|
|
588
|
+
queuedThinking(ev.text);
|
|
589
|
+
}
|
|
561
590
|
else if (ev.kind === 'verification_started') {
|
|
562
591
|
queuedAppend({ id: `vrfy-${Date.now()}`, kind: 'text', text: `[verify] running \`${ev.command}\``, role: 'assistant' });
|
|
563
592
|
queuedStatus({ status: 'running' });
|
|
@@ -630,6 +659,8 @@ export async function startRepl(opts = {}) {
|
|
|
630
659
|
}
|
|
631
660
|
}
|
|
632
661
|
else if (ev.kind === 'final_text') {
|
|
662
|
+
// Response arrived: thinking display goes away, only the answer stays.
|
|
663
|
+
clearThinking();
|
|
633
664
|
// streamingId is closed by status change; no extra handling needed
|
|
634
665
|
}
|
|
635
666
|
else if (ev.kind === 'usage') {
|
package/dist/tui/app.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface AppProps {
|
|
|
29
29
|
scrollToTop: () => void;
|
|
30
30
|
transcript: TranscriptScrollHandle;
|
|
31
31
|
updateTool: (idCall: string, patch: ToolResultPatch) => void;
|
|
32
|
+
appendThinkingDelta: (text: string) => void;
|
|
33
|
+
clearThinking: () => void;
|
|
32
34
|
}) => void;
|
|
33
35
|
version?: string;
|
|
34
36
|
isFullscreen?: boolean;
|
package/dist/tui/app.js
CHANGED
|
@@ -259,6 +259,9 @@ export function App(props) {
|
|
|
259
259
|
else if (it.kind === 'text') {
|
|
260
260
|
out.push({ key: it.id, desc: { kind: 'assistant', text: it.text }, groupIndex: gi, tail: null });
|
|
261
261
|
}
|
|
262
|
+
else if (it.kind === 'thinking') {
|
|
263
|
+
out.push({ key: it.id, desc: { kind: 'reasoning', text: it.text }, groupIndex: gi, tail: null });
|
|
264
|
+
}
|
|
262
265
|
else if (it.kind === 'error') {
|
|
263
266
|
out.push({ key: it.id, desc: { kind: 'error', message: it.message }, groupIndex: gi, tail: null });
|
|
264
267
|
}
|
|
@@ -393,8 +396,30 @@ export function App(props) {
|
|
|
393
396
|
setTranscript((prev) => [...prev, { id, kind: 'text', text, role: 'assistant' }]);
|
|
394
397
|
}
|
|
395
398
|
}, []);
|
|
396
|
-
|
|
397
|
-
|
|
399
|
+
// Ephemeral reasoning display: merges into one transient item (never in
|
|
400
|
+
// context/persistence); removed when the turn's answer completes.
|
|
401
|
+
const thinkingIdRef = useRef(null);
|
|
402
|
+
const appendThinkingDelta = useCallback((text) => {
|
|
403
|
+
if (!text)
|
|
404
|
+
return;
|
|
405
|
+
const tid = thinkingIdRef.current;
|
|
406
|
+
if (tid)
|
|
407
|
+
setTranscript((prev) => { const idx = prev.findIndex((x) => x.id === tid); if (idx === -1)
|
|
408
|
+
return [...prev, { id: tid, kind: 'thinking', text }]; const cur = prev[idx]; const copy = [...prev]; copy[idx] = { ...cur, text: cur.text + text }; return copy; });
|
|
409
|
+
else {
|
|
410
|
+
const id = nextId('thinking');
|
|
411
|
+
thinkingIdRef.current = id;
|
|
412
|
+
setTranscript((prev) => [...prev, { id, kind: 'thinking', text }]);
|
|
413
|
+
}
|
|
414
|
+
}, []);
|
|
415
|
+
const clearThinking = useCallback(() => {
|
|
416
|
+
thinkingIdRef.current = null;
|
|
417
|
+
setTranscript((prev) => (prev.some((x) => x.kind === 'thinking') ? prev.filter((x) => x.kind !== 'thinking') : prev));
|
|
418
|
+
}, []);
|
|
419
|
+
useEffect(() => { if (status.status !== 'running') {
|
|
420
|
+
streamingIdRef.current = null;
|
|
421
|
+
thinkingIdRef.current = null;
|
|
422
|
+
} }, [status.status]);
|
|
398
423
|
const updateStatus = useCallback((s) => setStatus((p) => ({ ...p, ...s })), []);
|
|
399
424
|
const updatePlan = useCallback((p) => setPlan(p), []);
|
|
400
425
|
// Tool results patch the running start-item IN PLACE (no second item, so a
|
|
@@ -439,7 +464,7 @@ export function App(props) {
|
|
|
439
464
|
}), []);
|
|
440
465
|
const onMountedRef = useRef(props.onMounted);
|
|
441
466
|
useEffect(() => { onMountedRef.current = props.onMounted; }, [props.onMounted]);
|
|
442
|
-
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]);
|
|
467
|
+
useEffect(() => { onMountedRef.current?.({ append, appendDelta, updateStatus, updatePlan, clearTranscript, scrollLines, scrollToBottom, scrollHalfPage, scrollToTop, transcript: transcriptHandle, updateTool, appendThinkingDelta, clearThinking }); 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]);
|
|
443
468
|
const toggleGroup = (id) => setExpandedGroups((prev) => { const n = new Set(prev); if (n.has(id))
|
|
444
469
|
n.delete(id);
|
|
445
470
|
else
|
|
@@ -714,6 +739,9 @@ export function App(props) {
|
|
|
714
739
|
// prose — render markdown, not raw **, with proper wrap and guide
|
|
715
740
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: tokens.colors.accent, children: [g('agentBullet'), " Klyro"] })] }), _jsx(Box, { paddingLeft: 2, flexDirection: "column", children: _jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.dim, children: [" ", g('guide'), " "] }), _jsx(Box, { flexGrow: 1, children: _jsx(MarkdownText, { text: it.text }) })] }) })] }, it.id));
|
|
716
741
|
}
|
|
742
|
+
// Ephemeral reasoning: light-white while working, removed on response.
|
|
743
|
+
if (it.kind === 'thinking')
|
|
744
|
+
return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsx(Text, { wrap: "wrap", color: tokens.colors.dim, children: it.text }) }, it.id);
|
|
717
745
|
if (it.kind === 'error')
|
|
718
746
|
return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.colors.err, children: [" ", g('guide'), " ", g('failure'), " ", it.message] }) }, it.id);
|
|
719
747
|
if (it.kind === 'policy')
|
package/dist/tui/app.test.js
CHANGED
|
@@ -94,6 +94,34 @@ describe('App', () => {
|
|
|
94
94
|
}
|
|
95
95
|
return out;
|
|
96
96
|
}
|
|
97
|
+
// Polling assertions: hook-driven updates flush on React's schedule, so
|
|
98
|
+
// fixed sleeps flake under load. Poll the frame instead.
|
|
99
|
+
async function waitForMatch(getFrame, re, timeout = 4000) {
|
|
100
|
+
const start = Date.now();
|
|
101
|
+
let frame = '';
|
|
102
|
+
for (;;) {
|
|
103
|
+
frame = getFrame() ?? '';
|
|
104
|
+
if (re.test(frame))
|
|
105
|
+
return frame;
|
|
106
|
+
if (Date.now() - start > timeout) {
|
|
107
|
+
throw new Error(`timed out waiting for ${re}\nlast frame:\n${frame.slice(0, 2000)}`);
|
|
108
|
+
}
|
|
109
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function waitForAbsent(getFrame, re, timeout = 4000) {
|
|
113
|
+
const start = Date.now();
|
|
114
|
+
let frame = '';
|
|
115
|
+
for (;;) {
|
|
116
|
+
frame = getFrame() ?? '';
|
|
117
|
+
if (!re.test(frame))
|
|
118
|
+
return frame;
|
|
119
|
+
if (Date.now() - start > timeout) {
|
|
120
|
+
throw new Error(`timed out waiting for absence of ${re}\nlast frame:\n${frame.slice(0, 2000)}`);
|
|
121
|
+
}
|
|
122
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
97
125
|
// ANSI sequences Ink's parse-keypress recognizes.
|
|
98
126
|
const KEY_HOME = '\x1b[H';
|
|
99
127
|
const KEY_END = '\x1b[F';
|
|
@@ -263,14 +291,11 @@ describe('App', () => {
|
|
|
263
291
|
// Wheel up ×12 (3 lines each = 36 > maxTop 32) → pinned at top.
|
|
264
292
|
for (let i = 0; i < 12; i++)
|
|
265
293
|
captured.scrollLines(-3);
|
|
266
|
-
|
|
267
|
-
const top = lastFrame() ?? '';
|
|
268
|
-
expect(top).toMatch(/MSG-00-tag/);
|
|
294
|
+
const top = await waitForMatch(lastFrame, /MSG-00-tag/);
|
|
269
295
|
expect(top).not.toMatch(/MSG-24-tag/);
|
|
270
296
|
// scrollToBottom → tail visible again.
|
|
271
297
|
captured.scrollToBottom();
|
|
272
|
-
await
|
|
273
|
-
expect(lastFrame() ?? '').toMatch(/MSG-24-tag/);
|
|
298
|
+
await waitForMatch(lastFrame, /MSG-24-tag/);
|
|
274
299
|
});
|
|
275
300
|
it('idle Ctrl+C quits (design.md §18)', async () => {
|
|
276
301
|
const onSlash = vi.fn(async () => { });
|
|
@@ -342,19 +367,14 @@ describe('App', () => {
|
|
|
342
367
|
await new Promise((r) => setTimeout(r, 50));
|
|
343
368
|
expect(handle).not.toBeNull();
|
|
344
369
|
handle.runTranscriptCommand('messages_half_page_up');
|
|
345
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
346
|
-
let frame = lastFrame() ?? '';
|
|
347
370
|
// Half page (10 lines) up from bottom (row 30 → 20): MSG-24 gone, MSG-14 in view.
|
|
348
|
-
|
|
349
|
-
|
|
371
|
+
await waitForAbsent(lastFrame, /MSG-24-tag/);
|
|
372
|
+
await waitForMatch(lastFrame, /MSG-14-tag/);
|
|
350
373
|
handle.runTranscriptCommand('messages_first');
|
|
351
|
-
|
|
352
|
-
frame = lastFrame() ?? '';
|
|
353
|
-
expect(frame).toMatch(/MSG-00-tag/);
|
|
374
|
+
let frame = await waitForMatch(lastFrame, /MSG-00-tag/);
|
|
354
375
|
expect(frame).not.toMatch(/MSG-24-tag/);
|
|
355
376
|
handle.runTranscriptCommand('messages_last');
|
|
356
|
-
await
|
|
357
|
-
expect(lastFrame() ?? '').toMatch(/MSG-24-tag/);
|
|
377
|
+
await waitForMatch(lastFrame, /MSG-24-tag/);
|
|
358
378
|
});
|
|
359
379
|
it('tool result patches the running item in place (no stale spinner)', async () => {
|
|
360
380
|
let hooks = null;
|
|
@@ -364,13 +384,10 @@ describe('App', () => {
|
|
|
364
384
|
await new Promise((r) => setTimeout(r, 50));
|
|
365
385
|
expect(hooks).not.toBeNull();
|
|
366
386
|
hooks.append({ id: 't1', kind: 'tool', name: 'read_file', id_call: 'c1', args: '{"path":"a.ts"}', status: 'running' });
|
|
367
|
-
await
|
|
368
|
-
expect(lastFrame() ?? '').toMatch(/Read/);
|
|
387
|
+
await waitForMatch(lastFrame, /Read/);
|
|
369
388
|
hooks.updateTool('c1', { result: 'ok', isError: false, latencyMs: 42, status: 'done' });
|
|
370
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
371
|
-
const frame = lastFrame() ?? '';
|
|
372
389
|
// Resolved with real latency — exactly one group (start item patched, no duplicate).
|
|
373
|
-
|
|
390
|
+
const frame = await waitForMatch(lastFrame, /42ms/);
|
|
374
391
|
expect(frame.match(/Read/g)?.length ?? 0).toBeLessThanOrEqual(2);
|
|
375
392
|
});
|
|
376
393
|
it('heavy transcript: frame bounded, input and tail visible', async () => {
|
|
@@ -405,10 +422,9 @@ describe('App', () => {
|
|
|
405
422
|
.then((c) => {
|
|
406
423
|
choice = c;
|
|
407
424
|
});
|
|
408
|
-
await new Promise((r) => setTimeout(r, 80));
|
|
409
425
|
// The modal must actually render — previously it never mounted, so every
|
|
410
426
|
// policy 'ask' hung the runtime forever.
|
|
411
|
-
|
|
427
|
+
await waitForMatch(lastFrame, /approval needed/i);
|
|
412
428
|
expect(bridge.resolve('deny')).toBe(true);
|
|
413
429
|
await pending;
|
|
414
430
|
expect(choice).toBe('deny');
|
|
@@ -429,12 +445,24 @@ describe('App', () => {
|
|
|
429
445
|
} }));
|
|
430
446
|
await new Promise((r) => setTimeout(r, 50));
|
|
431
447
|
hooks.append({ id: 't1', kind: 'tool', name: 'read_file', id_call: 'c1', args: '{"path":"a.ts"}', status: 'running' });
|
|
432
|
-
await new Promise((r) => setTimeout(r, 120));
|
|
433
448
|
// Running group: spinner next to the verb (braille frame or fallback text).
|
|
434
|
-
|
|
449
|
+
await waitForMatch(lastFrame, /⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏|Read/);
|
|
435
450
|
hooks.updateTool('c1', { result: 'ok', isError: false, latencyMs: 42, status: 'done' });
|
|
451
|
+
await waitForMatch(lastFrame, /42ms/);
|
|
452
|
+
});
|
|
453
|
+
it('thinking shows dim while working, clears on response', async () => {
|
|
454
|
+
let hooks = null;
|
|
455
|
+
const { lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, onMounted: (h) => {
|
|
456
|
+
hooks = { appendThinkingDelta: h.appendThinkingDelta, clearThinking: h.clearThinking };
|
|
457
|
+
} }));
|
|
436
458
|
await new Promise((r) => setTimeout(r, 50));
|
|
437
|
-
expect(
|
|
459
|
+
expect(hooks).not.toBeNull();
|
|
460
|
+
hooks.appendThinkingDelta('weighing two approaches... ');
|
|
461
|
+
hooks.appendThinkingDelta('leaning to the second.');
|
|
462
|
+
await waitForMatch(lastFrame, /weighing two approaches\.\.\. leaning to the second\./);
|
|
463
|
+
// Answered: thinking goes away, only the response stays.
|
|
464
|
+
hooks.clearThinking();
|
|
465
|
+
await waitForAbsent(lastFrame, /weighing two approaches/);
|
|
438
466
|
});
|
|
439
467
|
it('Shift+Up / Shift+Down scroll by one line', async () => {
|
|
440
468
|
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
|
package/dist/tui/measure.d.ts
CHANGED
package/dist/tui/measure.js
CHANGED
|
@@ -70,6 +70,8 @@ export function blockHeight(b, termWidth) {
|
|
|
70
70
|
return wrapCount(b.text, termWidth) + 1;
|
|
71
71
|
case 'assistant':
|
|
72
72
|
return 1 + wrapCount(b.text, cw) + 1;
|
|
73
|
+
case 'reasoning':
|
|
74
|
+
return wrapCount(b.text, cw) + 1;
|
|
73
75
|
case 'group':
|
|
74
76
|
if (!b.expanded)
|
|
75
77
|
return 1 + 1;
|
|
@@ -101,6 +103,8 @@ export function blockSig(b) {
|
|
|
101
103
|
return `u:${b.text.length}:${b.text.slice(0, 16)}:${b.text.slice(-16)}`;
|
|
102
104
|
case 'assistant':
|
|
103
105
|
return `a:${b.text.length}:${b.text.slice(0, 16)}:${b.text.slice(-16)}`;
|
|
106
|
+
case 'reasoning':
|
|
107
|
+
return `th:${b.text.length}:${b.text.slice(0, 16)}:${b.text.slice(-16)}`;
|
|
104
108
|
case 'group':
|
|
105
109
|
return `g:${b.count}:${b.expanded ? 1 : 0}:${b.status}:${b.resultLen}`;
|
|
106
110
|
case 'error':
|
package/dist/tui/transcript.d.ts
CHANGED
package/package.json
CHANGED