klyro 0.1.27 → 0.1.28

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,7 +1,6 @@
1
1
  /**
2
- * Klyro TUI — TUI_DESIGN.md §3-7,10-12 Professional monochrome + one accent orange #E8843C
3
- * No boxes, no borders (§24). Guide at col2, accent ● at col4 for agent.
4
- * Regions: scrollback (header+turns) / live window (streaming tail+groups) / pinned (input+status)
2
+ * Klyro TUI — opencode-perfect — TUI_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 │●.
5
4
  */
6
5
  import React from 'react';
7
6
  import type { StatusSnapshot } from './status.js';
package/dist/tui/app.js CHANGED
@@ -1,8 +1,7 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  /**
3
- * Klyro TUI — TUI_DESIGN.md §3-7,10-12 Professional monochrome + one accent orange #E8843C
4
- * No boxes, no borders (§24). Guide at col2, accent ● at col4 for agent.
5
- * Regions: scrollback (header+turns) / live window (streaming tail+groups) / pinned (input+status)
3
+ * Klyro TUI — opencode-perfect — TUI_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 │●.
6
5
  */
7
6
  import { useState, useEffect, useRef, useCallback } from 'react';
8
7
  import { Box, Text, useInput, useStdout } from 'ink';
@@ -11,7 +10,6 @@ import { parse as parseSlash } from '../cli/slash/parser.js';
11
10
  import { tokens, g } from './tokens.js';
12
11
  let _id = 0;
13
12
  function nextId(p) { _id++; return `${p}-${_id}`; }
14
- // ── Header §4 ───────────────────────────────────────────────────────────────
15
13
  function Header({ cwd, model, version, width }) {
16
14
  const branch = (() => { try {
17
15
  const { execSync } = require('node:child_process');
@@ -21,29 +19,26 @@ function Header({ cwd, model, version, width }) {
21
19
  return '';
22
20
  } })();
23
21
  const showLinks = width >= 120;
24
- const links = '│ /help /config /clear /exit';
25
- const ctxShort = '200k'; // TODO wire real context window
26
- const row1Left = `KLYRO v${version}`;
27
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: tokens.ansi.accent, children: row1Left }), showLinks ? _jsx(Text, { color: tokens.ansi.dim, children: links }) : null] }), _jsxs(Text, { color: tokens.ansi.dim, children: [model, "[", ctxShort, "] \u00B7 API Usage Billing"] }), _jsxs(Text, { color: tokens.ansi.dim, children: [cwd, branch ? ` · ${branch}` : ''] })] }));
22
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { bold: true, color: tokens.ansi.accent, children: ["KLYRO v", version] }), showLinks ? _jsx(Text, { color: tokens.ansi.dim, children: "\u2502 /help /config /clear /exit" }) : null] }), _jsxs(Text, { color: tokens.ansi.dim, children: [model, "[200k] \u00B7 API Usage Billing"] }), _jsxs(Text, { color: tokens.ansi.dim, children: [cwd, branch ? ` · ${branch}` : ''] })] }));
28
23
  }
29
24
  function verbForTool(name) {
30
25
  if (name === 'read_file')
31
- return { verb: 'Read', plural: 'Read' };
26
+ return 'Read';
32
27
  if (name === 'list_directory')
33
- return { verb: 'Listed', plural: 'Listed' };
28
+ return 'Listed';
34
29
  if (name === 'grep' || name === 'glob' || name === 'find_files' || name === 'search_files' || name === 'recent_files')
35
- return { verb: 'Searched', plural: 'Searched' };
30
+ return 'Searched';
36
31
  if (name === 'shell_exec')
37
- return { verb: 'Ran', plural: 'Ran' };
32
+ return 'Ran';
38
33
  if (name.startsWith('git_'))
39
- return { verb: 'Checked git', plural: 'Checked git' };
34
+ return 'Checked git';
40
35
  if (name === 'web_fetch')
41
- return { verb: 'Fetched', plural: 'Fetched' };
36
+ return 'Fetched';
42
37
  if (name === 'web_search')
43
- return { verb: 'Searched web', plural: 'Searched web' };
38
+ return 'Searched web';
44
39
  if (name === 'edit_file' || name === 'multi_edit' || name === 'apply_patch' || name === 'write_file')
45
- return { verb: 'Edited', plural: 'Edited' };
46
- return { verb: 'Called', plural: 'Called' };
40
+ return 'Edited';
41
+ return 'Called';
47
42
  }
48
43
  function groupTools(items) {
49
44
  const out = [];
@@ -51,10 +46,9 @@ function groupTools(items) {
51
46
  const flush = () => {
52
47
  if (cur.length === 0)
53
48
  return;
54
- // bucket by verb
55
49
  const byVerb = new Map();
56
50
  for (const it of cur) {
57
- const v = verbForTool(it.name).verb;
51
+ const v = verbForTool(it.name);
58
52
  if (!byVerb.has(v))
59
53
  byVerb.set(v, []);
60
54
  byVerb.get(v).push(it);
@@ -77,7 +71,6 @@ function groupTools(items) {
77
71
  flush();
78
72
  return out;
79
73
  }
80
- // ── Main App §3 ────────────────────────────────────────────────────────────
81
74
  export function App(props) {
82
75
  const { stdout } = useStdout();
83
76
  const [transcript, setTranscript] = useState(props.initialTranscript ?? []);
@@ -91,50 +84,43 @@ export function App(props) {
91
84
  const [expandedGroups, setExpandedGroups] = useState(new Set());
92
85
  const [scrollOffset, setScrollOffset] = useState(0);
93
86
  const streamingIdRef = useRef(null);
94
- const placeholders = ['Message Klyro…', 'Message @file to attach…', 'Type / for commands…', '! runs a shell command…'];
95
- const placeholder = placeholders[0] ?? 'Message Klyro…';
87
+ const placeholder = 'Message Klyro…';
88
+ const width = stdout?.columns ?? 100;
89
+ const height = stdout?.rows ?? 30;
90
+ const isFullscreen = props.isFullscreen ?? false;
91
+ const viewportH = Math.max(5, height - 10);
92
+ const grouped = groupTools(transcript);
93
+ const totalRows = grouped.length + (plan.length > 0 ? 1 : 0) + 2;
94
+ const maxOffset = Math.max(0, totalRows - viewportH);
95
+ const isAtBottom = scrollOffset >= maxOffset;
96
+ const trackH = viewportH;
97
+ const thumbPos = maxOffset === 0 ? 0 : Math.round((scrollOffset / maxOffset) * (trackH - 1));
98
+ const visibleGrouped = isFullscreen ? grouped.slice(scrollOffset, scrollOffset + viewportH) : grouped;
96
99
  useEffect(() => bridge.subscribe((p) => setAwaitingApproval(p !== null)), [bridge]);
97
- useEffect(() => {
98
- if (queued && status.status !== 'running' && !awaitingApproval) {
99
- const toSend = queued;
100
- setQueued(null);
101
- setTranscript((prev) => [...prev, { id: nextId('user'), kind: 'text', text: toSend, role: 'user' }]);
102
- streamingIdRef.current = null;
103
- const cmd = parseSlash(toSend.trim());
104
- if (cmd.kind === 'prompt')
105
- void props.onPrompt(cmd.text);
106
- else
107
- void props.onSlash(cmd);
108
- }
109
- }, [queued, status.status, awaitingApproval]);
110
- useEffect(() => {
111
- if (status.status !== 'running')
112
- return;
113
- const start = Date.now() - elapsed;
114
- const t = setInterval(() => setElapsed(Date.now() - start), 1000);
115
- return () => clearInterval(t);
116
- }, [status.status, elapsed]);
117
- const append = useCallback((item) => {
118
- if (item.kind !== 'text' || item.role !== 'assistant')
119
- streamingIdRef.current = null;
120
- setTranscript((prev) => [...prev, item]);
121
- }, []);
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]);
111
+ useEffect(() => { if (status.status !== 'running')
112
+ return; const start = Date.now() - elapsed; const t = setInterval(() => setElapsed(Date.now() - start), 1000); return () => clearInterval(t); }, [status.status, elapsed]);
113
+ useEffect(() => { if (isAtBottom)
114
+ setScrollOffset(maxOffset); }, [transcript.length, plan.length, maxOffset, isAtBottom]);
115
+ const append = useCallback((item) => { if (item.kind !== 'text' || item.role !== 'assistant')
116
+ streamingIdRef.current = null; setTranscript((prev) => [...prev, item]); }, []);
122
117
  const appendDelta = useCallback((text) => {
123
118
  if (!text)
124
119
  return;
125
120
  const sid = streamingIdRef.current;
126
- if (sid) {
127
- setTranscript((prev) => {
128
- const idx = prev.findIndex((x) => x.id === sid);
129
- if (idx === -1)
130
- return [...prev, { id: sid, kind: 'text', text, role: 'assistant' }];
131
- const cur = prev[idx];
132
- const next = { ...cur, text: cur.text + text };
133
- const copy = [...prev];
134
- copy[idx] = next;
135
- return copy;
136
- });
137
- }
121
+ if (sid)
122
+ setTranscript((prev) => { const idx = prev.findIndex((x) => x.id === sid); if (idx === -1)
123
+ return [...prev, { id: sid, kind: 'text', text, role: 'assistant' }]; const cur = prev[idx]; const copy = [...prev]; copy[idx] = { ...cur, text: cur.text + text }; return copy; });
138
124
  else {
139
125
  const id = nextId('stream');
140
126
  streamingIdRef.current = id;
@@ -147,32 +133,14 @@ export function App(props) {
147
133
  const updatePlan = useCallback((p) => setPlan(p), []);
148
134
  const onMountedRef = useRef(props.onMounted);
149
135
  useEffect(() => { onMountedRef.current = props.onMounted; }, [props.onMounted]);
150
- useEffect(() => {
151
- onMountedRef.current?.({ append, appendDelta, updateStatus, updatePlan });
152
- globalThis.__klyroAppAppend = append;
153
- globalThis.__klyroAppendDelta = appendDelta;
154
- globalThis.__klyroAppStatus = updateStatus;
155
- globalThis.__klyroAppPlan = updatePlan;
156
- return () => { delete globalThis.__klyroAppAppend; delete globalThis.__klyroAppendDelta; delete globalThis.__klyroAppStatus; delete globalThis.__klyroAppPlan; };
157
- }, [append, appendDelta, updateStatus, updatePlan]);
136
+ useEffect(() => { onMountedRef.current?.({ append, appendDelta, updateStatus, updatePlan }); globalThis.__klyroAppAppend = append; globalThis.__klyroAppendDelta = appendDelta; globalThis.__klyroAppStatus = updateStatus; globalThis.__klyroAppPlan = updatePlan; return () => { delete globalThis.__klyroAppAppend; delete globalThis.__klyroAppendDelta; delete globalThis.__klyroAppStatus; delete globalThis.__klyroAppPlan; }; }, [append, appendDelta, updateStatus, updatePlan]);
158
137
  const toggleGroup = (id) => setExpandedGroups((prev) => { const n = new Set(prev); if (n.has(id))
159
138
  n.delete(id);
160
139
  else
161
140
  n.add(id); return n; });
162
- const width = stdout?.columns ?? 100;
163
- const height = stdout?.rows ?? 30;
164
- const isFullscreen = props.isFullscreen ?? false;
165
- const grouped = groupTools(transcript);
166
- const viewportH = Math.max(5, height - 10);
167
- const totalRows = grouped.length + (plan.length > 0 ? 1 : 0) + 2;
168
- const maxOffset = Math.max(0, totalRows - viewportH);
169
- const isAtBottom = scrollOffset >= maxOffset;
170
- useEffect(() => { if (isAtBottom)
171
- setScrollOffset(maxOffset); }, [transcript.length, plan.length, maxOffset, isAtBottom]);
172
141
  const scrollUp = (n = 3) => setScrollOffset((p) => Math.max(0, p - n));
173
142
  const scrollDown = (n = 3) => setScrollOffset((p) => Math.min(maxOffset, p + n));
174
143
  useInput((inputStr, key) => {
175
- // slider scroll: PageUp/PageDown, Ctrl+U/D, Shift+↑/↓, j/k in vim-like
176
144
  if (key.pageUp || (key.ctrl && inputStr === 'u')) {
177
145
  scrollUp(5);
178
146
  return;
@@ -192,7 +160,7 @@ export function App(props) {
192
160
  if (awaitingApproval)
193
161
  return;
194
162
  if (key.ctrl && inputStr === 'o') {
195
- const groups = groupTools(transcript).filter((x) => typeof x.verb === 'string');
163
+ const groups = grouped.filter((x) => typeof x.verb === 'string');
196
164
  const last = groups[groups.length - 1];
197
165
  if (last)
198
166
  toggleGroup(last.id);
@@ -241,37 +209,37 @@ export function App(props) {
241
209
  if (!key.ctrl && !key.meta)
242
210
  setInput((v) => v + inputStr);
243
211
  });
244
- const isSmall = width < 80;
245
- const ver = props.version ?? '0.1.24';
212
+ const ver = props.version ?? '0.1.27';
246
213
  const rule = g('rule').repeat(Math.max(10, width - 2));
247
- // Derived status right
248
214
  const cost = (status.usageInput / 1000 * 0.003 + status.usageOutput / 1000 * 0.015);
249
215
  const totalTokens = status.usageInput + status.usageOutput;
250
216
  const ctxPct = totalTokens > 0 ? Math.round((totalTokens / 120_000) * 100) : 0;
251
- 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';
217
+ 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';
252
218
  const hints = maxOffset > 0 && isFullscreen ? `${baseHints} · PgUp/Dn scroll` : baseHints;
253
- const visibleGrouped = isFullscreen ? grouped.slice(scrollOffset, scrollOffset + viewportH) : grouped;
254
- const trackH = viewportH;
255
- const thumbPos = maxOffset === 0 ? 0 : Math.round((scrollOffset / maxOffset) * (trackH - 1));
219
+ // wrap width: leave 6 cells for guide+indent+scrollbar so words never clump
220
+ const wrapW = Math.max(20, width - 6);
256
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) => {
257
222
  if (item.verb) {
258
223
  const gr = item;
259
224
  const isExpanded = expandedGroups.has(gr.id);
260
- const marker = isExpanded ? g('expanded') : g('collapsed');
261
225
  const verbLine = (() => {
262
226
  if (gr.items.length === 1) {
263
227
  const it = gr.items[0];
264
- const primary = (() => {
265
- try {
266
- const a = JSON.parse(it.args);
267
- return a.path ?? a.pattern ?? a.command?.slice(0, 48) ?? '';
268
- }
269
- catch {
270
- return '';
271
- }
272
- })();
273
- 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}`;
274
- return name;
228
+ let p = '';
229
+ try {
230
+ const a = JSON.parse(it.args);
231
+ p = a.path ?? a.pattern ?? a.command?.slice(0, 48) ?? '';
232
+ }
233
+ catch {
234
+ p = '';
235
+ }
236
+ if (gr.verb === 'Read' && p)
237
+ return `Read ${p.split('/').pop()}`;
238
+ if (gr.verb === 'Searched' && p)
239
+ return `Searched "${p}"`;
240
+ if (gr.verb === 'Ran' && p)
241
+ return `Ran ${p.split(' ')[0]}`;
242
+ return `${gr.verb} ${p}`;
275
243
  }
276
244
  if (gr.verb === 'Read')
277
245
  return `Read ${gr.items.length} files`;
@@ -279,21 +247,21 @@ export function App(props) {
279
247
  return `Searched ${gr.items.length} patterns`;
280
248
  if (gr.verb === 'Ran')
281
249
  return `Ran ${gr.items.length} commands`;
282
- if (gr.verb === 'Edited' || gr.verb === 'Created')
283
- return `${gr.verb} ${gr.items.length} files`;
250
+ if (gr.verb === 'Edited')
251
+ return `Edited ${gr.items.length} files`;
284
252
  return `${gr.verb} ${gr.items.length} items`;
285
253
  })();
286
254
  const right = gr.status === 'running' ? `${(elapsed / 1000).toFixed(1)}s` : gr.status === 'error' ? '✗' : `${gr.totalMs}ms`;
287
- 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));
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));
288
256
  }
289
257
  const it = item;
290
258
  if (it.kind === 'text' && it.role === 'user') {
291
- return _jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: tokens.ansi.accent, bold: true, children: [g('prompt'), " "] }), _jsx(Text, { children: it.text })] }, it.id);
259
+ 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);
292
260
  }
293
261
  if (it.kind === 'text') {
294
262
  if (it.text.startsWith('queued:'))
295
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);
296
- 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));
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));
297
265
  }
298
266
  if (it.kind === 'error')
299
267
  return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.ansi.err, children: [" ", g('guide'), " \u2717 ", it.message] }) }, it.id);
@@ -304,5 +272,5 @@ export function App(props) {
304
272
  if (it.kind === 'diff')
305
273
  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));
306
274
  return null;
307
- }), 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] }), 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, { 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 ●' : ''] })] })] }));
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 ●' : ''] })] })] }));
308
276
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
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",