klyro 0.1.28 → 0.1.30

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/tui/app.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Klyro TUI — opencode-perfectTUI_DESIGN.md §2-7 + user request: no clumsy words, smooth scroll like opencode
3
- * Full-screen alt takeover when supported, inline degrade otherwise. Clean wrap at word boundaries, slider │●.
2
+ * Klyro TUI — opencode-clean — no clumsy words, correct wrap, markdown, scroll
3
+ * Header 3 rows, guide │ at col2, Klyro accent, prose wrapped at word boundaries
4
4
  */
5
5
  import React from 'react';
6
6
  import type { StatusSnapshot } from './status.js';
package/dist/tui/app.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  /**
3
- * Klyro TUI — opencode-perfectTUI_DESIGN.md §2-7 + user request: no clumsy words, smooth scroll like opencode
4
- * Full-screen alt takeover when supported, inline degrade otherwise. Clean wrap at word boundaries, slider │●.
3
+ * Klyro TUI — opencode-clean — no clumsy words, correct wrap, markdown, scroll
4
+ * Header 3 rows, guide │ at col2, Klyro accent, prose wrapped at word boundaries
5
5
  */
6
6
  import { useState, useEffect, useRef, useCallback } from 'react';
7
7
  import { Box, Text, useInput, useStdout } from 'ink';
@@ -71,6 +71,27 @@ function groupTools(items) {
71
71
  flush();
72
72
  return out;
73
73
  }
74
+ // Simple markdown: **bold** → bold, keep lists/tables, wrap at word boundaries
75
+ function MarkdownText({ text, dim, width }) {
76
+ // Split by **bold** segments
77
+ const parts = [];
78
+ let last = 0;
79
+ const re = /\*\*(.+?)\*\*/g;
80
+ let m;
81
+ let idx = 0;
82
+ while ((m = re.exec(text))) {
83
+ if (m.index > last)
84
+ parts.push(_jsx(Text, { color: dim ? tokens.ansi.dim : undefined, wrap: "wrap", children: text.slice(last, m.index) }, `t-${idx++}`));
85
+ parts.push(_jsx(Text, { bold: true, color: dim ? undefined : tokens.ansi.soft, wrap: "wrap", children: m[1] }, `b-${idx++}`));
86
+ last = m.index + m[0].length;
87
+ }
88
+ if (last < text.length)
89
+ parts.push(_jsx(Text, { color: dim ? tokens.ansi.dim : undefined, wrap: "wrap", children: text.slice(last) }, `t-${idx++}`));
90
+ if (parts.length === 0)
91
+ return _jsx(Text, { color: dim ? tokens.ansi.dim : undefined, wrap: "wrap", children: text });
92
+ // Render as single line with bold segments — Ink will wrap the parent Box
93
+ return _jsx(Text, { wrap: "wrap", children: parts });
94
+ }
74
95
  export function App(props) {
75
96
  const { stdout } = useStdout();
76
97
  const [transcript, setTranscript] = useState(props.initialTranscript ?? []);
@@ -80,16 +101,15 @@ export function App(props) {
80
101
  const [plan, setPlan] = useState([]);
81
102
  const [status, setStatus] = useState({ model: props.initialModel, step: 0, maxSteps: props.maxSteps, usageInput: 0, usageOutput: 0, repairs: 0, status: 'idle', ...props.initialStatus });
82
103
  const [elapsed, setElapsed] = useState(0);
83
- const [queued, setQueued] = useState(null);
104
+ const [queuedInputs, setQueuedInputs] = useState([]);
84
105
  const [expandedGroups, setExpandedGroups] = useState(new Set());
85
106
  const [scrollOffset, setScrollOffset] = useState(0);
86
107
  const streamingIdRef = useRef(null);
87
- const placeholder = 'Message Klyro…';
88
108
  const width = stdout?.columns ?? 100;
89
109
  const height = stdout?.rows ?? 30;
90
110
  const isFullscreen = props.isFullscreen ?? false;
91
- const viewportH = Math.max(5, height - 10);
92
111
  const grouped = groupTools(transcript);
112
+ const viewportH = Math.max(5, height - 10);
93
113
  const totalRows = grouped.length + (plan.length > 0 ? 1 : 0) + 2;
94
114
  const maxOffset = Math.max(0, totalRows - viewportH);
95
115
  const isAtBottom = scrollOffset >= maxOffset;
@@ -97,17 +117,19 @@ export function App(props) {
97
117
  const thumbPos = maxOffset === 0 ? 0 : Math.round((scrollOffset / maxOffset) * (trackH - 1));
98
118
  const visibleGrouped = isFullscreen ? grouped.slice(scrollOffset, scrollOffset + viewportH) : grouped;
99
119
  useEffect(() => bridge.subscribe((p) => setAwaitingApproval(p !== null)), [bridge]);
100
- useEffect(() => { if (queued && status.status !== 'running' && !awaitingApproval) {
101
- const toSend = queued;
102
- setQueued(null);
103
- setTranscript((prev) => [...prev, { id: nextId('user'), kind: 'text', text: toSend, role: 'user' }]);
104
- streamingIdRef.current = null;
105
- const cmd = parseSlash(toSend.trim());
106
- if (cmd.kind === 'prompt')
107
- void props.onPrompt(cmd.text);
108
- else
109
- void props.onSlash(cmd);
110
- } }, [queued, status.status, awaitingApproval]);
120
+ useEffect(() => {
121
+ if (queuedInputs.length > 0 && status.status !== 'running' && !awaitingApproval) {
122
+ const toSend = queuedInputs[0];
123
+ setQueuedInputs((prev) => prev.slice(1));
124
+ setTranscript((prev) => [...prev, { id: nextId('user'), kind: 'text', text: toSend, role: 'user' }]);
125
+ streamingIdRef.current = null;
126
+ const cmd = parseSlash(toSend.trim());
127
+ if (cmd.kind === 'prompt')
128
+ void props.onPrompt(cmd.text);
129
+ else
130
+ void props.onSlash(cmd);
131
+ }
132
+ }, [queuedInputs, status.status, awaitingApproval]);
111
133
  useEffect(() => { if (status.status !== 'running')
112
134
  return; const start = Date.now() - elapsed; const t = setInterval(() => setElapsed(Date.now() - start), 1000); return () => clearInterval(t); }, [status.status, elapsed]);
113
135
  useEffect(() => { if (isAtBottom)
@@ -141,6 +163,10 @@ export function App(props) {
141
163
  const scrollUp = (n = 3) => setScrollOffset((p) => Math.max(0, p - n));
142
164
  const scrollDown = (n = 3) => setScrollOffset((p) => Math.min(maxOffset, p + n));
143
165
  useInput((inputStr, key) => {
166
+ if (key.escape && queuedInputs.length > 0) {
167
+ setQueuedInputs((prev) => prev.slice(1));
168
+ return;
169
+ }
144
170
  if (key.pageUp || (key.ctrl && inputStr === 'u')) {
145
171
  scrollUp(5);
146
172
  return;
@@ -175,9 +201,10 @@ export function App(props) {
175
201
  const v = input.trim();
176
202
  if (!v)
177
203
  return;
178
- setQueued(v);
204
+ if (queuedInputs.length >= 3)
205
+ return;
206
+ setQueuedInputs((prev) => [...prev, v]);
179
207
  setInput('');
180
- setTranscript((prev) => [...prev, { id: nextId('queued'), kind: 'text', text: `queued: ${v.slice(0, 80)}`, role: 'assistant' }]);
181
208
  return;
182
209
  }
183
210
  if (key.backspace || key.delete) {
@@ -216,9 +243,7 @@ export function App(props) {
216
243
  const ctxPct = totalTokens > 0 ? Math.round((totalTokens / 120_000) * 100) : 0;
217
244
  const baseHints = 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';
218
245
  const hints = maxOffset > 0 && isFullscreen ? `${baseHints} · PgUp/Dn scroll` : baseHints;
219
- // wrap width: leave 6 cells for guide+indent+scrollbar so words never clump
220
- const wrapW = Math.max(20, width - 6);
221
- return (_jsxs(Box, { flexDirection: "column", width: width, height: isFullscreen ? height - 1 : 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: [grouped.length === 0 ? (_jsx(Text, { color: tokens.ansi.dim, children: placeholder })) : visibleGrouped.map((item) => {
246
+ return (_jsxs(Box, { flexDirection: "column", width: width, height: isFullscreen ? height - 1 : 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: [grouped.length === 0 ? (_jsx(Text, { color: tokens.ansi.dim, children: "Message Klyro\u2026" })) : visibleGrouped.map((item) => {
222
247
  if (item.verb) {
223
248
  const gr = item;
224
249
  const isExpanded = expandedGroups.has(gr.id);
@@ -252,25 +277,45 @@ export function App(props) {
252
277
  return `${gr.verb} ${gr.items.length} items`;
253
278
  })();
254
279
  const right = gr.status === 'running' ? `${(elapsed / 1000).toFixed(1)}s` : gr.status === 'error' ? '✗' : `${gr.totalMs}ms`;
255
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { children: [isExpanded ? g('expanded') : g('collapsed'), " ", verbLine] }), _jsxs(Text, { color: tokens.ansi.dim, children: [" ", right] })] }), isExpanded ? gr.items.map((it) => (_jsxs(Box, { paddingLeft: 4, children: [_jsxs(Text, { color: tokens.ansi.guide, children: [g('end'), " "] }), _jsx(Text, { children: it.name }), _jsxs(Text, { color: tokens.ansi.dim, children: [" ", it.args.slice(0, 50)] })] }, it.id))) : null] }, gr.id));
280
+ const marker = isExpanded ? '' : '';
281
+ const markerColor = gr.status === 'error' ? tokens.ansi.err : gr.status === 'running' ? tokens.ansi.warn : tokens.ansi.ok;
282
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: markerColor, children: [marker, " ", verbLine] }), _jsxs(Text, { color: tokens.ansi.dim, children: [" ", right] })] }), isExpanded ? gr.items.map((it) => {
283
+ let friendly = '';
284
+ try {
285
+ const a = JSON.parse(it.args);
286
+ const p = a.path ?? a.pattern ?? a.command ?? '';
287
+ const short = p ? String(p).split('/').pop()?.slice(0, 40) ?? p : '';
288
+ if (it.name === 'read_file' && short)
289
+ friendly = `${short}`;
290
+ else if (it.name === 'shell_exec' && p)
291
+ friendly = `$ ${String(p).slice(0, 40)}`;
292
+ else if (short)
293
+ friendly = short;
294
+ else
295
+ friendly = it.args.slice(0, 40);
296
+ }
297
+ catch {
298
+ friendly = it.args.slice(0, 40);
299
+ }
300
+ return (_jsxs(Box, { paddingLeft: 4, children: [_jsxs(Text, { color: tokens.ansi.guide, children: [g('end'), " "] }), _jsx(Text, { color: tokens.ansi.dim, children: friendly })] }, it.id));
301
+ }) : null] }, gr.id));
256
302
  }
257
303
  const it = item;
258
304
  if (it.kind === 'text' && it.role === 'user') {
259
305
  return _jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: tokens.ansi.accent, bold: true, children: [g('prompt'), " "] }), _jsx(Text, { wrap: "wrap", children: it.text })] }, it.id);
260
306
  }
261
307
  if (it.kind === 'text') {
262
- if (it.text.startsWith('queued:'))
263
- return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " ", it.text, " esc to drop"] }) }, it.id);
264
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: tokens.ansi.accent, children: [g('agentBullet'), " Klyro"] })] }), _jsxs(Box, { paddingLeft: 2, children: [_jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " "] }), _jsx(Text, { wrap: "wrap", children: it.text })] })] }, it.id));
308
+ // prose — render markdown, not raw **, with proper wrap and guide
309
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: tokens.ansi.accent, children: [g('agentBullet'), " Klyro"] })] }), _jsx(Box, { paddingLeft: 2, flexDirection: "column", children: _jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " "] }), _jsx(Box, { flexGrow: 1, children: _jsx(MarkdownText, { text: it.text }) })] }) })] }, it.id));
265
310
  }
266
311
  if (it.kind === 'error')
267
312
  return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.ansi.err, children: [" ", g('guide'), " \u2717 ", it.message] }) }, it.id);
268
313
  if (it.kind === 'policy')
269
- return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " [policy] ", it.action, " ", it.name] }) }, it.id);
314
+ return null;
270
315
  if (it.kind === 'file_changed')
271
316
  return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " ", g('editsBadge'), " ", it.path, " ", it.op] }) }, it.id);
272
317
  if (it.kind === 'diff')
273
318
  return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, color: tokens.ansi.soft, children: it.summary ?? 'Diff' }), it.hunks.map((h, i) => (_jsxs(Box, { flexDirection: "column", marginTop: 0, children: [_jsx(Text, { color: tokens.ansi.soft, children: h.path }), h.lines.map((l, j) => (_jsxs(Text, { wrap: "wrap", color: l.kind === 'add' ? tokens.ansi.ok : l.kind === 'remove' ? tokens.ansi.err : tokens.ansi.dim, children: [l.kind === 'add' ? '+ ' : l.kind === 'remove' ? '- ' : ' ', l.text] }, j)))] }, i)))] }, it.id));
274
319
  return null;
275
- }), status.status === 'running' && !streamingIdRef.current ? (_jsxs(Box, { paddingLeft: 2, marginBottom: 1, children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('guide'), " "] }), _jsx(Text, { color: tokens.ansi.dim, children: "Thinking..." }), _jsxs(Text, { color: tokens.ansi.dim, children: [" ", (elapsed / 1000).toFixed(1), "s"] })] })) : null, plan.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginTop: 0, marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { bold: true, children: [g('todoPlan'), " Plan ", plan.filter((p) => p.status === 'done').length, "/", plan.length] })] }), plan.slice(0, 8).map((p) => (_jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: p.status === 'done' ? tokens.ansi.ok : p.status === 'in_progress' ? tokens.ansi.accent : tokens.ansi.dim, children: [p.status === 'done' ? g('todoDone') : p.status === 'in_progress' ? g('todoActive') : g('todoPending'), " ", p.title] })] }, p.id)))] })) : null] }), isFullscreen ? (_jsx(Box, { flexDirection: "column", width: 1, marginLeft: 1, children: Array.from({ length: trackH }).map((_, i) => (_jsx(Text, { color: i === thumbPos ? tokens.ansi.accent : tokens.ansi.guide, children: i === thumbPos ? '●' : '│' }, i))) })) : null] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: tokens.ansi.guide, children: rule }), _jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.accent, bold: true, children: [g('prompt'), " "] }), _jsxs(Text, { wrap: "wrap", children: [input || _jsx(Text, { color: tokens.ansi.dim, children: placeholder }), "\u258F"] })] }), _jsx(Text, { color: tokens.ansi.guide, children: rule })] }), _jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: tokens.ansi.dim, children: hints }), _jsxs(Text, { color: tokens.ansi.dim, children: [cost > 0 ? `$${cost.toFixed(2)} · ` : '', ctxPct > 0 ? `${ctxPct}% ctx · ` : '', status.status === 'running' ? 'auto mode on ●' : ''] })] })] }));
320
+ }), status.status === 'running' && !streamingIdRef.current ? (_jsxs(Box, { paddingLeft: 2, marginBottom: 1, children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('guide'), " "] }), _jsx(Text, { color: tokens.ansi.dim, children: "Thinking..." }), _jsxs(Text, { color: tokens.ansi.dim, children: [" ", (elapsed / 1000).toFixed(1), "s"] })] })) : null, plan.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginTop: 0, marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { bold: true, children: [g('todoPlan'), " Plan ", plan.filter((p) => p.status === 'done').length, "/", plan.length] })] }), plan.slice(0, 8).map((p) => (_jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: p.status === 'done' ? tokens.ansi.ok : p.status === 'in_progress' ? tokens.ansi.accent : tokens.ansi.dim, children: [p.status === 'done' ? g('todoDone') : p.status === 'in_progress' ? g('todoActive') : g('todoPending'), " ", p.title] })] }, p.id)))] })) : null, queuedInputs.length > 0 ? (_jsx(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: queuedInputs.map((q, i) => (_jsxs(Text, { color: tokens.ansi.dim, children: ["queued: ", q.slice(0, 60), i === 0 ? ' esc to drop' : ''] }, i))) })) : null] }), isFullscreen ? (_jsx(Box, { flexDirection: "column", width: 1, marginLeft: 1, children: Array.from({ length: trackH }).map((_, i) => (_jsx(Text, { color: i === thumbPos ? tokens.ansi.accent : tokens.ansi.guide, children: i === thumbPos ? '●' : '│' }, i))) })) : null] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: tokens.ansi.guide, children: g('rule').repeat(Math.max(10, width - 2)) }), _jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.accent, bold: true, children: [g('prompt'), " "] }), _jsxs(Text, { wrap: "wrap", children: [input || _jsx(Text, { color: tokens.ansi.dim, children: "Message Klyro\u2026" }), "\u258F"] })] }), _jsx(Text, { color: tokens.ansi.guide, children: g('rule').repeat(Math.max(10, width - 2)) })] }), _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: tokens.ansi.dim, children: [baseHints, maxOffset > 0 && isFullscreen ? ' · PgUp/Dn scroll' : ''] }), _jsxs(Text, { color: tokens.ansi.dim, children: [cost > 0 ? `$${cost.toFixed(2)} · ` : '', ctxPct > 0 ? `${ctxPct}% ctx · ` : '', status.status === 'running' ? 'auto mode on ●' : ''] })] })] }));
276
321
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
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",