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.
@@ -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
- if (choice.delta.content) {
215
- yield { kind: 'text_delta', text: choice.delta.content };
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
- // Coalesce: reuse pending text item if still queued, otherwise create one.
193
- // App.tsx also coalesces post-mount, so we only need to avoid queue bloat.
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' && result.finalText) {
311
- // Simple chat with text but no tool calls — treat as done (provider finished)
312
- queuedStatus({ status: 'done', repairs: result.repairs ?? 0 });
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.13" }), _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)))), 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' })] })] }));
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Klyro — autonomous coding harness CLI that streams from any OpenAI-compatible or Anthropic LLM endpoint.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",