klyro 0.1.38 → 0.1.40

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,6 +1,6 @@
1
1
  /**
2
- * Klyro TUI design.md Claude Code-like White & Orange #FF6B1A 3 columns
3
- * Top bar + Sidebar 28 + Chat flex + Inspector 36 + Prompt per plan 1-10
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 design.md Claude Code-like White & Orange #FF6B1A 3 columns
4
- * Top bar + Sidebar 28 + Chat flex + Inspector 36 + Prompt per plan 1-10
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';
@@ -10,62 +10,33 @@ import { parse as parseSlash } from '../cli/slash/parser.js';
10
10
  import { tokens, g } from './tokens.js';
11
11
  let _id = 0;
12
12
  function nextId(p) { _id++; return `${p}-${_id}`; }
13
- // Top bar §3
14
- function TopBar({ width, cwd, model }) {
13
+ function Header({ cwd, model, version, width }) {
15
14
  const branch = (() => { try {
16
15
  const { execSync } = require('node:child_process');
17
16
  return execSync('git rev-parse --abbrev-ref HEAD', { cwd, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
18
17
  }
19
18
  catch {
20
- return 'main';
19
+ return '';
21
20
  } })();
22
- const showFull = width >= 120;
23
- return (_jsxs(Box, { width: width, justifyContent: "space-between", borderStyle: "single", borderColor: tokens.colors.border, paddingX: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.orange, bold: true, children: [g('logoBar'), " claude-code"] }), _jsxs(Text, { color: tokens.colors.fgDim, children: [" \u2502 ", branch, " \u2502"] }), _jsx(Text, { color: tokens.colors.orange, children: " \u25C9 3 sessions " }), _jsx(Text, { color: tokens.colors.fgDim, children: "\u2502 \u2318 palette \u2502 ? help" })] }), _jsxs(Box, { children: [showFull ? _jsx(Text, { color: tokens.colors.orange, children: "\u25CF\u25CF\u25CF \u25CF\u25CF " }) : null, _jsx(Text, { color: tokens.colors.fgDim, children: "\u229E \u25A2" })] })] }));
21
+ const showLinks = width >= 120;
22
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { bold: true, color: tokens.colors.accent, children: ["KLYRO v", version] }), showLinks ? _jsx(Text, { color: tokens.colors.dim, children: "\u00E2\u201D\u201A /help /config /clear /exit" }) : null] }), _jsxs(Text, { color: tokens.colors.dim, children: [model, "[200k] \u00C2\u00B7 API Usage Billing"] }), _jsxs(Text, { color: tokens.colors.dim, children: [cwd, branch ? ` · ${branch}` : ''] })] }));
24
23
  }
25
- // Sidebar §4
26
- function Sidebar({ width }) {
27
- if (width < 60)
28
- return null;
29
- const w = Math.min(28, Math.floor(width * 0.22));
30
- return (_jsxs(Box, { flexDirection: "column", width: w, borderStyle: "single", borderColor: tokens.colors.border, paddingX: 1, marginRight: 1, children: [_jsx(Text, { bold: true, color: tokens.colors.fg, children: "\u25BE Sessions" }), _jsxs(Text, { color: tokens.colors.orange, children: ["\u25C9 Build auth flow ", _jsx(Text, { color: tokens.colors.fgDim, children: "2m" })] }), _jsx(Text, { color: tokens.colors.fgDim, children: "\u25CB Refactor parser 12m" }), _jsx(Text, { color: tokens.colors.fgDim, children: "\u25CB Fix bug #42 1h" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, color: tokens.colors.fg, children: "\u25B8 Files" }) }), _jsx(Text, { color: tokens.colors.fgDim, children: "\u25B8 Agents" }), _jsx(Text, { color: tokens.colors.fgDim, children: "\u25B8 MCP" }), _jsx(Text, { color: tokens.colors.fgDim, children: "\u25B8 Hooks" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: tokens.colors.orange, children: "[+ new]" }) })] }));
31
- }
32
- // Inspector §6
33
- function Inspector({ width }) {
34
- if (width < 80)
35
- return null;
36
- const w = 36;
37
- return (_jsxs(Box, { flexDirection: "column", width: w, borderStyle: "single", borderColor: tokens.colors.border, paddingX: 1, marginLeft: 1, children: [_jsx(Text, { bold: true, color: tokens.colors.fg, children: "Inspector \u00B7 Tools" }), _jsx(Text, { color: tokens.colors.success, children: "\u2713 edit_file src/routes.rs" }), _jsx(Text, { color: tokens.colors.fgDim, children: " +12 / -3 0.3s" }), _jsx(Text, { children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx(Text, { color: tokens.colors.success, children: "\u2713 read_file Cargo.toml" }), _jsx(Text, { color: tokens.colors.fgDim, children: " 890 bytes 0.1s" }), _jsx(Text, { children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx(Text, { color: tokens.colors.warning, children: "\u23F3 bash cargo test" }), _jsx(Text, { color: tokens.colors.fgDim, children: " running... 2.1s" })] }));
38
- }
39
- // Chat bubbles §5.1-5.4
40
- function MarkdownText({ text }) {
41
- // minimal: **bold** → bold, keep rest
42
- const parts = [];
43
- let last = 0;
44
- const re = /\*\*(.+?)\*\*/g;
45
- let m;
46
- let i = 0;
47
- while ((m = re.exec(text))) {
48
- if (m.index > last)
49
- parts.push(_jsx(Text, { wrap: "wrap", children: text.slice(last, m.index) }, `t-${i++}`));
50
- parts.push(_jsx(Text, { bold: true, wrap: "wrap", children: m[1] }, `b-${i++}`));
51
- last = m.index + m[0].length;
52
- }
53
- if (last < text.length)
54
- parts.push(_jsx(Text, { wrap: "wrap", children: text.slice(last) }, `t-${i++}`));
55
- return _jsx(Text, { wrap: "wrap", children: parts.length ? parts : text });
56
- }
57
- function verbForTool(n) {
58
- if (n === 'read_file')
24
+ function verbForTool(name) {
25
+ if (name === 'read_file')
59
26
  return 'Read';
60
- if (n === 'list_directory')
27
+ if (name === 'list_directory')
61
28
  return 'Listed';
62
- if (n === 'grep' || n === 'glob' || n.includes('search'))
29
+ if (name === 'grep' || name === 'glob' || name === 'find_files' || name === 'search_files' || name === 'recent_files')
63
30
  return 'Searched';
64
- if (n === 'shell_exec')
31
+ if (name === 'shell_exec')
65
32
  return 'Ran';
66
- if (n.startsWith('git_'))
33
+ if (name.startsWith('git_'))
67
34
  return 'Checked git';
68
- if (n === 'edit_file' || n.includes('edit') || n.includes('write'))
35
+ if (name === 'web_fetch')
36
+ return 'Fetched';
37
+ if (name === 'web_search')
38
+ return 'Searched web';
39
+ if (name === 'edit_file' || name === 'multi_edit' || name === 'apply_patch' || name === 'write_file')
69
40
  return 'Edited';
70
41
  return 'Called';
71
42
  }
@@ -73,7 +44,7 @@ function groupTools(items) {
73
44
  const out = [];
74
45
  let cur = [];
75
46
  const flush = () => {
76
- if (!cur.length)
47
+ if (cur.length === 0)
77
48
  return;
78
49
  const byVerb = new Map();
79
50
  for (const it of cur) {
@@ -84,7 +55,7 @@ function groupTools(items) {
84
55
  }
85
56
  for (const [verb, list] of byVerb) {
86
57
  const totalMs = list.reduce((s, x) => s + (x.latencyMs ?? 0), 0);
87
- const status = list.some(x => x.isError || x.status === 'error') ? 'error' : list.some(x => x.status === 'running') ? 'running' : 'done';
58
+ const status = list.some((x) => x.isError || x.status === 'error') ? 'error' : list.some((x) => x.status === 'running') ? 'running' : 'done';
88
59
  out.push({ id: nextId('g'), verb, items: list, totalMs, status });
89
60
  }
90
61
  cur = [];
@@ -100,6 +71,27 @@ function groupTools(items) {
100
71
  flush();
101
72
  return out;
102
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.colors.dim : undefined, wrap: "wrap", children: text.slice(last, m.index) }, `t-${idx++}`));
85
+ parts.push(_jsx(Text, { bold: true, color: dim ? undefined : tokens.colors.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.colors.dim : undefined, wrap: "wrap", children: text.slice(last) }, `t-${idx++}`));
90
+ if (parts.length === 0)
91
+ return _jsx(Text, { color: dim ? tokens.colors.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
+ }
103
95
  export function App(props) {
104
96
  const { stdout } = useStdout();
105
97
  const [transcript, setTranscript] = useState(props.initialTranscript ?? []);
@@ -113,60 +105,66 @@ export function App(props) {
113
105
  const [expandedGroups, setExpandedGroups] = useState(new Set());
114
106
  const [scrollOffset, setScrollOffset] = useState(0);
115
107
  const streamingIdRef = useRef(null);
116
- const width = stdout?.columns ?? 120;
108
+ const width = stdout?.columns ?? 100;
117
109
  const height = stdout?.rows ?? 30;
118
110
  const isFullscreen = props.isFullscreen ?? false;
119
111
  const grouped = groupTools(transcript);
120
112
  const viewportH = Math.max(5, height - 10);
121
- const totalRows = grouped.reduce((s, it) => s + (it.verb ? 1 + (it.items.length > 1 && expandedGroups.has(it.id) ? it.items.length : 0) : 2), 0) + (plan.length ? 1 : 0) + 2;
113
+ const totalRows = grouped.length + (plan.length > 0 ? 1 : 0) + 2;
122
114
  const maxOffset = Math.max(0, totalRows - viewportH);
115
+ const isAtBottom = scrollOffset >= maxOffset;
123
116
  const trackH = viewportH;
124
- const thumbPos = maxOffset === 0 ? 0 : Math.round(scrollOffset / maxOffset * (trackH - 1));
117
+ const thumbPos = maxOffset === 0 ? 0 : Math.round((scrollOffset / maxOffset) * (trackH - 1));
125
118
  const visibleGrouped = isFullscreen ? grouped.slice(scrollOffset, scrollOffset + viewportH) : grouped;
126
- const placeholder = 'Message Klyro…';
127
- useEffect(() => bridge.subscribe(p => setAwaitingApproval(p !== null)), [bridge]);
128
- useEffect(() => { if (queuedInputs.length > 0 && status.status !== 'running' && !awaitingApproval) {
129
- const toSend = queuedInputs[0];
130
- setQueuedInputs(p => p.slice(1));
131
- setTranscript(p => [...p, { id: nextId('user'), kind: 'text', text: toSend, role: 'user' }]);
132
- streamingIdRef.current = null;
133
- const cmd = parseSlash(toSend.trim());
134
- if (cmd.kind === 'prompt')
135
- void props.onPrompt(cmd.text);
136
- else
137
- void props.onSlash(cmd);
138
- } }, [queuedInputs, status.status, awaitingApproval]);
119
+ useEffect(() => bridge.subscribe((p) => setAwaitingApproval(p !== null)), [bridge]);
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]);
139
133
  useEffect(() => { if (status.status !== 'running')
140
134
  return; const start = Date.now() - elapsed; const t = setInterval(() => setElapsed(Date.now() - start), 1000); return () => clearInterval(t); }, [status.status, elapsed]);
141
- useEffect(() => { if (scrollOffset >= maxOffset)
142
- setScrollOffset(maxOffset); }, [transcript.length, plan.length, maxOffset]);
135
+ useEffect(() => { if (isAtBottom)
136
+ setScrollOffset(maxOffset); }, [transcript.length, plan.length, maxOffset, isAtBottom]);
143
137
  const append = useCallback((item) => { if (item.kind !== 'text' || item.role !== 'assistant')
144
- streamingIdRef.current = null; setTranscript(p => [...p, item]); }, []);
145
- const appendDelta = useCallback((text) => { if (!text)
146
- return; const sid = streamingIdRef.current; if (sid)
147
- setTranscript(p => { const idx = p.findIndex(x => x.id === sid); if (idx === -1)
148
- return [...p, { id: sid, kind: 'text', text, role: 'assistant' }]; const cur = p[idx]; const copy = [...p]; copy[idx] = { ...cur, text: cur.text + text }; return copy; });
149
- else {
150
- const id = nextId('stream');
151
- streamingIdRef.current = id;
152
- setTranscript(p => [...p, { id, kind: 'text', text, role: 'assistant' }]);
153
- } }, []);
138
+ streamingIdRef.current = null; setTranscript((prev) => [...prev, item]); }, []);
139
+ const appendDelta = useCallback((text) => {
140
+ if (!text)
141
+ return;
142
+ const sid = streamingIdRef.current;
143
+ if (sid)
144
+ setTranscript((prev) => { const idx = prev.findIndex((x) => x.id === sid); if (idx === -1)
145
+ 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; });
146
+ else {
147
+ const id = nextId('stream');
148
+ streamingIdRef.current = id;
149
+ setTranscript((prev) => [...prev, { id, kind: 'text', text, role: 'assistant' }]);
150
+ }
151
+ }, []);
154
152
  useEffect(() => { if (status.status !== 'running')
155
153
  streamingIdRef.current = null; }, [status.status]);
156
- const updateStatus = useCallback((s) => setStatus(p => ({ ...p, ...s })), []);
154
+ const updateStatus = useCallback((s) => setStatus((p) => ({ ...p, ...s })), []);
157
155
  const updatePlan = useCallback((p) => setPlan(p), []);
158
156
  const onMountedRef = useRef(props.onMounted);
159
157
  useEffect(() => { onMountedRef.current = props.onMounted; }, [props.onMounted]);
160
158
  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]);
161
- const toggleGroup = (id) => setExpandedGroups(p => { const n = new Set(p); if (n.has(id))
159
+ const toggleGroup = (id) => setExpandedGroups((prev) => { const n = new Set(prev); if (n.has(id))
162
160
  n.delete(id);
163
161
  else
164
162
  n.add(id); return n; });
165
- const scrollUp = (n = 3) => setScrollOffset(p => Math.max(0, p - n));
166
- const scrollDown = (n = 3) => setScrollOffset(p => Math.min(maxOffset, p + n));
163
+ const scrollUp = (n = 3) => setScrollOffset((p) => Math.max(0, p - n));
164
+ const scrollDown = (n = 3) => setScrollOffset((p) => Math.min(maxOffset, p + n));
167
165
  useInput((inputStr, key) => {
168
166
  if (key.escape && queuedInputs.length > 0) {
169
- setQueuedInputs(p => p.slice(1));
167
+ setQueuedInputs((prev) => prev.slice(1));
170
168
  return;
171
169
  }
172
170
  if (key.pageUp || (key.ctrl && inputStr === 'u')) {
@@ -205,16 +203,16 @@ export function App(props) {
205
203
  return;
206
204
  if (queuedInputs.length >= 3)
207
205
  return;
208
- setQueuedInputs(p => [...p, v]);
206
+ setQueuedInputs((prev) => [...prev, v]);
209
207
  setInput('');
210
208
  return;
211
209
  }
212
210
  if (key.backspace || key.delete) {
213
- setInput(v => v.slice(0, -1));
211
+ setInput((v) => v.slice(0, -1));
214
212
  return;
215
213
  }
216
214
  if (!key.ctrl && !key.meta)
217
- setInput(v => v + inputStr.replace(/\r\n/g, '\n').replace(/\r/g, '\n'));
215
+ setInput((v) => v + inputStr.replace(/\r\n/g, '\n').replace(/\r/g, '\n'));
218
216
  return;
219
217
  }
220
218
  if (key.return) {
@@ -222,7 +220,7 @@ export function App(props) {
222
220
  if (!v)
223
221
  return;
224
222
  setInput('');
225
- setTranscript(p => [...p, { id: nextId('user'), kind: 'text', text: v, role: 'user' }]);
223
+ setTranscript((prev) => [...prev, { id: nextId('user'), kind: 'text', text: v, role: 'user' }]);
226
224
  streamingIdRef.current = null;
227
225
  const cmd = parseSlash(v);
228
226
  if (cmd.kind === 'prompt')
@@ -232,71 +230,92 @@ export function App(props) {
232
230
  return;
233
231
  }
234
232
  if (key.backspace || key.delete) {
235
- setInput(v => v.slice(0, -1));
233
+ setInput((v) => v.slice(0, -1));
236
234
  return;
237
235
  }
238
236
  if (!key.ctrl && !key.meta)
239
- setInput(v => v + inputStr);
237
+ setInput((v) => v + inputStr);
240
238
  });
241
- const ver = props.version ?? '0.1.36';
242
- const rule = ''.repeat(Math.max(10, width - 2));
239
+ const ver = props.version ?? '0.1.27';
240
+ const rule = g('rule').repeat(Math.max(10, width - 2));
243
241
  const cost = (status.usageInput / 1000 * 0.003 + status.usageOutput / 1000 * 0.015);
244
- const ctxPct = status.usageInput + status.usageOutput > 0 ? Math.round((status.usageInput + status.usageOutput) / 120_000 * 100) : 0;
245
- const hints = maxOffset > 0 && isFullscreen ? 'PgUp/Dn scroll · ' : '' + (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');
246
- return (_jsxs(Box, { flexDirection: "column", width: width, height: isFullscreen ? height - 1 : undefined, children: [_jsx(TopBar, { width: width, cwd: props.cwd, model: status.model }), _jsxs(Box, { flexDirection: "row", flexGrow: isFullscreen ? 1 : 0, overflow: isFullscreen ? 'hidden' : undefined, children: [_jsx(Sidebar, { width: width }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "single", borderColor: tokens.colors.border, paddingX: 1, overflow: isFullscreen ? 'hidden' : undefined, children: [grouped.length === 0 ? _jsx(Text, { color: tokens.colors.fgDim, children: "Message Klyro\u2026" }) : visibleGrouped.map(item => {
242
+ const totalTokens = status.usageInput + status.usageOutput;
243
+ const ctxPct = totalTokens > 0 ? Math.round((totalTokens / 120_000) * 100) : 0;
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';
245
+ const hints = maxOffset > 0 && isFullscreen ? `${baseHints} · PgUp/Dn scroll` : baseHints;
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.colors.dim, children: "Message Klyro\u00E2\u20AC\u00A6" })) : visibleGrouped.map((item) => {
247
247
  if (item.verb) {
248
248
  const gr = item;
249
249
  const isExpanded = expandedGroups.has(gr.id);
250
- const verbLine = (() => { if (gr.items.length === 1) {
251
- const it = gr.items[0];
252
- let p = '';
253
- try {
254
- const a = JSON.parse(it.args);
255
- p = a.path ?? a.pattern ?? a.command?.slice(0, 48) ?? '';
250
+ const verbLine = (() => {
251
+ if (gr.items.length === 1) {
252
+ const it = gr.items[0];
253
+ let p = '';
254
+ try {
255
+ const a = JSON.parse(it.args);
256
+ p = a.path ?? a.pattern ?? a.command?.slice(0, 48) ?? '';
257
+ }
258
+ catch {
259
+ p = '';
260
+ }
261
+ if (gr.verb === 'Read' && p)
262
+ return `Read ${p.split('/').pop()}`;
263
+ if (gr.verb === 'Searched' && p)
264
+ return `Searched "${p}"`;
265
+ if (gr.verb === 'Ran' && p)
266
+ return `Ran ${p.split(' ')[0]}`;
267
+ return `${gr.verb} ${p}`;
256
268
  }
257
- catch { }
258
- if (gr.verb === 'Read' && p)
259
- return `Read ${p.split('/').pop()}`;
260
- if (gr.verb === 'Searched' && p)
261
- return `Searched "${p}"`;
262
- if (gr.verb === 'Ran' && p)
263
- return `Ran ${p.split(' ')[0]}`;
264
- return `${gr.verb} ${p}`;
265
- } if (gr.verb === 'Read')
266
- return `Read ${gr.items.length} files`; if (gr.verb === 'Searched')
267
- return `Searched ${gr.items.length} patterns`; if (gr.verb === 'Ran')
268
- return `Ran ${gr.items.length} commands`; return `${gr.verb} ${gr.items.length} items`; })();
269
- const right = gr.status === 'running' ? `${(elapsed / 1000).toFixed(1)}s` : gr.status === 'error' ? '✗' : `${gr.totalMs}ms`;
270
- const marker = isExpanded ? '' : '✓';
271
- const markerColor = gr.status === 'error' ? tokens.colors.danger : gr.status === 'running' ? tokens.colors.orange : tokens.colors.success;
272
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "single", borderColor: tokens.colors.border, paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: markerColor, children: [marker, " ", verbLine] }), _jsx(Text, { color: tokens.colors.fgDim, children: right })] }), isExpanded ? gr.items.map(it => {
269
+ if (gr.verb === 'Read')
270
+ return `Read ${gr.items.length} files`;
271
+ if (gr.verb === 'Searched')
272
+ return `Searched ${gr.items.length} patterns`;
273
+ if (gr.verb === 'Ran')
274
+ return `Ran ${gr.items.length} commands`;
275
+ if (gr.verb === 'Edited')
276
+ return `Edited ${gr.items.length} files`;
277
+ return `${gr.verb} ${gr.items.length} items`;
278
+ })();
279
+ const right = gr.status === 'running' ? `${(elapsed / 1000).toFixed(1)}s` : gr.status === 'error' ? '✗' : `${gr.totalMs}ms`;
280
+ const marker = isExpanded ? '▼' : '✓';
281
+ const markerColor = gr.status === 'error' ? tokens.colors.err : gr.status === 'running' ? tokens.colors.warn : tokens.colors.ok;
282
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: markerColor, children: [marker, " ", verbLine] }), _jsxs(Text, { color: tokens.colors.dim, children: [" ", right] })] }), isExpanded ? gr.items.map((it) => {
273
283
  let friendly = '';
274
284
  try {
275
285
  const a = JSON.parse(it.args);
276
- const p = a.path ?? '';
277
- friendly = p ? p.split('/').pop() ?? p : it.args.slice(0, 40);
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);
278
296
  }
279
297
  catch {
280
298
  friendly = it.args.slice(0, 40);
281
299
  }
282
- return _jsx(Box, { paddingLeft: 1, children: _jsxs(Text, { color: tokens.colors.fgDim, children: ["\u2514 ", friendly] }) }, it.id);
300
+ return (_jsxs(Box, { paddingLeft: 4, children: [_jsxs(Text, { color: tokens.colors.guide, children: [g('end'), " "] }), _jsx(Text, { color: tokens.colors.dim, children: friendly })] }, it.id));
283
301
  }) : null] }, gr.id));
284
302
  }
285
303
  const it = item;
286
304
  if (it.kind === 'text' && it.role === 'user') {
287
- return _jsxs(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "single", borderColor: tokens.colors.border, paddingX: 1, children: [_jsx(Text, { color: tokens.colors.fgMuted, children: "You" }), _jsx(Text, { wrap: "wrap", children: it.text })] }, it.id);
305
+ return _jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: tokens.colors.accent, bold: true, children: [g('prompt'), " "] }), _jsx(Text, { wrap: "wrap", children: it.text })] }, it.id);
288
306
  }
289
307
  if (it.kind === 'text') {
290
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "single", borderColor: tokens.colors.orangeSoft, paddingX: 1, borderLeftColor: tokens.colors.orange, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: tokens.colors.orange, children: "Claude" }), _jsxs(Text, { color: tokens.colors.fgDim, children: [" \u00B7 ", status.model, " ", status.status === 'running' && !streamingIdRef.current ? '◯ thinking' : ''] })] }), _jsx(Box, { marginTop: 1, children: _jsx(MarkdownText, { text: 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.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: tokens.colors.accent, children: [g('agentBullet'), " Klyro"] })] }), _jsx(Box, { paddingLeft: 2, flexDirection: "column", children: _jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.dim, children: [" ", g('guide'), " "] }), _jsx(Box, { flexGrow: 1, children: _jsx(MarkdownText, { text: it.text }) })] }) })] }, it.id));
291
310
  }
292
311
  if (it.kind === 'error')
293
- return _jsx(Box, { borderStyle: "single", borderColor: tokens.colors.danger, paddingX: 1, marginBottom: 1, children: _jsxs(Text, { color: tokens.colors.danger, children: ["\u2717 ", it.message] }) }, it.id);
312
+ return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.colors.err, children: [" ", g('guide'), " \u00E2\u0153\u2014 ", it.message] }) }, it.id);
294
313
  if (it.kind === 'policy')
295
314
  return null;
296
315
  if (it.kind === 'file_changed')
297
- return _jsx(Box, { borderStyle: "single", borderColor: tokens.colors.border, paddingX: 1, marginBottom: 1, children: _jsxs(Text, { color: tokens.colors.fgDim, children: ["\u270E ", it.path, " ", it.op] }) }, it.id);
316
+ return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.colors.dim, children: [" ", g('guide'), " ", g('editsBadge'), " ", it.path, " ", it.op] }) }, it.id);
298
317
  if (it.kind === 'diff')
299
- return _jsxs(Box, { borderStyle: "single", borderColor: tokens.colors.border, paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, children: it.summary }), it.hunks.map((h, i) => _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: tokens.colors.fg, children: h.path }), h.lines.map((l, j) => _jsxs(Text, { wrap: "wrap", color: l.kind === 'add' ? tokens.colors.success : l.kind === 'remove' ? tokens.colors.danger : tokens.colors.fgDim, children: [l.kind === 'add' ? "+ " : l.kind === 'remove' ? "- " : " ", l.text] }, j))] }, i))] }, it.id);
318
+ return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, color: tokens.colors.soft, children: it.summary ?? 'Diff' }), it.hunks.map((h, i) => (_jsxs(Box, { flexDirection: "column", marginTop: 0, children: [_jsx(Text, { color: tokens.colors.soft, children: h.path }), h.lines.map((l, j) => (_jsxs(Text, { wrap: "wrap", color: l.kind === 'add' ? tokens.colors.ok : l.kind === 'remove' ? tokens.colors.err : tokens.colors.dim, children: [l.kind === 'add' ? '+ ' : l.kind === 'remove' ? '- ' : ' ', l.text] }, j)))] }, i)))] }, it.id));
300
319
  return null;
301
- }), status.status === 'running' && !streamingIdRef.current ? _jsx(Box, { borderStyle: "single", borderColor: tokens.colors.orangeSoft, paddingX: 1, marginBottom: 1, children: _jsxs(Text, { color: tokens.colors.orange, children: ["\u25EF thinking ", (elapsed / 1000).toFixed(1), "s"] }) }) : null, plan.length > 0 ? _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: tokens.colors.border, paddingX: 1, marginBottom: 1, children: [_jsxs(Text, { bold: true, children: ["\u25C7 Plan ", plan.filter(p => p.status === 'done').length, "/", plan.length] }), plan.slice(0, 8).map(p => _jsxs(Text, { color: p.status === 'done' ? tokens.colors.success : p.status === 'in_progress' ? tokens.colors.orange : tokens.colors.fgDim, children: [p.status === 'done' ? '' : p.status === 'in_progress' ? '' : '', " ", p.title] }, p.id))] }) : null, queuedInputs.length > 0 ? _jsx(Box, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: queuedInputs.map((q, i) => _jsxs(Text, { color: tokens.colors.fgDim, children: ["queued: ", q.slice(0, 60), i === 0 ? ' esc to drop' : ''] }, i)) }) : null] }), _jsx(Inspector, { width: width }), isFullscreen ? _jsx(Box, { flexDirection: "column", width: 1, marginLeft: 1, children: Array.from({ length: trackH }).map((_, i) => _jsx(Text, { color: i === thumbPos ? tokens.colors.orange : tokens.colors.border, children: i === thumbPos ? '' : '' }, i)) }) : null] }), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: tokens.colors.border, paddingX: 1, children: _jsxs(Box, { children: [_jsx(Text, { color: tokens.colors.orange, children: "\u258C " }), _jsxs(Text, { wrap: "wrap", children: [input || _jsx(Text, { color: tokens.colors.fgDim, children: "Type a message\u2026 \u23CE send \u21E7\u23CE newline /" }), "\u258F"] })] }) }), _jsxs(Box, { justifyContent: "space-between", paddingX: 1, children: [_jsx(Text, { color: tokens.colors.fgDim, children: hints }), _jsxs(Text, { color: tokens.colors.fgDim, children: [cost > 0 ? `$${cost.toFixed(2)} · ` : '', ctxPct > 0 ? `${ctxPct}% ` : ' ', _jsx(Text, { color: tokens.colors.orange, children: "\u25CF\u25CF\u25CF \u25CF\u25CF" })] })] })] }));
320
+ }), status.status === 'running' && !streamingIdRef.current ? (_jsxs(Box, { paddingLeft: 2, marginBottom: 1, children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsx(Text, { color: tokens.colors.dim, children: "Thinking..." }), _jsxs(Text, { color: tokens.colors.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.colors.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.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: p.status === 'done' ? tokens.colors.ok : p.status === 'in_progress' ? tokens.colors.accent : tokens.colors.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.colors.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.colors.accent : tokens.colors.guide, children: i === thumbPos ? '●' : '│' }, i))) })) : null] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: tokens.colors.guide, children: g('rule').repeat(Math.max(10, width - 2)) }), _jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.accent, bold: true, children: [g('prompt'), " "] }), _jsxs(Text, { wrap: "wrap", children: [input || _jsx(Text, { color: tokens.colors.dim, children: "Message Klyro\u00E2\u20AC\u00A6" }), "\u00E2\u2013\u008F"] })] }), _jsx(Text, { color: tokens.colors.guide, children: g('rule').repeat(Math.max(10, width - 2)) })] }), _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: tokens.colors.dim, children: [baseHints, maxOffset > 0 && isFullscreen ? ' · PgUp/Dn scroll' : ''] }), _jsxs(Text, { color: tokens.colors.dim, children: [cost > 0 ? `$${cost.toFixed(2)} · ` : '', ctxPct > 0 ? `${ctxPct}% ctx · ` : '', status.status === 'running' ? 'auto mode on ●' : ''] })] })] }));
302
321
  }
@@ -1,53 +1,40 @@
1
1
  /**
2
- * design.md §2 — White & Orange (light default) + Dark mirror
3
- * Light: bg #FFFFFF orange #FF6B1A Dark: bg #1A1A1A orange #FF8A3D
4
- * Only orange is saturated in chrome; content uses dim borders.
2
+ * Klyro tokens — White & Orange #FF6B1A light + Dark #E8843C — per design.md + TUI_DESIGN.md
3
+ * True 24-bit hex, Ink will render via 256-color fallback. No backgrounds except diff.
5
4
  */
6
5
  export declare const tokens: {
7
6
  readonly colors: {
8
7
  readonly bg: "#FFFFFF";
9
8
  readonly bgElevated: "#FAF7F2";
10
- readonly bgSubtle: "#F5F0E8";
11
9
  readonly fg: "#1A1A1A";
12
- readonly fgMuted: "#6B6B6B";
13
- readonly fgDim: "#9A9A9A";
14
- readonly orange: "#FF6B1A";
15
- readonly orangeHot: "#FF8A3D";
16
- readonly orangeDeep: "#CC4F0A";
17
- readonly orangeSoft: "#FFF1E6";
18
- readonly border: "#E8E0D2";
19
- readonly borderStrong: "#1A1A1A";
20
- readonly success: "#2E7D32";
21
- readonly warning: "#C77700";
22
- readonly danger: "#C62828";
23
- readonly codeBg: "#FAF7F2";
24
- readonly codeBorder: "#E8E0D2";
25
- readonly guide: "#E8E0D2";
26
- readonly darkBg: "#1A1A1A";
27
- readonly darkOrange: "#FF8A3D";
28
- readonly darkBorder: "#333333";
10
+ readonly soft: "#F5F5F5";
11
+ readonly dim: "#9A9A9A";
12
+ readonly guide: "#3A3A3A";
13
+ readonly accent: "#FF6B1A";
14
+ readonly accentSoft: "#FFF1E6";
15
+ readonly ok: "#FF6B1A";
16
+ readonly err: "#E06C6C";
17
+ readonly warn: "#FF6B1A";
18
+ readonly info: "#6FA8DC";
19
+ readonly diffAddBg: "#12250F";
20
+ readonly diffDelBg: "#2A1212";
29
21
  };
30
22
  readonly ansi: {
31
- readonly bg: "white";
32
- readonly fg: "black";
33
- readonly fgMuted: "gray";
23
+ readonly accent: "yellowBright";
24
+ readonly accentBold: "yellowBright";
25
+ readonly fg: "white";
26
+ readonly soft: "whiteBright";
34
27
  readonly dim: "gray";
35
- readonly orange: "yellow";
36
- readonly orangeBright: "red";
37
- readonly border: "gray";
38
28
  readonly guide: "gray";
39
- readonly success: "green";
40
- readonly warning: "yellow";
41
- readonly danger: "red";
42
- readonly accent: "yellow";
43
- readonly accentBold: "yellowBright";
44
- readonly soft: "white";
29
+ readonly ok: "yellowBright";
30
+ readonly err: "red";
31
+ readonly warn: "yellowBright";
32
+ readonly info: "blue";
33
+ readonly border: "gray";
45
34
  readonly muted: "gray";
35
+ readonly success: "yellowBright";
46
36
  readonly error: "red";
47
- readonly ok: "green";
48
- readonly warn: "yellow";
49
- readonly info: "blue";
50
- readonly successAlt: "green";
37
+ readonly warning: "yellowBright";
51
38
  };
52
39
  };
53
40
  export declare const glyphs: {
@@ -72,12 +59,10 @@ export declare const glyphs: {
72
59
  readonly logoBar: "▌";
73
60
  readonly dotFilled: "●";
74
61
  readonly dotEmpty: "○";
75
- readonly dotHalf: "◐";
76
- readonly sidebar: "⊞";
77
- readonly inspector: "▢";
78
- readonly copy: "⧉";
79
62
  readonly brand: "◆";
80
63
  readonly compaction: "⟲";
64
+ readonly editsBadge: "✎";
65
+ readonly dot: "·";
81
66
  };
82
67
  export declare const glyphAscii: {
83
68
  readonly prompt: ">";
@@ -101,14 +86,10 @@ export declare const glyphAscii: {
101
86
  readonly logoBar: "|";
102
87
  readonly dotFilled: "*";
103
88
  readonly dotEmpty: "o";
104
- readonly dotHalf: "o";
105
89
  };
106
90
  export declare function isAsciiMode(): boolean;
107
91
  export declare function g(name: keyof typeof glyphs): string;
108
92
  export declare const spacing: {
109
- readonly maxWidth: 120;
110
- readonly indent: 2;
111
- readonly gap: 1;
112
93
  readonly sidebar: 28;
113
94
  readonly inspector: 36;
114
95
  };
@@ -1,54 +1,40 @@
1
1
  /**
2
- * design.md §2 — White & Orange (light default) + Dark mirror
3
- * Light: bg #FFFFFF orange #FF6B1A Dark: bg #1A1A1A orange #FF8A3D
4
- * Only orange is saturated in chrome; content uses dim borders.
2
+ * Klyro tokens — White & Orange #FF6B1A light + Dark #E8843C — per design.md + TUI_DESIGN.md
3
+ * True 24-bit hex, Ink will render via 256-color fallback. No backgrounds except diff.
5
4
  */
6
5
  export const tokens = {
7
6
  colors: {
8
7
  bg: '#FFFFFF',
9
8
  bgElevated: '#FAF7F2',
10
- bgSubtle: '#F5F0E8',
11
9
  fg: '#1A1A1A',
12
- fgMuted: '#6B6B6B',
13
- fgDim: '#9A9A9A',
14
- orange: '#FF6B1A',
15
- orangeHot: '#FF8A3D',
16
- orangeDeep: '#CC4F0A',
17
- orangeSoft: '#FFF1E6',
18
- border: '#E8E0D2',
19
- borderStrong: '#1A1A1A',
20
- success: '#2E7D32',
21
- warning: '#C77700',
22
- danger: '#C62828',
23
- codeBg: '#FAF7F2',
24
- codeBorder: '#E8E0D2',
25
- guide: '#E8E0D2',
26
- // dark mirror
27
- darkBg: '#1A1A1A',
28
- darkOrange: '#FF8A3D',
29
- darkBorder: '#333333',
10
+ soft: '#F5F5F5',
11
+ dim: '#9A9A9A',
12
+ guide: '#3A3A3A',
13
+ accent: '#FF6B1A',
14
+ accentSoft: '#FFF1E6',
15
+ ok: '#FF6B1A',
16
+ err: '#E06C6C',
17
+ warn: '#FF6B1A',
18
+ info: '#6FA8DC',
19
+ diffAddBg: '#12250F',
20
+ diffDelBg: '#2A1212',
30
21
  },
31
22
  ansi: {
32
- bg: 'white',
33
- fg: 'black',
34
- fgMuted: 'gray',
23
+ accent: 'yellowBright',
24
+ accentBold: 'yellowBright',
25
+ fg: 'white',
26
+ soft: 'whiteBright',
35
27
  dim: 'gray',
36
- orange: 'yellow', // #FF6B1A → 202
37
- orangeBright: 'red', // fallback vivid
38
- border: 'gray',
39
28
  guide: 'gray',
40
- success: 'green',
41
- warning: 'yellow',
42
- danger: 'red',
43
- accent: 'yellow',
44
- accentBold: 'yellowBright',
45
- soft: 'white',
29
+ ok: 'yellowBright',
30
+ err: 'red',
31
+ warn: 'yellowBright',
32
+ info: 'blue',
33
+ border: 'gray',
46
34
  muted: 'gray',
35
+ success: 'yellowBright',
47
36
  error: 'red',
48
- ok: 'green',
49
- warn: 'yellow',
50
- info: 'blue',
51
- successAlt: 'green',
37
+ warning: 'yellowBright',
52
38
  },
53
39
  };
54
40
  export const glyphs = {
@@ -73,12 +59,10 @@ export const glyphs = {
73
59
  logoBar: '▌',
74
60
  dotFilled: '●',
75
61
  dotEmpty: '○',
76
- dotHalf: '◐',
77
- sidebar: '⊞',
78
- inspector: '▢',
79
- copy: '⧉',
80
62
  brand: '◆',
81
63
  compaction: '⟲',
64
+ editsBadge: '✎',
65
+ dot: '·',
82
66
  };
83
67
  export const glyphAscii = {
84
68
  prompt: '>',
@@ -102,7 +86,6 @@ export const glyphAscii = {
102
86
  logoBar: '|',
103
87
  dotFilled: '*',
104
88
  dotEmpty: 'o',
105
- dotHalf: 'o',
106
89
  };
107
90
  export function isAsciiMode() {
108
91
  return process.env.TERM === 'dumb' || process.env.KLYRO_ASCII === '1' || false;
@@ -112,4 +95,4 @@ export function g(name) {
112
95
  return glyphAscii[name] ?? glyphs[name];
113
96
  return glyphs[name];
114
97
  }
115
- export const spacing = { maxWidth: 120, indent: 2, gap: 1, sidebar: 28, inspector: 36 };
98
+ export const spacing = { sidebar: 28, inspector: 36 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
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",