klyro 0.1.35 → 0.1.37

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 — opencode-cleanno clumsy words, correct wrap, markdown, scroll
3
- * Header 3 rows, guide at col2, Klyro accent, prose wrapped at word boundaries
2
+ * Klyro TUI — design.md Claude Code-likeWhite & Orange #FF6B1A 3 columns
3
+ * Top bar + Sidebar 28 + Chat flex + Inspector 36 + Prompt per plan 1-10
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 — opencode-cleanno clumsy words, correct wrap, markdown, scroll
4
- * Header 3 rows, guide at col2, Klyro accent, prose wrapped at word boundaries
3
+ * Klyro TUI — design.md Claude Code-likeWhite & Orange #FF6B1A 3 columns
4
+ * Top bar + Sidebar 28 + Chat flex + Inspector 36 + Prompt per plan 1-10
5
5
  */
6
6
  import { useState, useEffect, useRef, useCallback } from 'react';
7
7
  import { Box, Text, useInput, useStdout } from 'ink';
@@ -10,33 +10,62 @@ 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
- function Header({ cwd, model, version, width }) {
13
+ // Top bar §3
14
+ function TopBar({ width, cwd, model }) {
14
15
  const branch = (() => { try {
15
16
  const { execSync } = require('node:child_process');
16
17
  return execSync('git rev-parse --abbrev-ref HEAD', { cwd, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
17
18
  }
18
19
  catch {
19
- return '';
20
+ return 'main';
20
21
  } })();
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.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}` : ''] })] }));
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" })] })] }));
23
24
  }
24
- function verbForTool(name) {
25
- if (name === 'read_file')
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')
26
59
  return 'Read';
27
- if (name === 'list_directory')
60
+ if (n === 'list_directory')
28
61
  return 'Listed';
29
- if (name === 'grep' || name === 'glob' || name === 'find_files' || name === 'search_files' || name === 'recent_files')
62
+ if (n === 'grep' || n === 'glob' || n.includes('search'))
30
63
  return 'Searched';
31
- if (name === 'shell_exec')
64
+ if (n === 'shell_exec')
32
65
  return 'Ran';
33
- if (name.startsWith('git_'))
66
+ if (n.startsWith('git_'))
34
67
  return 'Checked git';
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')
68
+ if (n === 'edit_file' || n.includes('edit') || n.includes('write'))
40
69
  return 'Edited';
41
70
  return 'Called';
42
71
  }
@@ -44,7 +73,7 @@ function groupTools(items) {
44
73
  const out = [];
45
74
  let cur = [];
46
75
  const flush = () => {
47
- if (cur.length === 0)
76
+ if (!cur.length)
48
77
  return;
49
78
  const byVerb = new Map();
50
79
  for (const it of cur) {
@@ -55,7 +84,7 @@ function groupTools(items) {
55
84
  }
56
85
  for (const [verb, list] of byVerb) {
57
86
  const totalMs = list.reduce((s, x) => s + (x.latencyMs ?? 0), 0);
58
- const status = list.some((x) => x.isError || x.status === 'error') ? 'error' : list.some((x) => x.status === 'running') ? 'running' : 'done';
87
+ const status = list.some(x => x.isError || x.status === 'error') ? 'error' : list.some(x => x.status === 'running') ? 'running' : 'done';
59
88
  out.push({ id: nextId('g'), verb, items: list, totalMs, status });
60
89
  }
61
90
  cur = [];
@@ -71,27 +100,6 @@ function groupTools(items) {
71
100
  flush();
72
101
  return out;
73
102
  }
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.ansi.dim : undefined, wrap: "wrap", children: text.slice(last, m.index) }, `t-${idx++}`));
85
- parts.push(_jsx(Text, { bold: true, color: dim ? undefined : tokens.ansi.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.ansi.dim : undefined, wrap: "wrap", children: text.slice(last) }, `t-${idx++}`));
90
- if (parts.length === 0)
91
- return _jsx(Text, { color: dim ? tokens.ansi.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
- }
95
103
  export function App(props) {
96
104
  const { stdout } = useStdout();
97
105
  const [transcript, setTranscript] = useState(props.initialTranscript ?? []);
@@ -105,74 +113,60 @@ export function App(props) {
105
113
  const [expandedGroups, setExpandedGroups] = useState(new Set());
106
114
  const [scrollOffset, setScrollOffset] = useState(0);
107
115
  const streamingIdRef = useRef(null);
108
- const width = stdout?.columns ?? 100;
116
+ const width = stdout?.columns ?? 120;
109
117
  const height = stdout?.rows ?? 30;
110
118
  const isFullscreen = props.isFullscreen ?? false;
111
119
  const grouped = groupTools(transcript);
112
120
  const viewportH = Math.max(5, height - 10);
113
- const estLines = (s) => Math.max(1, Math.ceil(s.length / Math.max(20, width - 6)));
114
- const totalRows = grouped.reduce((sum, it) => {
115
- if (it.verb)
116
- return sum + 1 + ((it.items.length > 1 && expandedGroups.has(it.id)) ? it.items.length : 0);
117
- const t = it;
118
- if (t.kind === 'text')
119
- return sum + estLines(t.text) + 1;
120
- return sum + 2;
121
- }, 0) + (plan.length > 0 ? plan.length + 1 : 0) + 2;
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;
122
122
  const maxOffset = Math.max(0, totalRows - viewportH);
123
- const isAtBottom = scrollOffset >= maxOffset;
124
123
  const trackH = viewportH;
125
- const thumbPos = maxOffset === 0 ? 0 : Math.round((scrollOffset / maxOffset) * (trackH - 1));
124
+ const thumbPos = maxOffset === 0 ? 0 : Math.round(scrollOffset / maxOffset * (trackH - 1));
126
125
  const visibleGrouped = isFullscreen ? grouped.slice(scrollOffset, scrollOffset + viewportH) : grouped;
127
- useEffect(() => bridge.subscribe((p) => setAwaitingApproval(p !== null)), [bridge]);
128
- useEffect(() => {
129
- if (queuedInputs.length > 0 && status.status !== 'running' && !awaitingApproval) {
130
- const toSend = queuedInputs[0];
131
- setQueuedInputs((prev) => prev.slice(1));
132
- setTranscript((prev) => [...prev, { id: nextId('user'), kind: 'text', text: toSend, role: 'user' }]);
133
- streamingIdRef.current = null;
134
- const cmd = parseSlash(toSend.trim());
135
- if (cmd.kind === 'prompt')
136
- void props.onPrompt(cmd.text);
137
- else
138
- void props.onSlash(cmd);
139
- }
140
- }, [queuedInputs, status.status, awaitingApproval]);
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]);
141
139
  useEffect(() => { if (status.status !== 'running')
142
140
  return; const start = Date.now() - elapsed; const t = setInterval(() => setElapsed(Date.now() - start), 1000); return () => clearInterval(t); }, [status.status, elapsed]);
143
- useEffect(() => { if (isAtBottom)
144
- setScrollOffset(maxOffset); }, [transcript.length, plan.length, maxOffset, isAtBottom]);
141
+ useEffect(() => { if (scrollOffset >= maxOffset)
142
+ setScrollOffset(maxOffset); }, [transcript.length, plan.length, maxOffset]);
145
143
  const append = useCallback((item) => { if (item.kind !== 'text' || item.role !== 'assistant')
146
- streamingIdRef.current = null; setTranscript((prev) => [...prev, item]); }, []);
147
- const appendDelta = useCallback((text) => {
148
- if (!text)
149
- return;
150
- const sid = streamingIdRef.current;
151
- if (sid)
152
- setTranscript((prev) => { const idx = prev.findIndex((x) => x.id === sid); if (idx === -1)
153
- 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; });
154
- else {
155
- const id = nextId('stream');
156
- streamingIdRef.current = id;
157
- setTranscript((prev) => [...prev, { id, kind: 'text', text, role: 'assistant' }]);
158
- }
159
- }, []);
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
+ } }, []);
160
154
  useEffect(() => { if (status.status !== 'running')
161
155
  streamingIdRef.current = null; }, [status.status]);
162
- const updateStatus = useCallback((s) => setStatus((p) => ({ ...p, ...s })), []);
156
+ const updateStatus = useCallback((s) => setStatus(p => ({ ...p, ...s })), []);
163
157
  const updatePlan = useCallback((p) => setPlan(p), []);
164
158
  const onMountedRef = useRef(props.onMounted);
165
159
  useEffect(() => { onMountedRef.current = props.onMounted; }, [props.onMounted]);
166
160
  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]);
167
- const toggleGroup = (id) => setExpandedGroups((prev) => { const n = new Set(prev); if (n.has(id))
161
+ const toggleGroup = (id) => setExpandedGroups(p => { const n = new Set(p); if (n.has(id))
168
162
  n.delete(id);
169
163
  else
170
164
  n.add(id); return n; });
171
- const scrollUp = (n = 3) => setScrollOffset((p) => Math.max(0, p - n));
172
- const scrollDown = (n = 3) => setScrollOffset((p) => Math.min(maxOffset, p + 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));
173
167
  useInput((inputStr, key) => {
174
168
  if (key.escape && queuedInputs.length > 0) {
175
- setQueuedInputs((prev) => prev.slice(1));
169
+ setQueuedInputs(p => p.slice(1));
176
170
  return;
177
171
  }
178
172
  if (key.pageUp || (key.ctrl && inputStr === 'u')) {
@@ -211,16 +205,16 @@ export function App(props) {
211
205
  return;
212
206
  if (queuedInputs.length >= 3)
213
207
  return;
214
- setQueuedInputs((prev) => [...prev, v]);
208
+ setQueuedInputs(p => [...p, v]);
215
209
  setInput('');
216
210
  return;
217
211
  }
218
212
  if (key.backspace || key.delete) {
219
- setInput((v) => v.slice(0, -1));
213
+ setInput(v => v.slice(0, -1));
220
214
  return;
221
215
  }
222
216
  if (!key.ctrl && !key.meta)
223
- setInput((v) => v + inputStr.replace(/\r\n/g, '\n').replace(/\r/g, '\n'));
217
+ setInput(v => v + inputStr.replace(/\r\n/g, '\n').replace(/\r/g, '\n'));
224
218
  return;
225
219
  }
226
220
  if (key.return) {
@@ -228,7 +222,7 @@ export function App(props) {
228
222
  if (!v)
229
223
  return;
230
224
  setInput('');
231
- setTranscript((prev) => [...prev, { id: nextId('user'), kind: 'text', text: v, role: 'user' }]);
225
+ setTranscript(p => [...p, { id: nextId('user'), kind: 'text', text: v, role: 'user' }]);
232
226
  streamingIdRef.current = null;
233
227
  const cmd = parseSlash(v);
234
228
  if (cmd.kind === 'prompt')
@@ -238,92 +232,71 @@ export function App(props) {
238
232
  return;
239
233
  }
240
234
  if (key.backspace || key.delete) {
241
- setInput((v) => v.slice(0, -1));
235
+ setInput(v => v.slice(0, -1));
242
236
  return;
243
237
  }
244
238
  if (!key.ctrl && !key.meta)
245
- setInput((v) => v + inputStr);
239
+ setInput(v => v + inputStr);
246
240
  });
247
- const ver = props.version ?? '0.1.27';
248
- const rule = g('rule').repeat(Math.max(10, width - 2));
241
+ const ver = props.version ?? '0.1.36';
242
+ const rule = ''.repeat(Math.max(10, width - 2));
249
243
  const cost = (status.usageInput / 1000 * 0.003 + status.usageOutput / 1000 * 0.015);
250
- const totalTokens = status.usageInput + status.usageOutput;
251
- const ctxPct = totalTokens > 0 ? Math.round((totalTokens / 120_000) * 100) : 0;
252
- 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';
253
- const hints = maxOffset > 0 && isFullscreen ? `${baseHints} · PgUp/Dn scroll` : baseHints;
254
- 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: "Message Klyro\u2026" })) : visibleGrouped.map((item) => {
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 => {
255
247
  if (item.verb) {
256
248
  const gr = item;
257
249
  const isExpanded = expandedGroups.has(gr.id);
258
- const verbLine = (() => {
259
- if (gr.items.length === 1) {
260
- const it = gr.items[0];
261
- let p = '';
262
- try {
263
- const a = JSON.parse(it.args);
264
- p = a.path ?? a.pattern ?? a.command?.slice(0, 48) ?? '';
265
- }
266
- catch {
267
- p = '';
268
- }
269
- if (gr.verb === 'Read' && p)
270
- return `Read ${p.split('/').pop()}`;
271
- if (gr.verb === 'Searched' && p)
272
- return `Searched "${p}"`;
273
- if (gr.verb === 'Ran' && p)
274
- return `Ran ${p.split(' ')[0]}`;
275
- return `${gr.verb} ${p}`;
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) ?? '';
276
256
  }
277
- if (gr.verb === 'Read')
278
- return `Read ${gr.items.length} files`;
279
- if (gr.verb === 'Searched')
280
- return `Searched ${gr.items.length} patterns`;
281
- if (gr.verb === 'Ran')
282
- return `Ran ${gr.items.length} commands`;
283
- if (gr.verb === 'Edited')
284
- return `Edited ${gr.items.length} files`;
285
- return `${gr.verb} ${gr.items.length} items`;
286
- })();
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`; })();
287
269
  const right = gr.status === 'running' ? `${(elapsed / 1000).toFixed(1)}s` : gr.status === 'error' ? '✗' : `${gr.totalMs}ms`;
288
270
  const marker = isExpanded ? '▼' : '✓';
289
- const markerColor = gr.status === 'error' ? tokens.ansi.err : gr.status === 'running' ? tokens.ansi.warn : tokens.ansi.ok;
290
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: markerColor, children: [marker, " ", verbLine] }), _jsxs(Text, { color: tokens.ansi.dim, children: [" ", right] })] }), isExpanded ? gr.items.map((it) => {
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 => {
291
273
  let friendly = '';
292
274
  try {
293
275
  const a = JSON.parse(it.args);
294
- const p = a.path ?? a.pattern ?? a.command ?? '';
295
- const short = p ? String(p).split('/').pop()?.slice(0, 40) ?? p : '';
296
- if (it.name === 'read_file' && short)
297
- friendly = `${short}`;
298
- else if (it.name === 'shell_exec' && p)
299
- friendly = `$ ${String(p).slice(0, 40)}`;
300
- else if (short)
301
- friendly = short;
302
- else
303
- friendly = it.args.slice(0, 40);
276
+ const p = a.path ?? '';
277
+ friendly = p ? p.split('/').pop() ?? p : it.args.slice(0, 40);
304
278
  }
305
279
  catch {
306
280
  friendly = it.args.slice(0, 40);
307
281
  }
308
- return (_jsxs(Box, { paddingLeft: 4, children: [_jsxs(Text, { color: tokens.ansi.guide, children: [g('end'), " "] }), _jsx(Text, { color: tokens.ansi.dim, children: friendly })] }, it.id));
282
+ return _jsx(Box, { paddingLeft: 1, children: _jsxs(Text, { color: tokens.colors.fgDim, children: ["\u2514 ", friendly] }) }, it.id);
309
283
  }) : null] }, gr.id));
310
284
  }
311
285
  const it = item;
312
286
  if (it.kind === 'text' && it.role === 'user') {
313
- 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);
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);
314
288
  }
315
289
  if (it.kind === 'text') {
316
- // prose render markdown, not raw **, with proper wrap and guide
317
- 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"] })] }), _jsx(Box, { paddingLeft: 2, flexDirection: "column", children: _jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " "] }), _jsx(Box, { flexGrow: 1, children: _jsx(MarkdownText, { text: it.text }) })] }) })] }, it.id));
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));
318
291
  }
319
292
  if (it.kind === 'error')
320
- return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.ansi.err, children: [" ", g('guide'), " \u2717 ", it.message] }) }, it.id);
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);
321
294
  if (it.kind === 'policy')
322
295
  return null;
323
296
  if (it.kind === 'file_changed')
324
- return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " ", g('editsBadge'), " ", it.path, " ", it.op] }) }, it.id);
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);
325
298
  if (it.kind === 'diff')
326
- 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));
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);
327
300
  return null;
328
- }), 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, queuedInputs.length > 0 ? (_jsx(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: queuedInputs.map((q, i) => (_jsxs(Text, { color: tokens.ansi.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.ansi.accent : tokens.ansi.guide, children: i === thumbPos ? '●' : '│' }, i))) })) : null] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: tokens.ansi.guide, children: g('rule').repeat(Math.max(10, width - 2)) }), _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: "Message Klyro\u2026" }), "\u258F"] })] }), _jsx(Text, { color: tokens.ansi.guide, children: g('rule').repeat(Math.max(10, width - 2)) })] }), _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: tokens.ansi.dim, children: [baseHints, maxOffset > 0 && isFullscreen ? ' · PgUp/Dn scroll' : ''] }), _jsxs(Text, { color: tokens.ansi.dim, children: [cost > 0 ? `$${cost.toFixed(2)} · ` : '', ctxPct > 0 ? `${ctxPct}% ctx · ` : '', status.status === 'running' ? 'auto mode on ●' : ''] })] })] }));
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" })] })] })] }));
329
302
  }
@@ -6,8 +6,9 @@ describe('App', () => {
6
6
  it('shows the empty-state hint and the status line', () => {
7
7
  const { lastFrame } = render(_jsx(App, { initialModel: "mock", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { } }));
8
8
  const out = lastFrame();
9
- expect(out).toContain('mock');
10
- expect(out).toMatch(/Message Klyro|KLYRO|Type a prompt/i);
9
+ // design.md: top bar shows claude-code + sessions, center shows Message Klyro placeholder
10
+ expect(out).toMatch(/claude-code|Sessions|Message Klyro/i);
11
+ expect(out).toMatch(/Message Klyro|Type a message/i);
11
12
  });
12
13
  it('renders initial transcript items', () => {
13
14
  const items = [
@@ -16,7 +17,7 @@ describe('App', () => {
16
17
  const { lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, initialTranscript: items }));
17
18
  const frame = lastFrame();
18
19
  expect(frame).toContain('seed');
19
- expect(frame).toMatch(/›|>/);
20
+ expect(frame).toMatch(/You|seed|›|>/);
20
21
  });
21
22
  it('honors initialStatus overrides', () => {
22
23
  const overrides = { step: 5, repairs: 3, status: 'running' };
@@ -16,11 +16,11 @@ describe('App visual snapshot', () => {
16
16
  it('renders header + statusline + transcript + input at idle', () => {
17
17
  const { lastFrame } = render(_jsx(App, { ...DEFAULT_PROPS, initialModel: "gpt-4o-mini", maxSteps: 30, initialStatus: { status: 'idle', model: 'gpt-4o-mini', step: 0, maxSteps: 30, usageInput: 0, usageOutput: 0, repairs: 0 } }));
18
18
  const frame = lastFrame();
19
- expect(frame).toContain('KLYRO');
20
- expect(frame).toContain('demo');
21
- expect(frame).toContain('gpt-4o-mini');
22
- expect(frame).toMatch(/shift\+tab|for history|Message Klyro/);
23
- expect(frame).toMatch(/KLYRO|Message Klyro|>/i);
19
+ // design.md: top bar shows ▌ claude-code, not KLYRO, and sessions/files
20
+ expect(frame).toMatch(/claude-code|KLYRO/i);
21
+ expect(frame).toMatch(/demo|Sessions|Files/i);
22
+ expect(frame).toMatch(/shift\+tab|for history|Message Klyro|Type a message/i);
23
+ expect(frame).toMatch(/Message Klyro|claude-code|>/i);
24
24
  });
25
25
  it('renders a transcript with assistant text', () => {
26
26
  const { lastFrame } = render(_jsx(App, { ...DEFAULT_PROPS, initialModel: "gpt-4o-mini", maxSteps: 30, initialStatus: { status: 'idle', model: 'gpt-4o-mini', step: 0, maxSteps: 30, usageInput: 0, usageOutput: 0, repairs: 0 }, initialTranscript: [
@@ -1,38 +1,53 @@
1
1
  /**
2
- * §2.1 Color tokens + §2.2 Glyph set TUI_DESIGN.md
3
- * Accent is Orange #E8843C (256:209, 16: yellow bold), one accent ≤5%
4
- * No backgrounds except diff viewer. fg.dim ≥4.5:1 on near-black.
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.
5
5
  */
6
6
  export declare const tokens: {
7
7
  readonly colors: {
8
- readonly accent: "#E8843C";
9
- readonly fg: "#FFFFFF";
10
- readonly soft: "#F5F5F5";
11
- readonly dim: "#9A9A9A";
12
- readonly guide: "#3A3A3A";
13
- readonly ok: "#E8843C";
14
- readonly err: "#E06C6C";
15
- readonly warn: "#E8843C";
16
- readonly info: "#FFFFFF";
17
- readonly diffAddBg: "#12250F";
18
- readonly diffDelBg: "#2A1212";
8
+ readonly bg: "#FFFFFF";
9
+ readonly bgElevated: "#FAF7F2";
10
+ readonly bgSubtle: "#F5F0E8";
11
+ 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";
19
29
  };
20
30
  readonly ansi: {
21
- readonly accent: "yellowBright";
22
- readonly accentBold: "yellowBright";
23
- readonly fg: "whiteBright";
24
- readonly soft: "white";
31
+ readonly bg: "white";
32
+ readonly fg: "black";
33
+ readonly fgMuted: "gray";
25
34
  readonly dim: "gray";
26
- readonly guide: "gray";
27
- readonly ok: "yellowBright";
28
- readonly err: "red";
29
- readonly warn: "yellowBright";
30
- readonly info: "white";
35
+ readonly orange: "yellow";
36
+ readonly orangeBright: "red";
31
37
  readonly border: "gray";
38
+ 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";
32
45
  readonly muted: "gray";
33
- readonly success: "yellowBright";
34
46
  readonly error: "red";
35
- readonly warning: "yellowBright";
47
+ readonly ok: "green";
48
+ readonly warn: "yellow";
49
+ readonly info: "blue";
50
+ readonly successAlt: "green";
36
51
  };
37
52
  };
38
53
  export declare const glyphs: {
@@ -54,15 +69,13 @@ export declare const glyphs: {
54
69
  readonly todoActive: "●";
55
70
  readonly todoDone: "✓";
56
71
  readonly todoPlan: "◇";
57
- readonly modeAccept: "";
58
- readonly modePlan: "";
59
- readonly modeAuto: "";
60
- readonly editsBadge: "";
61
- readonly dot: "·";
62
- readonly ellipsis: "";
63
- readonly meterFilled: "";
64
- readonly meterEmpty: "▱";
65
- readonly continuation: "↪";
72
+ readonly logoBar: "";
73
+ readonly dotFilled: "";
74
+ readonly dotEmpty: "";
75
+ readonly dotHalf: "";
76
+ readonly sidebar: "";
77
+ readonly inspector: "";
78
+ readonly copy: "";
66
79
  readonly brand: "◆";
67
80
  readonly compaction: "⟲";
68
81
  };
@@ -85,6 +98,10 @@ export declare const glyphAscii: {
85
98
  readonly todoActive: "[>]";
86
99
  readonly todoDone: "[x]";
87
100
  readonly todoPlan: "#";
101
+ readonly logoBar: "|";
102
+ readonly dotFilled: "*";
103
+ readonly dotEmpty: "o";
104
+ readonly dotHalf: "o";
88
105
  };
89
106
  export declare function isAsciiMode(): boolean;
90
107
  export declare function g(name: keyof typeof glyphs): string;
@@ -92,4 +109,6 @@ export declare const spacing: {
92
109
  readonly maxWidth: 120;
93
110
  readonly indent: 2;
94
111
  readonly gap: 1;
112
+ readonly sidebar: 28;
113
+ readonly inspector: 36;
95
114
  };
@@ -1,38 +1,54 @@
1
1
  /**
2
- * §2.1 Color tokens + §2.2 Glyph set TUI_DESIGN.md
3
- * Accent is Orange #E8843C (256:209, 16: yellow bold), one accent ≤5%
4
- * No backgrounds except diff viewer. fg.dim ≥4.5:1 on near-black.
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.
5
5
  */
6
6
  export const tokens = {
7
7
  colors: {
8
- accent: '#E8843C', // Orange — wordmark, prompt >, ●, thumb, selected
9
- fg: '#FFFFFF', // White — user input, headings, file names
10
- soft: '#F5F5F5', // Soft white — assistant prose
11
- dim: '#9A9A9A', // Dim white — hints, durations
12
- guide: '#3A3A3A', // Guide │
13
- ok: '#E8843C', // Orange for success check (white+orange theme)
14
- err: '#E06C6C',
15
- warn: '#E8843C', // Orange for running/spinner
16
- info: '#FFFFFF',
17
- diffAddBg: '#12250F',
18
- diffDelBg: '#2A1212',
8
+ bg: '#FFFFFF',
9
+ bgElevated: '#FAF7F2',
10
+ bgSubtle: '#F5F0E8',
11
+ 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',
19
30
  },
20
31
  ansi: {
21
- accent: 'yellowBright', // #E8843C → vivid orange
22
- accentBold: 'yellowBright',
23
- fg: 'whiteBright', // #FFFFFF — pure white
24
- soft: 'white',
32
+ bg: 'white',
33
+ fg: 'black',
34
+ fgMuted: 'gray',
25
35
  dim: 'gray',
26
- guide: 'gray',
27
- ok: 'yellowBright', // orange vivid
28
- err: 'red',
29
- warn: 'yellowBright', // spinner orange vivid
30
- info: 'white',
36
+ orange: 'yellow', // #FF6B1A → 202
37
+ orangeBright: 'red', // fallback vivid
31
38
  border: 'gray',
39
+ guide: 'gray',
40
+ success: 'green',
41
+ warning: 'yellow',
42
+ danger: 'red',
43
+ accent: 'yellow',
44
+ accentBold: 'yellowBright',
45
+ soft: 'white',
32
46
  muted: 'gray',
33
- success: 'yellowBright',
34
47
  error: 'red',
35
- warning: 'yellowBright',
48
+ ok: 'green',
49
+ warn: 'yellow',
50
+ info: 'blue',
51
+ successAlt: 'green',
36
52
  },
37
53
  };
38
54
  export const glyphs = {
@@ -54,16 +70,13 @@ export const glyphs = {
54
70
  todoActive: '●',
55
71
  todoDone: '✓',
56
72
  todoPlan: '◇',
57
- modeAccept: '',
58
- modePlan: '',
59
- modeAuto: '',
60
- editsBadge: '',
61
- dot: '·',
62
- ellipsis: '',
63
- meterFilled: '',
64
- meterEmpty: '▱',
65
- continuation: '↪',
66
- // compat
73
+ logoBar: '',
74
+ dotFilled: '',
75
+ dotEmpty: '',
76
+ dotHalf: '',
77
+ sidebar: '',
78
+ inspector: '',
79
+ copy: '',
67
80
  brand: '◆',
68
81
  compaction: '⟲',
69
82
  };
@@ -86,16 +99,17 @@ export const glyphAscii = {
86
99
  todoActive: '[>]',
87
100
  todoDone: '[x]',
88
101
  todoPlan: '#',
102
+ logoBar: '|',
103
+ dotFilled: '*',
104
+ dotEmpty: 'o',
105
+ dotHalf: 'o',
89
106
  };
90
107
  export function isAsciiMode() {
91
- return (process.env.TERM === 'dumb' ||
92
- process.env.KLYRO_ASCII === '1' ||
93
- (process.env.LANG !== undefined && !process.env.LANG.toLowerCase().includes('utf-8')) ||
94
- false);
108
+ return process.env.TERM === 'dumb' || process.env.KLYRO_ASCII === '1' || false;
95
109
  }
96
110
  export function g(name) {
97
111
  if (isAsciiMode())
98
112
  return glyphAscii[name] ?? glyphs[name];
99
113
  return glyphs[name];
100
114
  }
101
- export const spacing = { maxWidth: 120, indent: 2, gap: 1 };
115
+ export const spacing = { maxWidth: 120, indent: 2, gap: 1, sidebar: 28, inspector: 36 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
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",