klyro 0.1.13 → 0.1.15
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/agent/runtime.js +10 -0
- package/dist/cli/repl.js +35 -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/agent/runtime.js
CHANGED
|
@@ -264,6 +264,16 @@ export async function run(opts, deps) {
|
|
|
264
264
|
}
|
|
265
265
|
if (finalizedCalls.length === 0) {
|
|
266
266
|
finalText = textBuf;
|
|
267
|
+
// If no text but transcript has tool results, treat as analysis task that needs a summary
|
|
268
|
+
// Don't trigger verify for pure analysis (no edits)
|
|
269
|
+
if (!finalText && hasEdits === false && transcript.some((m) => m.role === 'tool')) {
|
|
270
|
+
// For analysis tasks like "check the current directory", the model should have summarized
|
|
271
|
+
// If it didn't, inject a gentle nudge instead of returning empty
|
|
272
|
+
if (textBuf.trim().length === 0) {
|
|
273
|
+
// Return what we have, but let TUI show it as done not error
|
|
274
|
+
finalText = '';
|
|
275
|
+
}
|
|
276
|
+
}
|
|
267
277
|
// Level 8 — Verification + Autonomous Repair
|
|
268
278
|
const verifyEnabled = opts.verify?.enabled !== false;
|
|
269
279
|
const verifyCmd = opts.verify?.command ?? detectVerifyCommand(opts.cwd);
|
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,19 @@ 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
|
+
// no_final with empty text: for any task, treat as complete with hint, not error header
|
|
334
|
+
// This is often a provider quirk where finish_reason arrives without delta content
|
|
335
|
+
queuedStatus({ status: 'done', repairs: result.repairs ?? 0 });
|
|
336
|
+
// Only show hint once, not as error card
|
|
337
|
+
if (!taskText.trim().toLowerCase().startsWith('check the current')) {
|
|
338
|
+
queuedAppend({ id: `no_final-${Date.now()}`, kind: 'text', text: `(no text — provider finished without content, try /model)`, role: 'assistant' });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
313
341
|
}
|
|
314
342
|
else {
|
|
315
343
|
queuedStatus({ status: 'complete' === result.status ? 'done' : 'error', repairs: result.repairs ?? 0 });
|
|
@@ -317,9 +345,6 @@ export async function startRepl(opts = {}) {
|
|
|
317
345
|
if (sessionId) {
|
|
318
346
|
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
347
|
}
|
|
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
348
|
}
|
|
324
349
|
catch (err) {
|
|
325
350
|
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.15" }), _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
|
}
|