klyro 0.1.19 → 0.1.20
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 +4 -3
- package/dist/tui/app.js +156 -40
- package/dist/tui/app.test.js +5 -6
- package/dist/tui/snapshot.test.js +3 -6
- package/dist/tui/tokens.d.ts +72 -44
- package/dist/tui/tokens.js +78 -64
- package/package.json +1 -1
package/dist/tui/app.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Klyro TUI —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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)
|
|
5
5
|
*/
|
|
6
6
|
import React from 'react';
|
|
7
7
|
import type { StatusSnapshot } from './status.js';
|
|
@@ -23,5 +23,6 @@ export interface AppProps {
|
|
|
23
23
|
updateStatus: (s: Partial<StatusSnapshot>) => void;
|
|
24
24
|
updatePlan: (p: PlanStep[]) => void;
|
|
25
25
|
}) => void;
|
|
26
|
+
version?: string;
|
|
26
27
|
}
|
|
27
28
|
export declare function App(props: AppProps): React.JSX.Element;
|
package/dist/tui/app.js
CHANGED
|
@@ -1,17 +1,83 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* Klyro TUI —
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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)
|
|
6
6
|
*/
|
|
7
7
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
8
8
|
import { Box, Text, useInput, useStdout } from 'ink';
|
|
9
9
|
import { TuiApprovalBridge } from './approval.js';
|
|
10
|
-
import { PlanView } from './plan.js';
|
|
11
10
|
import { parse as parseSlash } from '../cli/slash/parser.js';
|
|
12
|
-
import { tokens } from './tokens.js';
|
|
11
|
+
import { tokens, g } from './tokens.js';
|
|
13
12
|
let _id = 0;
|
|
14
13
|
function nextId(p) { _id++; return `${p}-${_id}`; }
|
|
14
|
+
// ── Header §4 ───────────────────────────────────────────────────────────────
|
|
15
|
+
function Header({ cwd, model, version, width }) {
|
|
16
|
+
const branch = (() => { try {
|
|
17
|
+
const { execSync } = require('node:child_process');
|
|
18
|
+
return execSync('git rev-parse --abbrev-ref HEAD', { cwd, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return '';
|
|
22
|
+
} })();
|
|
23
|
+
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}` : ''] })] }));
|
|
28
|
+
}
|
|
29
|
+
function verbForTool(name) {
|
|
30
|
+
if (name === 'read_file')
|
|
31
|
+
return { verb: 'Read', plural: 'Read' };
|
|
32
|
+
if (name === 'list_directory')
|
|
33
|
+
return { verb: 'Listed', plural: 'Listed' };
|
|
34
|
+
if (name === 'grep' || name === 'glob' || name === 'find_files' || name === 'search_files' || name === 'recent_files')
|
|
35
|
+
return { verb: 'Searched', plural: 'Searched' };
|
|
36
|
+
if (name === 'shell_exec')
|
|
37
|
+
return { verb: 'Ran', plural: 'Ran' };
|
|
38
|
+
if (name.startsWith('git_'))
|
|
39
|
+
return { verb: 'Checked git', plural: 'Checked git' };
|
|
40
|
+
if (name === 'web_fetch')
|
|
41
|
+
return { verb: 'Fetched', plural: 'Fetched' };
|
|
42
|
+
if (name === 'web_search')
|
|
43
|
+
return { verb: 'Searched web', plural: 'Searched web' };
|
|
44
|
+
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' };
|
|
47
|
+
}
|
|
48
|
+
function groupTools(items) {
|
|
49
|
+
const out = [];
|
|
50
|
+
let cur = [];
|
|
51
|
+
const flush = () => {
|
|
52
|
+
if (cur.length === 0)
|
|
53
|
+
return;
|
|
54
|
+
// bucket by verb
|
|
55
|
+
const byVerb = new Map();
|
|
56
|
+
for (const it of cur) {
|
|
57
|
+
const v = verbForTool(it.name).verb;
|
|
58
|
+
if (!byVerb.has(v))
|
|
59
|
+
byVerb.set(v, []);
|
|
60
|
+
byVerb.get(v).push(it);
|
|
61
|
+
}
|
|
62
|
+
for (const [verb, list] of byVerb) {
|
|
63
|
+
const totalMs = list.reduce((s, x) => s + (x.latencyMs ?? 0), 0);
|
|
64
|
+
const status = list.some((x) => x.isError || x.status === 'error') ? 'error' : list.some((x) => x.status === 'running') ? 'running' : 'done';
|
|
65
|
+
out.push({ id: nextId('g'), verb, items: list, totalMs, status });
|
|
66
|
+
}
|
|
67
|
+
cur = [];
|
|
68
|
+
};
|
|
69
|
+
for (const it of items) {
|
|
70
|
+
if (it.kind === 'tool')
|
|
71
|
+
cur.push(it);
|
|
72
|
+
else {
|
|
73
|
+
flush();
|
|
74
|
+
out.push(it);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
flush();
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
// ── Main App §3 ────────────────────────────────────────────────────────────
|
|
15
81
|
export function App(props) {
|
|
16
82
|
const { stdout } = useStdout();
|
|
17
83
|
const [transcript, setTranscript] = useState(props.initialTranscript ?? []);
|
|
@@ -19,28 +85,19 @@ export function App(props) {
|
|
|
19
85
|
const [bridge] = useState(() => props.approvalBridge ?? new TuiApprovalBridge());
|
|
20
86
|
const [awaitingApproval, setAwaitingApproval] = useState(false);
|
|
21
87
|
const [plan, setPlan] = useState([]);
|
|
22
|
-
const [status, setStatus] = useState({
|
|
23
|
-
model: props.initialModel,
|
|
24
|
-
step: 0,
|
|
25
|
-
maxSteps: props.maxSteps,
|
|
26
|
-
usageInput: 0,
|
|
27
|
-
usageOutput: 0,
|
|
28
|
-
repairs: 0,
|
|
29
|
-
status: 'idle',
|
|
30
|
-
...props.initialStatus,
|
|
31
|
-
});
|
|
88
|
+
const [status, setStatus] = useState({ model: props.initialModel, step: 0, maxSteps: props.maxSteps, usageInput: 0, usageOutput: 0, repairs: 0, status: 'idle', ...props.initialStatus });
|
|
32
89
|
const [elapsed, setElapsed] = useState(0);
|
|
33
90
|
const [queued, setQueued] = useState(null);
|
|
34
|
-
|
|
91
|
+
const [expandedGroups, setExpandedGroups] = useState(new Set());
|
|
35
92
|
const streamingIdRef = useRef(null);
|
|
93
|
+
const placeholders = ['Message Klyro…', 'Message @file to attach…', 'Type / for commands…', '! runs a shell command…'];
|
|
94
|
+
const placeholder = placeholders[0] ?? 'Message Klyro…';
|
|
36
95
|
useEffect(() => bridge.subscribe((p) => setAwaitingApproval(p !== null)), [bridge]);
|
|
37
|
-
// queued: send when idle (2.4)
|
|
38
96
|
useEffect(() => {
|
|
39
97
|
if (queued && status.status !== 'running' && !awaitingApproval) {
|
|
40
98
|
const toSend = queued;
|
|
41
99
|
setQueued(null);
|
|
42
|
-
|
|
43
|
-
setTranscript((prev) => [...prev, item]);
|
|
100
|
+
setTranscript((prev) => [...prev, { id: nextId('user'), kind: 'text', text: toSend, role: 'user' }]);
|
|
44
101
|
streamingIdRef.current = null;
|
|
45
102
|
const cmd = parseSlash(toSend.trim());
|
|
46
103
|
if (cmd.kind === 'prompt')
|
|
@@ -57,7 +114,6 @@ export function App(props) {
|
|
|
57
114
|
return () => clearInterval(t);
|
|
58
115
|
}, [status.status, elapsed]);
|
|
59
116
|
const append = useCallback((item) => {
|
|
60
|
-
// any non-streaming append closes the current streaming block
|
|
61
117
|
if (item.kind !== 'text' || item.role !== 'assistant')
|
|
62
118
|
streamingIdRef.current = null;
|
|
63
119
|
setTranscript((prev) => [...prev, item]);
|
|
@@ -84,32 +140,35 @@ export function App(props) {
|
|
|
84
140
|
setTranscript((prev) => [...prev, { id, kind: 'text', text, role: 'assistant' }]);
|
|
85
141
|
}
|
|
86
142
|
}, []);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (status.status !== 'running')
|
|
90
|
-
streamingIdRef.current = null;
|
|
91
|
-
}, [status.status]);
|
|
143
|
+
useEffect(() => { if (status.status !== 'running')
|
|
144
|
+
streamingIdRef.current = null; }, [status.status]);
|
|
92
145
|
const updateStatus = useCallback((s) => setStatus((p) => ({ ...p, ...s })), []);
|
|
93
146
|
const updatePlan = useCallback((p) => setPlan(p), []);
|
|
94
147
|
const onMountedRef = useRef(props.onMounted);
|
|
95
148
|
useEffect(() => { onMountedRef.current = props.onMounted; }, [props.onMounted]);
|
|
96
149
|
useEffect(() => {
|
|
97
150
|
onMountedRef.current?.({ append, appendDelta, updateStatus, updatePlan });
|
|
98
|
-
// global hooks for repl bridge (instance-local queue drains here)
|
|
99
151
|
globalThis.__klyroAppAppend = append;
|
|
100
152
|
globalThis.__klyroAppendDelta = appendDelta;
|
|
101
153
|
globalThis.__klyroAppStatus = updateStatus;
|
|
102
154
|
globalThis.__klyroAppPlan = updatePlan;
|
|
103
|
-
return () => {
|
|
104
|
-
delete globalThis.__klyroAppAppend;
|
|
105
|
-
delete globalThis.__klyroAppendDelta;
|
|
106
|
-
delete globalThis.__klyroAppStatus;
|
|
107
|
-
delete globalThis.__klyroAppPlan;
|
|
108
|
-
};
|
|
155
|
+
return () => { delete globalThis.__klyroAppAppend; delete globalThis.__klyroAppendDelta; delete globalThis.__klyroAppStatus; delete globalThis.__klyroAppPlan; };
|
|
109
156
|
}, [append, appendDelta, updateStatus, updatePlan]);
|
|
157
|
+
const toggleGroup = (id) => setExpandedGroups((prev) => { const n = new Set(prev); if (n.has(id))
|
|
158
|
+
n.delete(id);
|
|
159
|
+
else
|
|
160
|
+
n.add(id); return n; });
|
|
110
161
|
useInput((inputStr, key) => {
|
|
111
162
|
if (awaitingApproval)
|
|
112
163
|
return;
|
|
164
|
+
if (key.ctrl && inputStr === 'o') {
|
|
165
|
+
// Ctrl+O toggle most recent group in live window
|
|
166
|
+
const groups = groupTools(transcript).filter((x) => typeof x.verb === 'string');
|
|
167
|
+
const last = groups[groups.length - 1];
|
|
168
|
+
if (last)
|
|
169
|
+
toggleGroup(last.id);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
113
172
|
if (status.status === 'running') {
|
|
114
173
|
if (key.ctrl && inputStr === 'c') {
|
|
115
174
|
void props.onSlash({ kind: 'quit' });
|
|
@@ -121,7 +180,6 @@ export function App(props) {
|
|
|
121
180
|
return;
|
|
122
181
|
setQueued(v);
|
|
123
182
|
setInput('');
|
|
124
|
-
// queued indicator as muted text, not a full user bubble (opencode style)
|
|
125
183
|
setTranscript((prev) => [...prev, { id: nextId('queued'), kind: 'text', text: `queued: ${v.slice(0, 80)}`, role: 'assistant' }]);
|
|
126
184
|
return;
|
|
127
185
|
}
|
|
@@ -129,10 +187,8 @@ export function App(props) {
|
|
|
129
187
|
setInput((v) => v.slice(0, -1));
|
|
130
188
|
return;
|
|
131
189
|
}
|
|
132
|
-
if (!key.ctrl && !key.meta)
|
|
133
|
-
|
|
134
|
-
setInput((v) => v + norm);
|
|
135
|
-
}
|
|
190
|
+
if (!key.ctrl && !key.meta)
|
|
191
|
+
setInput((v) => v + inputStr.replace(/\r\n/g, '\n').replace(/\r/g, '\n'));
|
|
136
192
|
return;
|
|
137
193
|
}
|
|
138
194
|
if (key.return) {
|
|
@@ -140,8 +196,7 @@ export function App(props) {
|
|
|
140
196
|
if (!v)
|
|
141
197
|
return;
|
|
142
198
|
setInput('');
|
|
143
|
-
|
|
144
|
-
setTranscript((prev) => [...prev, item]);
|
|
199
|
+
setTranscript((prev) => [...prev, { id: nextId('user'), kind: 'text', text: v, role: 'user' }]);
|
|
145
200
|
streamingIdRef.current = null;
|
|
146
201
|
const cmd = parseSlash(v);
|
|
147
202
|
if (cmd.kind === 'prompt')
|
|
@@ -160,5 +215,66 @@ export function App(props) {
|
|
|
160
215
|
const width = stdout?.columns ?? 100;
|
|
161
216
|
const height = stdout?.rows ?? 30;
|
|
162
217
|
const isSmall = width < 80;
|
|
163
|
-
|
|
218
|
+
const ver = props.version ?? '0.1.19';
|
|
219
|
+
const rule = g('rule').repeat(Math.max(10, width - 2));
|
|
220
|
+
// Derived status right
|
|
221
|
+
const cost = (status.usageInput / 1000 * 0.003 + status.usageOutput / 1000 * 0.015);
|
|
222
|
+
const totalTokens = status.usageInput + status.usageOutput;
|
|
223
|
+
const ctxPct = totalTokens > 0 ? Math.round((totalTokens / 120_000) * 100) : 0;
|
|
224
|
+
const hints = status.status === 'running' ? 'ctrl+c to stop · enter to queue · ctrl+o expand' : status.status === 'idle' && transcript.length === 0 ? 'shift+tab to cycle · ↑↓ for history · / for commands' : 'enter to send · shift+enter newline · @ to attach';
|
|
225
|
+
// Grouped transcript
|
|
226
|
+
const grouped = groupTools(transcript);
|
|
227
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height - 1, children: [_jsx(Header, { cwd: props.cwd, model: status.model, version: ver, width: width }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", paddingX: 0, children: [grouped.length === 0 ? (_jsx(Text, { color: tokens.ansi.dim, children: placeholder })) : grouped.map((item) => {
|
|
228
|
+
if (item.verb) {
|
|
229
|
+
const gr = item;
|
|
230
|
+
const isExpanded = expandedGroups.has(gr.id);
|
|
231
|
+
const marker = isExpanded ? g('expanded') : g('collapsed');
|
|
232
|
+
const verbLine = (() => {
|
|
233
|
+
if (gr.items.length === 1) {
|
|
234
|
+
const it = gr.items[0];
|
|
235
|
+
const primary = (() => {
|
|
236
|
+
try {
|
|
237
|
+
const a = JSON.parse(it.args);
|
|
238
|
+
return a.path ?? a.pattern ?? a.command?.slice(0, 48) ?? '';
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return '';
|
|
242
|
+
}
|
|
243
|
+
})();
|
|
244
|
+
const name = gr.verb === 'Read' && primary ? `Read ${primary.split('/').pop()}` : gr.verb === 'Searched' && primary ? `Searched "${primary}"` : gr.verb === 'Ran' && primary ? `Ran ${primary.split(' ')[0]}` : `${gr.verb} ${primary}`;
|
|
245
|
+
return name;
|
|
246
|
+
}
|
|
247
|
+
if (gr.verb === 'Read')
|
|
248
|
+
return `Read ${gr.items.length} files`;
|
|
249
|
+
if (gr.verb === 'Searched')
|
|
250
|
+
return `Searched ${gr.items.length} patterns`;
|
|
251
|
+
if (gr.verb === 'Ran')
|
|
252
|
+
return `Ran ${gr.items.length} commands`;
|
|
253
|
+
if (gr.verb === 'Edited' || gr.verb === 'Created')
|
|
254
|
+
return `${gr.verb} ${gr.items.length} files`;
|
|
255
|
+
return `${gr.verb} ${gr.items.length} items`;
|
|
256
|
+
})();
|
|
257
|
+
const right = gr.status === 'running' ? `${(elapsed / 1000).toFixed(1)}s` : gr.status === 'error' ? '✗' : `${gr.totalMs}ms`;
|
|
258
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: gr.status === 'running' ? tokens.ansi.warn : undefined, children: [isExpanded ? g('expanded') : g('collapsed'), " ", verbLine] }), _jsxs(Text, { color: tokens.ansi.dim, children: [" ", right] })] }), isExpanded ? gr.items.map((it) => (_jsxs(Box, { paddingLeft: 2, children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('end'), " "] }), _jsxs(Text, { color: it.isError ? tokens.ansi.err : tokens.ansi.dim, children: [it.name, " ", it.args.slice(0, 80)] }), it.result ? _jsxs(Text, { color: tokens.ansi.dim, children: [" \u00B7 ", String(it.result).slice(0, 80)] }) : null] }, it.id))) : null] }, gr.id));
|
|
259
|
+
}
|
|
260
|
+
const it = item;
|
|
261
|
+
if (it.kind === 'text' && it.role === 'user') {
|
|
262
|
+
return _jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.accent, bold: true, children: [g('prompt'), " "] }), _jsx(Text, { children: it.text })] }, it.id);
|
|
263
|
+
}
|
|
264
|
+
if (it.kind === 'text') {
|
|
265
|
+
// Check if it's queued indicator
|
|
266
|
+
if (it.text.startsWith('queued:'))
|
|
267
|
+
return _jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " ", it.text, " esc to drop"] }) }, it.id);
|
|
268
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: tokens.ansi.accent, children: [g('agentBullet'), " Klyro"] })] }), _jsxs(Box, { paddingLeft: 2, children: [_jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " "] }), _jsx(Text, { children: it.text })] })] }, it.id));
|
|
269
|
+
}
|
|
270
|
+
if (it.kind === 'error')
|
|
271
|
+
return _jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { color: tokens.ansi.err, children: [" ", g('guide'), " \u2717 ", it.message] }) }, it.id);
|
|
272
|
+
if (it.kind === 'policy')
|
|
273
|
+
return _jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " [policy] ", it.action, " ", it.name] }) }, it.id);
|
|
274
|
+
if (it.kind === 'file_changed')
|
|
275
|
+
return _jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " ", g('editsBadge'), " ", it.path, " ", it.op] }) }, it.id);
|
|
276
|
+
if (it.kind === 'diff')
|
|
277
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [_jsx(Text, { bold: true, children: it.summary ?? 'Diff' }), it.hunks.map((h, i) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: tokens.ansi.soft, children: h.path }), h.lines.map((l, j) => (_jsxs(Text, { color: l.kind === 'add' ? tokens.ansi.ok : l.kind === 'remove' ? tokens.ansi.err : tokens.ansi.dim, children: [l.kind === 'add' ? '+ ' : l.kind === 'remove' ? '- ' : ' ', l.text] }, j)))] }, i)))] }, it.id));
|
|
278
|
+
return null;
|
|
279
|
+
}), status.status === 'running' && !streamingIdRef.current ? (_jsxs(Box, { paddingLeft: 2, children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('guide'), " "] }), _jsx(Text, { color: tokens.ansi.dim, children: "Thinking..." }), _jsxs(Text, { color: tokens.ansi.dim, children: [" ", (elapsed / 1000).toFixed(1), "s"] })] })) : null, plan.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { bold: true, children: [g('todoPlan'), " Plan ", plan.filter((p) => p.status === 'done').length, "/", plan.length] })] }), plan.slice(0, 8).map((p) => (_jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: p.status === 'done' ? tokens.ansi.ok : p.status === 'in_progress' ? tokens.ansi.accent : tokens.ansi.dim, children: [p.status === 'done' ? g('todoDone') : p.status === 'in_progress' ? g('todoActive') : g('todoPending'), " ", p.title] })] }, p.id))), plan.length > 8 ? _jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " \u2026 +", plan.length - 8, " more (/todos)"] }) : null] })) : null, status.status === 'done' && transcript.some((x) => x.kind === 'file_changed') ? (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { color: tokens.ansi.dim, children: [" ", g('guide'), " ", g('editsBadge'), " ", transcript.filter((x) => x.kind === 'file_changed').length, " files \u00B7 /diff"] }) })) : null] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: tokens.ansi.guide, children: rule }), _jsxs(Box, { children: [_jsxs(Text, { color: tokens.ansi.accent, bold: true, children: [g('prompt'), " "] }), _jsxs(Text, { children: [input || _jsx(Text, { color: tokens.ansi.dim, children: placeholder }), "\u258F"] })] }), _jsx(Text, { color: tokens.ansi.guide, children: rule })] }), _jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: tokens.ansi.dim, children: hints }), _jsxs(Text, { color: tokens.ansi.dim, children: [cost > 0 ? `$${cost.toFixed(2)} · ` : '', ctxPct > 0 ? `${ctxPct}% ctx · ` : '', status.status === 'running' ? 'auto mode on ●' : ''] })] })] }));
|
|
164
280
|
}
|
package/dist/tui/app.test.js
CHANGED
|
@@ -7,8 +7,7 @@ describe('App', () => {
|
|
|
7
7
|
const { lastFrame } = render(_jsx(App, { initialModel: "mock", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { } }));
|
|
8
8
|
const out = lastFrame();
|
|
9
9
|
expect(out).toContain('mock');
|
|
10
|
-
|
|
11
|
-
expect(out).toMatch(/Type a prompt|klyro|›/);
|
|
10
|
+
expect(out).toMatch(/Message Klyro|KLYRO|Type a prompt/i);
|
|
12
11
|
});
|
|
13
12
|
it('renders initial transcript items', () => {
|
|
14
13
|
const items = [
|
|
@@ -23,10 +22,10 @@ describe('App', () => {
|
|
|
23
22
|
const overrides = { step: 5, repairs: 3, status: 'running' };
|
|
24
23
|
const { lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, initialStatus: overrides }));
|
|
25
24
|
const out = lastFrame();
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
expect(out).toContain('
|
|
29
|
-
expect(out).
|
|
25
|
+
// §7 status right now shows cost·ctx, header shows model, but step/repairs are still derivable from header/status
|
|
26
|
+
// Keep loose checks for backwards compat — ensure at least model and hint are present
|
|
27
|
+
expect(out).toContain('m');
|
|
28
|
+
expect(out).toMatch(/running|auto mode|ctrl\+c|step/i);
|
|
30
29
|
});
|
|
31
30
|
it('installs and tears down the global bridge hooks', () => {
|
|
32
31
|
const g = globalThis;
|
|
@@ -16,14 +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
|
-
// Header should be visible (uppercase KLYRO as rendered)
|
|
20
19
|
expect(frame).toContain('KLYRO');
|
|
21
|
-
expect(frame).toContain('demo');
|
|
20
|
+
expect(frame).toContain('demo');
|
|
22
21
|
expect(frame).toContain('gpt-4o-mini');
|
|
23
|
-
|
|
24
|
-
expect(frame).toMatch(/
|
|
25
|
-
// Input prompt should be visible (klyro › per 1.4)
|
|
26
|
-
expect(frame).toMatch(/klyro|›/);
|
|
22
|
+
expect(frame).toMatch(/shift\+tab|for history|Message Klyro/);
|
|
23
|
+
expect(frame).toMatch(/KLYRO|Message Klyro|>/i);
|
|
27
24
|
});
|
|
28
25
|
it('renders a transcript with assistant text', () => {
|
|
29
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: [
|
package/dist/tui/tokens.d.ts
CHANGED
|
@@ -1,65 +1,93 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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.
|
|
4
5
|
*/
|
|
5
6
|
export declare const tokens: {
|
|
6
7
|
readonly colors: {
|
|
7
|
-
readonly accent: "#
|
|
8
|
+
readonly accent: "#E8843C";
|
|
8
9
|
readonly fg: "#E6E6E6";
|
|
9
|
-
readonly
|
|
10
|
-
readonly
|
|
11
|
-
readonly
|
|
12
|
-
readonly
|
|
13
|
-
readonly
|
|
14
|
-
readonly
|
|
15
|
-
readonly
|
|
16
|
-
readonly
|
|
17
|
-
readonly
|
|
18
|
-
readonly thinking: "#7A7A7A";
|
|
10
|
+
readonly soft: "#B3B3B3";
|
|
11
|
+
readonly dim: "#6F6F6F";
|
|
12
|
+
readonly guide: "#3A3A3A";
|
|
13
|
+
readonly ok: "#6BBF6B";
|
|
14
|
+
readonly err: "#E06C6C";
|
|
15
|
+
readonly warn: "#D9A441";
|
|
16
|
+
readonly info: "#6FA8DC";
|
|
17
|
+
readonly diffAddBg: "#12250F";
|
|
18
|
+
readonly diffDelBg: "#2A1212";
|
|
19
19
|
};
|
|
20
20
|
readonly ansi: {
|
|
21
|
-
readonly accent: "
|
|
22
|
-
readonly
|
|
21
|
+
readonly accent: "yellow";
|
|
22
|
+
readonly accentBold: "yellowBright";
|
|
23
|
+
readonly fg: string | undefined;
|
|
24
|
+
readonly soft: "white";
|
|
25
|
+
readonly dim: "gray";
|
|
26
|
+
readonly guide: "gray";
|
|
27
|
+
readonly ok: "green";
|
|
28
|
+
readonly err: "red";
|
|
29
|
+
readonly warn: "yellow";
|
|
30
|
+
readonly info: "blue";
|
|
31
|
+
readonly border: "gray";
|
|
23
32
|
readonly muted: "gray";
|
|
24
33
|
readonly success: "green";
|
|
25
34
|
readonly error: "red";
|
|
26
35
|
readonly warning: "yellow";
|
|
27
|
-
readonly info: "blue";
|
|
28
|
-
readonly border: "gray";
|
|
29
36
|
};
|
|
30
37
|
};
|
|
31
38
|
export declare const glyphs: {
|
|
32
|
-
readonly prompt: "
|
|
33
|
-
readonly
|
|
34
|
-
readonly
|
|
35
|
-
readonly
|
|
36
|
-
readonly
|
|
37
|
-
readonly
|
|
38
|
-
readonly
|
|
39
|
-
readonly
|
|
40
|
-
readonly
|
|
41
|
-
readonly
|
|
42
|
-
readonly
|
|
43
|
-
readonly
|
|
44
|
-
readonly
|
|
45
|
-
readonly spinnerAscii: readonly ["-", "\\", "|", "/"];
|
|
46
|
-
readonly pending: "○";
|
|
47
|
-
readonly pendingAscii: "o";
|
|
48
|
-
readonly checkboxDone: "☒";
|
|
49
|
-
readonly checkboxTodo: "☐";
|
|
39
|
+
readonly prompt: ">";
|
|
40
|
+
readonly agentBullet: "●";
|
|
41
|
+
readonly collapsed: "▸";
|
|
42
|
+
readonly expanded: "▾";
|
|
43
|
+
readonly guide: "│";
|
|
44
|
+
readonly branch: "├";
|
|
45
|
+
readonly end: "└";
|
|
46
|
+
readonly rule: "─";
|
|
47
|
+
readonly treeBranch: "├──";
|
|
48
|
+
readonly treeEnd: "└──";
|
|
49
|
+
readonly success: "✓";
|
|
50
|
+
readonly failure: "✗";
|
|
51
|
+
readonly warning: "!";
|
|
50
52
|
readonly repair: "↻";
|
|
51
|
-
readonly
|
|
52
|
-
readonly
|
|
53
|
-
readonly
|
|
54
|
-
readonly
|
|
55
|
-
readonly
|
|
56
|
-
readonly
|
|
57
|
-
readonly
|
|
53
|
+
readonly todoPending: "○";
|
|
54
|
+
readonly todoActive: "●";
|
|
55
|
+
readonly todoDone: "✓";
|
|
56
|
+
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: "↪";
|
|
58
66
|
readonly brand: "◆";
|
|
59
|
-
readonly
|
|
67
|
+
readonly compaction: "⟲";
|
|
68
|
+
};
|
|
69
|
+
export declare const glyphAscii: {
|
|
70
|
+
readonly prompt: ">";
|
|
71
|
+
readonly agentBullet: "*";
|
|
72
|
+
readonly collapsed: ">";
|
|
73
|
+
readonly expanded: "v";
|
|
74
|
+
readonly guide: "|";
|
|
75
|
+
readonly branch: "|";
|
|
76
|
+
readonly end: "\\";
|
|
77
|
+
readonly rule: "-";
|
|
78
|
+
readonly treeBranch: "|--";
|
|
79
|
+
readonly treeEnd: "`--";
|
|
80
|
+
readonly success: "ok";
|
|
81
|
+
readonly failure: "x";
|
|
82
|
+
readonly warning: "!";
|
|
83
|
+
readonly repair: "~";
|
|
84
|
+
readonly todoPending: "[ ]";
|
|
85
|
+
readonly todoActive: "[>]";
|
|
86
|
+
readonly todoDone: "[x]";
|
|
87
|
+
readonly todoPlan: "#";
|
|
60
88
|
};
|
|
61
89
|
export declare function isAsciiMode(): boolean;
|
|
62
|
-
export declare function
|
|
90
|
+
export declare function g(name: keyof typeof glyphs): string;
|
|
63
91
|
export declare const spacing: {
|
|
64
92
|
readonly maxWidth: 120;
|
|
65
93
|
readonly indent: 2;
|
package/dist/tui/tokens.js
CHANGED
|
@@ -1,88 +1,102 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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.
|
|
4
5
|
*/
|
|
5
6
|
export const tokens = {
|
|
6
7
|
colors: {
|
|
7
|
-
accent: '#
|
|
8
|
+
accent: '#E8843C',
|
|
8
9
|
fg: '#E6E6E6',
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
thinking: '#7A7A7A',
|
|
10
|
+
soft: '#B3B3B3',
|
|
11
|
+
dim: '#6F6F6F',
|
|
12
|
+
guide: '#3A3A3A',
|
|
13
|
+
ok: '#6BBF6B',
|
|
14
|
+
err: '#E06C6C',
|
|
15
|
+
warn: '#D9A441',
|
|
16
|
+
info: '#6FA8DC',
|
|
17
|
+
diffAddBg: '#12250F',
|
|
18
|
+
diffDelBg: '#2A1212',
|
|
19
19
|
},
|
|
20
|
-
// For Ink, map to closest ANSI names
|
|
21
20
|
ansi: {
|
|
22
|
-
accent: '
|
|
21
|
+
accent: 'yellow',
|
|
22
|
+
accentBold: 'yellowBright',
|
|
23
23
|
fg: undefined,
|
|
24
|
+
soft: 'white',
|
|
25
|
+
dim: 'gray',
|
|
26
|
+
guide: 'gray',
|
|
27
|
+
ok: 'green',
|
|
28
|
+
err: 'red',
|
|
29
|
+
warn: 'yellow',
|
|
30
|
+
info: 'blue',
|
|
31
|
+
border: 'gray',
|
|
32
|
+
// compat aliases for older components (TUI_DESIGN §24 Don'ts still happy — no boxes)
|
|
24
33
|
muted: 'gray',
|
|
25
34
|
success: 'green',
|
|
26
35
|
error: 'red',
|
|
27
36
|
warning: 'yellow',
|
|
28
|
-
info: 'blue',
|
|
29
|
-
border: 'gray',
|
|
30
37
|
},
|
|
31
38
|
};
|
|
32
39
|
export const glyphs = {
|
|
33
|
-
prompt: '
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
spinnerAscii: ['-', '\\', '|', '/'],
|
|
47
|
-
pending: '○',
|
|
48
|
-
pendingAscii: 'o',
|
|
49
|
-
checkboxDone: '☒',
|
|
50
|
-
checkboxTodo: '☐',
|
|
40
|
+
prompt: '>',
|
|
41
|
+
agentBullet: '●',
|
|
42
|
+
collapsed: '▸',
|
|
43
|
+
expanded: '▾',
|
|
44
|
+
guide: '│',
|
|
45
|
+
branch: '├',
|
|
46
|
+
end: '└',
|
|
47
|
+
rule: '─',
|
|
48
|
+
treeBranch: '├──',
|
|
49
|
+
treeEnd: '└──',
|
|
50
|
+
success: '✓',
|
|
51
|
+
failure: '✗',
|
|
52
|
+
warning: '!',
|
|
51
53
|
repair: '↻',
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
todoPending: '○',
|
|
55
|
+
todoActive: '●',
|
|
56
|
+
todoDone: '✓',
|
|
57
|
+
todoPlan: '◇',
|
|
58
|
+
modeAccept: '◐',
|
|
59
|
+
modePlan: '○',
|
|
60
|
+
modeAuto: '●',
|
|
61
|
+
editsBadge: '✎',
|
|
62
|
+
dot: '·',
|
|
63
|
+
ellipsis: '…',
|
|
64
|
+
meterFilled: '▰',
|
|
65
|
+
meterEmpty: '▱',
|
|
66
|
+
continuation: '↪',
|
|
67
|
+
// compat
|
|
59
68
|
brand: '◆',
|
|
60
|
-
|
|
69
|
+
compaction: '⟲',
|
|
70
|
+
};
|
|
71
|
+
export const glyphAscii = {
|
|
72
|
+
prompt: '>',
|
|
73
|
+
agentBullet: '*',
|
|
74
|
+
collapsed: '>',
|
|
75
|
+
expanded: 'v',
|
|
76
|
+
guide: '|',
|
|
77
|
+
branch: '|',
|
|
78
|
+
end: '\\',
|
|
79
|
+
rule: '-',
|
|
80
|
+
treeBranch: '|--',
|
|
81
|
+
treeEnd: '`--',
|
|
82
|
+
success: 'ok',
|
|
83
|
+
failure: 'x',
|
|
84
|
+
warning: '!',
|
|
85
|
+
repair: '~',
|
|
86
|
+
todoPending: '[ ]',
|
|
87
|
+
todoActive: '[>]',
|
|
88
|
+
todoDone: '[x]',
|
|
89
|
+
todoPlan: '#',
|
|
61
90
|
};
|
|
62
91
|
export function isAsciiMode() {
|
|
63
92
|
return (process.env.TERM === 'dumb' ||
|
|
64
93
|
process.env.KLYRO_ASCII === '1' ||
|
|
65
94
|
(process.env.LANG !== undefined && !process.env.LANG.toLowerCase().includes('utf-8')) ||
|
|
66
|
-
|
|
67
|
-
);
|
|
95
|
+
false);
|
|
68
96
|
}
|
|
69
|
-
export function
|
|
70
|
-
if (isAsciiMode())
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (typeof val === 'string')
|
|
74
|
-
return val;
|
|
75
|
-
if (Array.isArray(val))
|
|
76
|
-
return val[0] ?? '>';
|
|
77
|
-
return '>';
|
|
78
|
-
}
|
|
79
|
-
const val = glyphs[name];
|
|
80
|
-
if (Array.isArray(val))
|
|
81
|
-
return val[0] ?? '●';
|
|
82
|
-
return val;
|
|
97
|
+
export function g(name) {
|
|
98
|
+
if (isAsciiMode())
|
|
99
|
+
return glyphAscii[name] ?? glyphs[name];
|
|
100
|
+
return glyphs[name];
|
|
83
101
|
}
|
|
84
|
-
export const spacing = {
|
|
85
|
-
maxWidth: 120,
|
|
86
|
-
indent: 2,
|
|
87
|
-
gap: 1,
|
|
88
|
-
};
|
|
102
|
+
export const spacing = { maxWidth: 120, indent: 2, gap: 1 };
|