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 +2 -2
- package/dist/tui/app.js +72 -27
- package/package.json +1 -1
package/dist/tui/app.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Klyro TUI — opencode-
|
|
3
|
-
*
|
|
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-
|
|
4
|
-
*
|
|
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 [
|
|
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(() => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
|
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:
|
|
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
|
}
|