klyro 0.1.13 → 0.1.14
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/provider-adapter.js +8 -2
- package/dist/cli/repl.js +39 -10
- package/dist/tui/app.js +33 -2
- package/package.json +1 -1
|
@@ -211,8 +211,14 @@ async function* streamChatCompletions(url, opts, req, fetchImpl) {
|
|
|
211
211
|
pendingUsage = { input: chunk.usage.prompt_tokens, output: chunk.usage.completion_tokens };
|
|
212
212
|
}
|
|
213
213
|
for (const choice of chunk.choices) {
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
// Handle multiple possible content fields for compat (OpenAI, Ollama, OpenRouter gemini, etc.)
|
|
215
|
+
const delta = choice.delta;
|
|
216
|
+
const text = delta?.content ??
|
|
217
|
+
delta?.text ??
|
|
218
|
+
choice?.message?.content ??
|
|
219
|
+
choice?.delta?.text;
|
|
220
|
+
if (typeof text === 'string' && text) {
|
|
221
|
+
yield { kind: 'text_delta', text };
|
|
216
222
|
}
|
|
217
223
|
for (const tc of choice.delta.tool_calls ?? []) {
|
|
218
224
|
if (tc.id && tc.function?.name) {
|
package/dist/cli/repl.js
CHANGED
|
@@ -189,8 +189,16 @@ export async function startRepl(opts = {}) {
|
|
|
189
189
|
}
|
|
190
190
|
else if (ev.kind === 'text_delta') {
|
|
191
191
|
textBuf += ev.text;
|
|
192
|
-
//
|
|
193
|
-
|
|
192
|
+
// Try full-screen live region first (batched 30fps)
|
|
193
|
+
const g = globalThis;
|
|
194
|
+
if (isMounted && g.__klyroAppendDelta) {
|
|
195
|
+
g.__klyroAppendDelta(ev.text);
|
|
196
|
+
// Also keep coalescing for fallback inline mode
|
|
197
|
+
if (!pendingTextId)
|
|
198
|
+
pendingTextId = `text-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// Fallback inline mode: coalesce via queuedAppend
|
|
194
202
|
if (pendingTextId) {
|
|
195
203
|
const last = pendingQueue[pendingQueue.length - 1];
|
|
196
204
|
if (last?.kind === 'append' && last.item.kind === 'text' && last.item.id === pendingTextId) {
|
|
@@ -198,8 +206,6 @@ export async function startRepl(opts = {}) {
|
|
|
198
206
|
return;
|
|
199
207
|
}
|
|
200
208
|
}
|
|
201
|
-
// For mounted case, App will merge via its own coalescing (same id)
|
|
202
|
-
// so reuse pendingTextId to let App merge
|
|
203
209
|
if (pendingTextId && isMounted) {
|
|
204
210
|
queuedAppend({ id: pendingTextId, kind: 'text', text: ev.text, role: 'assistant' });
|
|
205
211
|
return;
|
|
@@ -265,6 +271,18 @@ export async function startRepl(opts = {}) {
|
|
|
265
271
|
status: ev.isError ? 'error' : 'done',
|
|
266
272
|
});
|
|
267
273
|
}
|
|
274
|
+
else if (ev.kind === 'final_text') {
|
|
275
|
+
// Commit live region for both inline and full-screen TUI
|
|
276
|
+
const g2 = globalThis;
|
|
277
|
+
g2.__klyroCommitLive?.();
|
|
278
|
+
pendingTextId = null;
|
|
279
|
+
// For full-screen, also ensure liveText is committed if any remaining
|
|
280
|
+
if (ev.text) {
|
|
281
|
+
const g3 = globalThis;
|
|
282
|
+
// If liveText was batched, ensure it's flushed and committed
|
|
283
|
+
g2.__klyroCommitLive?.();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
268
286
|
else if (ev.kind === 'usage') {
|
|
269
287
|
queuedStatus({ usageInput: ev.input, usageOutput: ev.output });
|
|
270
288
|
}
|
|
@@ -307,9 +325,23 @@ export async function startRepl(opts = {}) {
|
|
|
307
325
|
queuedAppend({ id: `verr-${Date.now()}`, kind: 'error', message: `Verification failed after ${result.verification?.attempts ?? 3} attempts. Check output above.` });
|
|
308
326
|
queuedStatus({ status: 'error', errorMessage: 'verification failed' });
|
|
309
327
|
}
|
|
310
|
-
else if (result.status === 'no_final'
|
|
311
|
-
|
|
312
|
-
|
|
328
|
+
else if (result.status === 'no_final') {
|
|
329
|
+
if (result.finalText) {
|
|
330
|
+
queuedStatus({ status: 'done', repairs: result.repairs ?? 0 });
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
// For simple chat, no_final with empty text is often a provider quirk (e.g. gemini via openai compat)
|
|
334
|
+
// Show a helpful message but don't mark as error in header for chat
|
|
335
|
+
const isSimpleChat = taskText.trim().split(/\s+/).length <= 6;
|
|
336
|
+
if (isSimpleChat) {
|
|
337
|
+
queuedStatus({ status: 'done', repairs: result.repairs ?? 0 });
|
|
338
|
+
queuedAppend({ id: `no_final-${Date.now()}`, kind: 'text', text: `(no response — try /model ${model} or check provider logs)`, role: 'assistant' });
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
queuedStatus({ status: 'error', errorMessage: 'no final text' });
|
|
342
|
+
queuedAppend({ id: `no_final-${Date.now()}`, kind: 'error', message: 'Provider returned no final text — check model/provider (try /model or /doctor)' });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
313
345
|
}
|
|
314
346
|
else {
|
|
315
347
|
queuedStatus({ status: 'complete' === result.status ? 'done' : 'error', repairs: result.repairs ?? 0 });
|
|
@@ -317,9 +349,6 @@ export async function startRepl(opts = {}) {
|
|
|
317
349
|
if (sessionId) {
|
|
318
350
|
queuedAppend({ id: `sess-end-${Date.now()}`, kind: 'text', text: `session ${sessionId.slice(0, 8)} ${result.status} — ${result.steps} steps, ${result.toolCalls} tool calls`, role: 'assistant' });
|
|
319
351
|
}
|
|
320
|
-
else if (result.status === 'no_final' && !result.finalText) {
|
|
321
|
-
queuedAppend({ id: `no_final-${Date.now()}`, kind: 'error', message: 'Provider returned no final text — check model/provider' });
|
|
322
|
-
}
|
|
323
352
|
}
|
|
324
353
|
catch (err) {
|
|
325
354
|
const message = err instanceof Error ? err.message : String(err);
|
package/dist/tui/app.js
CHANGED
|
@@ -31,6 +31,33 @@ export function App(props) {
|
|
|
31
31
|
});
|
|
32
32
|
const [elapsed, setElapsed] = useState(0);
|
|
33
33
|
const [queued, setQueued] = useState(null);
|
|
34
|
+
const [liveText, setLiveText] = useState('');
|
|
35
|
+
const batchRef = useRef('');
|
|
36
|
+
const batchTimer = useRef(null);
|
|
37
|
+
const flushBatch = useCallback(() => {
|
|
38
|
+
if (batchRef.current) {
|
|
39
|
+
const chunk = batchRef.current;
|
|
40
|
+
batchRef.current = '';
|
|
41
|
+
setLiveText((prev) => prev + chunk);
|
|
42
|
+
}
|
|
43
|
+
if (batchTimer.current) {
|
|
44
|
+
clearTimeout(batchTimer.current);
|
|
45
|
+
batchTimer.current = null;
|
|
46
|
+
}
|
|
47
|
+
}, []);
|
|
48
|
+
const appendDeltaBatched = useCallback((text) => {
|
|
49
|
+
batchRef.current += text;
|
|
50
|
+
if (!batchTimer.current)
|
|
51
|
+
batchTimer.current = setTimeout(flushBatch, 33);
|
|
52
|
+
}, [flushBatch]);
|
|
53
|
+
const commitLive = useCallback(() => {
|
|
54
|
+
flushBatch();
|
|
55
|
+
if (liveText) {
|
|
56
|
+
const item = { id: nextId('text'), kind: 'text', text: liveText, role: 'assistant' };
|
|
57
|
+
setTranscript((prev) => [...prev, item]);
|
|
58
|
+
setLiveText('');
|
|
59
|
+
}
|
|
60
|
+
}, [liveText, flushBatch]);
|
|
34
61
|
useEffect(() => bridge.subscribe((p) => setAwaitingApproval(p !== null)), [bridge]);
|
|
35
62
|
// 2.4 — send queued when idle
|
|
36
63
|
useEffect(() => {
|
|
@@ -71,12 +98,16 @@ export function App(props) {
|
|
|
71
98
|
globalThis.__klyroAppAppend = append;
|
|
72
99
|
globalThis.__klyroAppStatus = updateStatus;
|
|
73
100
|
globalThis.__klyroAppPlan = updatePlan;
|
|
101
|
+
globalThis.__klyroAppendDelta = appendDeltaBatched;
|
|
102
|
+
globalThis.__klyroCommitLive = commitLive;
|
|
74
103
|
return () => {
|
|
75
104
|
delete globalThis.__klyroAppAppend;
|
|
76
105
|
delete globalThis.__klyroAppStatus;
|
|
77
106
|
delete globalThis.__klyroAppPlan;
|
|
107
|
+
delete globalThis.__klyroAppendDelta;
|
|
108
|
+
delete globalThis.__klyroCommitLive;
|
|
78
109
|
};
|
|
79
|
-
}, [append, updateStatus, updatePlan]);
|
|
110
|
+
}, [append, updateStatus, updatePlan, appendDeltaBatched, commitLive]);
|
|
80
111
|
// Single useInput owner — handles queued when running (2.4)
|
|
81
112
|
useInput((inputStr, key) => {
|
|
82
113
|
if (awaitingApproval)
|
|
@@ -129,5 +160,5 @@ export function App(props) {
|
|
|
129
160
|
const width = stdout?.columns ?? 100;
|
|
130
161
|
const height = stdout?.rows ?? 30;
|
|
131
162
|
const isSmall = width < 80;
|
|
132
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, height: height - 1, children: [_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: tokens.ansi.border, paddingX: 1, children: [_jsx(Text, { bold: true, children: "KLYRO v0.1.
|
|
163
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height - 1, children: [_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: tokens.ansi.border, paddingX: 1, children: [_jsx(Text, { bold: true, children: "KLYRO v0.1.14" }), _jsxs(Text, { color: tokens.ansi.muted, children: [status.model, " \u00B7 API Usage Billing \u00B7 step ", status.step, "/", status.maxSteps, " \u00B7 ", status.status, " \u00B7 repairs ", status.repairs] }), _jsx(Text, { color: tokens.ansi.muted, children: props.cwd })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", paddingX: 1, paddingY: 1, children: [transcript.length === 0 ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: tokens.ansi.muted, children: "No conversation yet. Try \"fix the failing login test\"" }) })) : (transcript.map((item) => (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: item.kind === 'text' && item.role === 'user' ? (_jsxs(Text, { children: ["\u203A ", item.text] })) : item.kind === 'text' ? (_jsxs(Text, { children: [" ", item.text] })) : item.kind === 'tool' ? (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: tokens.ansi.border, paddingX: 1, children: [_jsxs(Text, { children: [item.name, " ", item.isError ? '✗' : '✓', " ", item.latencyMs ?? 0, "ms"] }), item.result ? _jsx(Text, { color: tokens.ansi.muted, children: String(item.result).slice(0, 200) }) : null] })) : item.kind === 'diff' ? (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: tokens.ansi.border, paddingX: 1, children: [_jsx(Text, { bold: true, children: item.summary ?? 'Diff' }), item.hunks.map((h, i) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: tokens.ansi.info, children: h.path }), h.lines.map((l, j) => (_jsxs(Text, { color: l.kind === 'add' ? tokens.ansi.success : l.kind === 'remove' ? tokens.ansi.error : tokens.ansi.muted, children: [l.kind === 'add' ? '+ ' : l.kind === 'remove' ? '- ' : ' ', l.text] }, j)))] }, i)))] })) : (_jsx(Transcript, { items: [item] })) }, item.id)))), liveText ? (_jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { children: [liveText, "\u258D"] }) })) : status.status === 'running' ? (_jsxs(Box, { children: [_jsx(Text, { color: tokens.ansi.info, children: "\u2726 Thinking..." }), _jsxs(Text, { color: tokens.ansi.muted, children: [" \u00B7 ", Math.round(elapsed / 1000), "s"] })] })) : null, plan.length > 0 ? _jsx(PlanView, { steps: plan, expanded: false, onToggle: () => { } }) : null] }), _jsxs(Box, { borderStyle: "single", borderColor: tokens.ansi.accent, paddingX: 1, children: [_jsx(Text, { children: "\u203A " }), _jsxs(Text, { children: [input, "\u258F"] })] }), _jsxs(Box, { justifyContent: "space-between", paddingX: 1, borderStyle: "single", borderColor: tokens.ansi.border, children: [_jsxs(Text, { color: tokens.ansi.muted, children: [status.model, " \u00B7 ", status.usageInput + status.usageOutput, " tokens \u00B7 $", (status.usageInput / 1000 * 0.003 + status.usageOutput / 1000 * 0.015).toFixed(2), " \u00B7 ", Math.round(elapsed / 1000), "s"] }), _jsx(Text, { color: tokens.ansi.muted, children: isSmall ? 'Ctrl+C interrupt' : 'Ctrl+C interrupt · Ctrl+O expand · ↑↓ scroll' })] })] }));
|
|
133
164
|
}
|