klyro 0.1.26 → 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/cli/repl.js CHANGED
@@ -106,9 +106,11 @@ export async function startRepl(opts = {}) {
106
106
  pendingQueue.push({ kind: 'plan', plan: p });
107
107
  }
108
108
  // ── Full-screen takeover like OpenCode/Claude Code (§3) ──
109
- // When useTui, enter alternate screen so `klyro` owns the whole terminal.
110
- // Ctrl+C (SIGINT) + /quit both leave the alt screen and restore the shell like opencode.
111
- const isAltScreen = useTui && !!process.stdout.isTTY;
109
+ // Detect alt-screen support (§19.5): Windows Terminal (WT_SESSION), VS Code, xterm, or non-Windows.
110
+ // Fallback to inline (native scroll) on conhost/mintty so text isn't clipped degrade don't break (§1.8).
111
+ const supportsAlt = !!process.env.WT_SESSION || process.env.TERM_PROGRAM === 'vscode' || (process.env.TERM ?? '').includes('xterm') || process.platform !== 'win32' || process.env.KLYRO_ALT_SCREEN === '1';
112
+ const noAlt = process.env.KLYRO_NO_ALT === '1' || process.env.NO_ALT === '1';
113
+ const isAltScreen = useTui && !!process.stdout.isTTY && supportsAlt && !noAlt;
112
114
  const enterAlt = () => {
113
115
  if (!isAltScreen)
114
116
  return;
@@ -140,6 +142,7 @@ export async function startRepl(opts = {}) {
140
142
  cwd,
141
143
  initialStatus: { status: 'idle' },
142
144
  approvalBridge: tuiBridge,
145
+ isFullscreen: isAltScreen,
143
146
  onPrompt: async (text) => {
144
147
  inflight = runWithBridge(text);
145
148
  await inflight;
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';
@@ -24,5 +23,6 @@ export interface AppProps {
24
23
  updatePlan: (p: PlanStep[]) => void;
25
24
  }) => void;
26
25
  version?: string;
26
+ isFullscreen?: boolean;
27
27
  }
28
28
  export declare function App(props: AppProps): React.JSX.Element;
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,31 +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 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
141
  const scrollUp = (n = 3) => setScrollOffset((p) => Math.max(0, p - n));
172
142
  const scrollDown = (n = 3) => setScrollOffset((p) => Math.min(maxOffset, p + n));
173
143
  useInput((inputStr, key) => {
174
- // slider scroll: PageUp/PageDown, Ctrl+U/D, Shift+↑/↓, j/k in vim-like
175
144
  if (key.pageUp || (key.ctrl && inputStr === 'u')) {
176
145
  scrollUp(5);
177
146
  return;
@@ -191,7 +160,7 @@ export function App(props) {
191
160
  if (awaitingApproval)
192
161
  return;
193
162
  if (key.ctrl && inputStr === 'o') {
194
- const groups = groupTools(transcript).filter((x) => typeof x.verb === 'string');
163
+ const groups = grouped.filter((x) => typeof x.verb === 'string');
195
164
  const last = groups[groups.length - 1];
196
165
  if (last)
197
166
  toggleGroup(last.id);
@@ -240,37 +209,37 @@ export function App(props) {
240
209
  if (!key.ctrl && !key.meta)
241
210
  setInput((v) => v + inputStr);
242
211
  });
243
- const isSmall = width < 80;
244
- const ver = props.version ?? '0.1.24';
212
+ const ver = props.version ?? '0.1.27';
245
213
  const rule = g('rule').repeat(Math.max(10, width - 2));
246
- // Derived status right
247
214
  const cost = (status.usageInput / 1000 * 0.003 + status.usageOutput / 1000 * 0.015);
248
215
  const totalTokens = status.usageInput + status.usageOutput;
249
216
  const ctxPct = totalTokens > 0 ? Math.round((totalTokens / 120_000) * 100) : 0;
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) => {
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';
218
+ 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) => {
256
222
  if (item.verb) {
257
223
  const gr = item;
258
224
  const isExpanded = expandedGroups.has(gr.id);
259
- const marker = isExpanded ? g('expanded') : g('collapsed');
260
225
  const verbLine = (() => {
261
226
  if (gr.items.length === 1) {
262
227
  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;
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}`;
274
243
  }
275
244
  if (gr.verb === 'Read')
276
245
  return `Read ${gr.items.length} files`;
@@ -278,21 +247,21 @@ export function App(props) {
278
247
  return `Searched ${gr.items.length} patterns`;
279
248
  if (gr.verb === 'Ran')
280
249
  return `Ran ${gr.items.length} commands`;
281
- if (gr.verb === 'Edited' || gr.verb === 'Created')
282
- return `${gr.verb} ${gr.items.length} files`;
250
+ if (gr.verb === 'Edited')
251
+ return `Edited ${gr.items.length} files`;
283
252
  return `${gr.verb} ${gr.items.length} items`;
284
253
  })();
285
254
  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));
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));
287
256
  }
288
257
  const it = item;
289
258
  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);
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);
291
260
  }
292
261
  if (it.kind === 'text') {
293
262
  if (it.text.startsWith('queued:'))
294
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);
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));
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));
296
265
  }
297
266
  if (it.kind === 'error')
298
267
  return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.ansi.err, children: [" ", g('guide'), " \u2717 ", it.message] }) }, it.id);
@@ -303,5 +272,5 @@ export function App(props) {
303
272
  if (it.kind === 'diff')
304
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));
305
274
  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 ●' : ''] })] })] }));
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 ●' : ''] })] })] }));
307
276
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "0.1.26",
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",