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.
Files changed (2) hide show
  1. package/dist/tui/app.js +84 -57
  2. 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.19';
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 hints = 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';
225
- // Grouped transcript
226
- const grouped = groupTools(transcript);
227
- 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: "column", flexGrow: 1, overflow: "hidden", paddingX: 0, children: [grouped.length === 0 ? (_jsx(Text, { color: tokens.ansi.dim, children: placeholder })) : grouped.map((item) => {
228
- if (item.verb) {
229
- const gr = item;
230
- const isExpanded = expandedGroups.has(gr.id);
231
- const marker = isExpanded ? g('expanded') : g('collapsed');
232
- const verbLine = (() => {
233
- if (gr.items.length === 1) {
234
- const it = gr.items[0];
235
- const primary = (() => {
236
- try {
237
- const a = JSON.parse(it.args);
238
- return a.path ?? a.pattern ?? a.command?.slice(0, 48) ?? '';
239
- }
240
- catch {
241
- return '';
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 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}`;
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 (gr.verb === 'Read')
248
- return `Read ${gr.items.length} files`;
249
- if (gr.verb === 'Searched')
250
- return `Searched ${gr.items.length} patterns`;
251
- if (gr.verb === 'Ran')
252
- return `Ran ${gr.items.length} commands`;
253
- if (gr.verb === 'Edited' || gr.verb === 'Created')
254
- return `${gr.verb} ${gr.items.length} files`;
255
- return `${gr.verb} ${gr.items.length} items`;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
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",