klyro 0.1.24 → 0.1.26
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.js +84 -57
- package/package.json +1 -1
package/dist/tui/app.js
CHANGED
|
@@ -89,6 +89,7 @@ export function App(props) {
|
|
|
89
89
|
const [elapsed, setElapsed] = useState(0);
|
|
90
90
|
const [queued, setQueued] = useState(null);
|
|
91
91
|
const [expandedGroups, setExpandedGroups] = useState(new Set());
|
|
92
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
92
93
|
const streamingIdRef = useRef(null);
|
|
93
94
|
const placeholders = ['Message Klyro…', 'Message @file to attach…', 'Type / for commands…', '! runs a shell command…'];
|
|
94
95
|
const placeholder = placeholders[0] ?? 'Message Klyro…';
|
|
@@ -158,11 +159,38 @@ export function App(props) {
|
|
|
158
159
|
n.delete(id);
|
|
159
160
|
else
|
|
160
161
|
n.add(id); return n; });
|
|
162
|
+
const width = stdout?.columns ?? 100;
|
|
163
|
+
const height = stdout?.rows ?? 30;
|
|
164
|
+
const grouped = groupTools(transcript);
|
|
165
|
+
const viewportH = Math.max(5, height - 10);
|
|
166
|
+
const totalRows = grouped.length + (plan.length > 0 ? 1 : 0) + 2;
|
|
167
|
+
const maxOffset = Math.max(0, totalRows - viewportH);
|
|
168
|
+
const isAtBottom = scrollOffset >= maxOffset;
|
|
169
|
+
useEffect(() => { if (isAtBottom)
|
|
170
|
+
setScrollOffset(maxOffset); }, [transcript.length, plan.length, maxOffset, isAtBottom]);
|
|
171
|
+
const scrollUp = (n = 3) => setScrollOffset((p) => Math.max(0, p - n));
|
|
172
|
+
const scrollDown = (n = 3) => setScrollOffset((p) => Math.min(maxOffset, p + n));
|
|
161
173
|
useInput((inputStr, key) => {
|
|
174
|
+
// slider scroll: PageUp/PageDown, Ctrl+U/D, Shift+↑/↓, j/k in vim-like
|
|
175
|
+
if (key.pageUp || (key.ctrl && inputStr === 'u')) {
|
|
176
|
+
scrollUp(5);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (key.pageDown || (key.ctrl && inputStr === 'd')) {
|
|
180
|
+
scrollDown(5);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (key.upArrow && (key.shift || key.ctrl)) {
|
|
184
|
+
scrollUp(1);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (key.downArrow && (key.shift || key.ctrl)) {
|
|
188
|
+
scrollDown(1);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
162
191
|
if (awaitingApproval)
|
|
163
192
|
return;
|
|
164
193
|
if (key.ctrl && inputStr === 'o') {
|
|
165
|
-
// Ctrl+O toggle most recent group in live window
|
|
166
194
|
const groups = groupTools(transcript).filter((x) => typeof x.verb === 'string');
|
|
167
195
|
const last = groups[groups.length - 1];
|
|
168
196
|
if (last)
|
|
@@ -212,69 +240,68 @@ export function App(props) {
|
|
|
212
240
|
if (!key.ctrl && !key.meta)
|
|
213
241
|
setInput((v) => v + inputStr);
|
|
214
242
|
});
|
|
215
|
-
const width = stdout?.columns ?? 100;
|
|
216
|
-
const height = stdout?.rows ?? 30;
|
|
217
243
|
const isSmall = width < 80;
|
|
218
|
-
const ver = props.version ?? '0.1.
|
|
244
|
+
const ver = props.version ?? '0.1.24';
|
|
219
245
|
const rule = g('rule').repeat(Math.max(10, width - 2));
|
|
220
246
|
// Derived status right
|
|
221
247
|
const cost = (status.usageInput / 1000 * 0.003 + status.usageOutput / 1000 * 0.015);
|
|
222
248
|
const totalTokens = status.usageInput + status.usageOutput;
|
|
223
249
|
const ctxPct = totalTokens > 0 ? Math.round((totalTokens / 120_000) * 100) : 0;
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
250
|
+
const baseHints = status.status === 'running' ? 'ctrl+c to stop · enter to queue · ctrl+o expand' : status.status === 'idle' && transcript.length === 0 ? 'shift+tab to cycle · ↑↓ for history · / for commands' : 'enter to send · shift+enter newline · @ to attach';
|
|
251
|
+
const hints = maxOffset > 0 ? `${baseHints} · PgUp/Dn scroll` : baseHints;
|
|
252
|
+
const visibleGrouped = grouped.slice(scrollOffset, scrollOffset + viewportH);
|
|
253
|
+
const trackH = viewportH;
|
|
254
|
+
const thumbPos = maxOffset === 0 ? 0 : Math.round((scrollOffset / maxOffset) * (trackH - 1));
|
|
255
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height - 1, children: [_jsx(Header, { cwd: props.cwd, model: status.model, version: ver, width: width }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", paddingX: 0, children: [grouped.length === 0 ? (_jsx(Text, { color: tokens.ansi.dim, children: placeholder })) : visibleGrouped.map((item) => {
|
|
256
|
+
if (item.verb) {
|
|
257
|
+
const gr = item;
|
|
258
|
+
const isExpanded = expandedGroups.has(gr.id);
|
|
259
|
+
const marker = isExpanded ? g('expanded') : g('collapsed');
|
|
260
|
+
const verbLine = (() => {
|
|
261
|
+
if (gr.items.length === 1) {
|
|
262
|
+
const it = gr.items[0];
|
|
263
|
+
const primary = (() => {
|
|
264
|
+
try {
|
|
265
|
+
const a = JSON.parse(it.args);
|
|
266
|
+
return a.path ?? a.pattern ?? a.command?.slice(0, 48) ?? '';
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return '';
|
|
270
|
+
}
|
|
271
|
+
})();
|
|
272
|
+
const name = gr.verb === 'Read' && primary ? `Read ${primary.split('/').pop()}` : gr.verb === 'Searched' && primary ? `Searched "${primary}"` : gr.verb === 'Ran' && primary ? `Ran ${primary.split(' ')[0]}` : `${gr.verb} ${primary}`;
|
|
273
|
+
return name;
|
|
242
274
|
}
|
|
275
|
+
if (gr.verb === 'Read')
|
|
276
|
+
return `Read ${gr.items.length} files`;
|
|
277
|
+
if (gr.verb === 'Searched')
|
|
278
|
+
return `Searched ${gr.items.length} patterns`;
|
|
279
|
+
if (gr.verb === 'Ran')
|
|
280
|
+
return `Ran ${gr.items.length} commands`;
|
|
281
|
+
if (gr.verb === 'Edited' || gr.verb === 'Created')
|
|
282
|
+
return `${gr.verb} ${gr.items.length} files`;
|
|
283
|
+
return `${gr.verb} ${gr.items.length} items`;
|
|
243
284
|
})();
|
|
244
|
-
const
|
|
245
|
-
return name;
|
|
285
|
+
const right = gr.status === 'running' ? `${(elapsed / 1000).toFixed(1)}s` : gr.status === 'error' ? '✗' : `${gr.totalMs}ms`;
|
|
286
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: gr.status === 'running' ? tokens.ansi.warn : undefined, children: [isExpanded ? g('expanded') : g('collapsed'), " ", verbLine] }), _jsxs(Text, { color: tokens.ansi.dim, children: [" ", right] })] }), isExpanded ? gr.items.map((it) => (_jsxs(Box, { paddingLeft: 4, marginTop: 0, children: [_jsxs(Text, { color: tokens.ansi.guide, children: [g('end'), " "] }), _jsx(Text, { color: it.isError ? tokens.ansi.err : undefined, children: it.name }), _jsxs(Text, { color: tokens.ansi.dim, children: [" ", it.args.slice(0, 60)] })] }, it.id))) : null] }, gr.id));
|
|
287
|
+
}
|
|
288
|
+
const it = item;
|
|
289
|
+
if (it.kind === 'text' && it.role === 'user') {
|
|
290
|
+
return _jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: tokens.ansi.accent, bold: true, children: [g('prompt'), " "] }), _jsx(Text, { children: it.text })] }, it.id);
|
|
291
|
+
}
|
|
292
|
+
if (it.kind === 'text') {
|
|
293
|
+
if (it.text.startsWith('queued:'))
|
|
294
|
+
return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " ", it.text, " esc to drop"] }) }, it.id);
|
|
295
|
+
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, marginTop: 0, children: [_jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " "] }), _jsx(Text, { wrap: "wrap", children: it.text })] })] }, it.id));
|
|
246
296
|
}
|
|
247
|
-
if (
|
|
248
|
-
return
|
|
249
|
-
if (
|
|
250
|
-
return
|
|
251
|
-
if (
|
|
252
|
-
return
|
|
253
|
-
if (
|
|
254
|
-
return
|
|
255
|
-
return
|
|
256
|
-
})();
|
|
257
|
-
const right = gr.status === 'running' ? `${(elapsed / 1000).toFixed(1)}s` : gr.status === 'error' ? '✗' : `${gr.totalMs}ms`;
|
|
258
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: gr.status === 'running' ? tokens.ansi.warn : undefined, children: [isExpanded ? g('expanded') : g('collapsed'), " ", verbLine] }), _jsxs(Text, { color: tokens.ansi.dim, children: [" ", right] })] }), isExpanded ? gr.items.map((it) => (_jsxs(Box, { paddingLeft: 2, children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('end'), " "] }), _jsxs(Text, { color: it.isError ? tokens.ansi.err : tokens.ansi.dim, children: [it.name, " ", it.args.slice(0, 80)] }), it.result ? _jsxs(Text, { color: tokens.ansi.dim, children: [" \u00B7 ", String(it.result).slice(0, 80)] }) : null] }, it.id))) : null] }, gr.id));
|
|
259
|
-
}
|
|
260
|
-
const it = item;
|
|
261
|
-
if (it.kind === 'text' && it.role === 'user') {
|
|
262
|
-
return _jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.accent, bold: true, children: [g('prompt'), " "] }), _jsx(Text, { children: it.text })] }, it.id);
|
|
263
|
-
}
|
|
264
|
-
if (it.kind === 'text') {
|
|
265
|
-
// Check if it's queued indicator
|
|
266
|
-
if (it.text.startsWith('queued:'))
|
|
267
|
-
return _jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " ", it.text, " esc to drop"] }) }, it.id);
|
|
268
|
-
return (_jsxs(Box, { flexDirection: "column", 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, { children: it.text })] })] }, it.id));
|
|
269
|
-
}
|
|
270
|
-
if (it.kind === 'error')
|
|
271
|
-
return _jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { color: tokens.ansi.err, children: [" ", g('guide'), " \u2717 ", it.message] }) }, it.id);
|
|
272
|
-
if (it.kind === 'policy')
|
|
273
|
-
return _jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " [policy] ", it.action, " ", it.name] }) }, it.id);
|
|
274
|
-
if (it.kind === 'file_changed')
|
|
275
|
-
return _jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " ", g('editsBadge'), " ", it.path, " ", it.op] }) }, it.id);
|
|
276
|
-
if (it.kind === 'diff')
|
|
277
|
-
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [_jsx(Text, { bold: true, children: it.summary ?? 'Diff' }), it.hunks.map((h, i) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: tokens.ansi.soft, children: h.path }), h.lines.map((l, j) => (_jsxs(Text, { 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));
|
|
278
|
-
return null;
|
|
279
|
-
}), status.status === 'running' && !streamingIdRef.current ? (_jsxs(Box, { paddingLeft: 2, 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: 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))), plan.length > 8 ? _jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " \u2026 +", plan.length - 8, " more (/todos)"] }) : null] })) : null, status.status === 'done' && transcript.some((x) => x.kind === 'file_changed') ? (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " ", g('editsBadge'), " ", transcript.filter((x) => x.kind === 'file_changed').length, " files \u00B7 /diff"] }) })) : 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, { 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 ●' : ''] })] })] }));
|
|
297
|
+
if (it.kind === 'error')
|
|
298
|
+
return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.ansi.err, children: [" ", g('guide'), " \u2717 ", it.message] }) }, it.id);
|
|
299
|
+
if (it.kind === 'policy')
|
|
300
|
+
return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " [policy] ", it.action, " ", it.name] }) }, it.id);
|
|
301
|
+
if (it.kind === 'file_changed')
|
|
302
|
+
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);
|
|
303
|
+
if (it.kind === 'diff')
|
|
304
|
+
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));
|
|
305
|
+
return null;
|
|
306
|
+
}), 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))), plan.length > 8 ? _jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " \u2026 +", plan.length - 8, " more (/todos)"] }) : null] })) : null, status.status === 'done' && transcript.some((x) => x.kind === 'file_changed') ? (_jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " ", g('editsBadge'), " ", transcript.filter((x) => x.kind === 'file_changed').length, " files \u00B7 /diff"] }) })) : null] }), _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))) })] }), _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, { 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 ●' : ''] })] })] }));
|
|
280
307
|
}
|