klyro 0.1.39 → 0.1.41
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 +2 -2
- package/dist/tui/app.js +125 -36
- package/dist/tui/app.test.js +142 -0
- package/dist/tui/tokens.d.ts +41 -41
- package/dist/tui/tokens.js +47 -50
- package/package.json +1 -1
package/dist/tui/app.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Klyro TUI
|
|
3
|
-
* Header 3 rows, guide
|
|
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,9 +1,9 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* Klyro TUI
|
|
4
|
-
* Header 3 rows, guide
|
|
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
|
-
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
6
|
+
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
7
7
|
import { Box, Text, useInput, useStdout } from 'ink';
|
|
8
8
|
import { TuiApprovalBridge } from './approval.js';
|
|
9
9
|
import { parse as parseSlash } from '../cli/slash/parser.js';
|
|
@@ -19,7 +19,7 @@ function Header({ cwd, model, version, width }) {
|
|
|
19
19
|
return '';
|
|
20
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.colors.accent, children: ["KLYRO v", version] }), showLinks ? _jsx(Text, { color: tokens.colors.dim, children: "
|
|
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}` : ''] })] }));
|
|
23
23
|
}
|
|
24
24
|
function verbForTool(name) {
|
|
25
25
|
if (name === 'read_file')
|
|
@@ -71,7 +71,7 @@ function groupTools(items) {
|
|
|
71
71
|
flush();
|
|
72
72
|
return out;
|
|
73
73
|
}
|
|
74
|
-
// Simple markdown: **bold**
|
|
74
|
+
// Simple markdown: **bold** → bold, keep lists/tables, wrap at word boundaries
|
|
75
75
|
function MarkdownText({ text, dim, width }) {
|
|
76
76
|
// Split by **bold** segments
|
|
77
77
|
const parts = [];
|
|
@@ -89,9 +89,82 @@ function MarkdownText({ text, dim, width }) {
|
|
|
89
89
|
parts.push(_jsx(Text, { color: dim ? tokens.colors.dim : undefined, wrap: "wrap", children: text.slice(last) }, `t-${idx++}`));
|
|
90
90
|
if (parts.length === 0)
|
|
91
91
|
return _jsx(Text, { color: dim ? tokens.colors.dim : undefined, wrap: "wrap", children: text });
|
|
92
|
-
// Render as single line with bold segments
|
|
92
|
+
// Render as single line with bold segments — Ink will wrap the parent Box
|
|
93
93
|
return _jsx(Text, { wrap: "wrap", children: parts });
|
|
94
94
|
}
|
|
95
|
+
// Chat scroll state: scrollOffset, pinned (user scrolled away from bottom),
|
|
96
|
+
// pendingNew (rows arrived while pinned), and a commands bag for key handlers.
|
|
97
|
+
// The `tick` prop is a monotonic value that increments on every content mutation,
|
|
98
|
+
// including in-place text growth during streaming (appendDelta mutates by index,
|
|
99
|
+
// so transcript.length does not change on a delta — the effect must fire anyway).
|
|
100
|
+
function useChatScroll(opts) {
|
|
101
|
+
const { totalRows, viewportH, messageBoundaries, tick } = opts;
|
|
102
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
103
|
+
const [pinned, setPinned] = useState(false);
|
|
104
|
+
const [pendingNew, setPendingNew] = useState(0);
|
|
105
|
+
const pinnedRef = useRef(false);
|
|
106
|
+
const maxOffset = Math.max(0, totalRows - viewportH);
|
|
107
|
+
// 1-line tolerance: maxOffset can shift by 1 during streaming and leave us
|
|
108
|
+
// at maxOffset - 1, which would otherwise be "not at bottom". The +1 tolerance
|
|
109
|
+
// keeps follow-tail engaged through that off-by-one.
|
|
110
|
+
const isAtBottom = scrollOffset + 1 >= maxOffset;
|
|
111
|
+
const recomputePinned = useCallback((next) => {
|
|
112
|
+
const atBottom = next + 1 >= maxOffset;
|
|
113
|
+
pinnedRef.current = !atBottom;
|
|
114
|
+
setPinned(!atBottom);
|
|
115
|
+
if (atBottom)
|
|
116
|
+
setPendingNew(0);
|
|
117
|
+
}, [maxOffset]);
|
|
118
|
+
// Watch `tick` — fires on every content change (add, remove, in-place delta).
|
|
119
|
+
const lastTickRef = useRef(tick);
|
|
120
|
+
const lastMaxOffsetRef = useRef(maxOffset);
|
|
121
|
+
const firstEffectRef = useRef(true);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (firstEffectRef.current) {
|
|
124
|
+
// Initial mount: if there's content, follow the tail (preserves the
|
|
125
|
+
// pre-refactor behavior where scrollOffset was 0 only on empty state).
|
|
126
|
+
firstEffectRef.current = false;
|
|
127
|
+
lastTickRef.current = tick;
|
|
128
|
+
lastMaxOffsetRef.current = maxOffset;
|
|
129
|
+
if (maxOffset > 0) {
|
|
130
|
+
setScrollOffset(maxOffset);
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (tick === lastTickRef.current)
|
|
135
|
+
return;
|
|
136
|
+
lastTickRef.current = tick;
|
|
137
|
+
const grew = maxOffset - lastMaxOffsetRef.current;
|
|
138
|
+
lastMaxOffsetRef.current = maxOffset;
|
|
139
|
+
if (pinnedRef.current) {
|
|
140
|
+
if (grew > 0)
|
|
141
|
+
setPendingNew((p) => p + grew);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
// FollowTail: snap to the new bottom.
|
|
145
|
+
setScrollOffset(maxOffset);
|
|
146
|
+
}
|
|
147
|
+
}, [tick, maxOffset]);
|
|
148
|
+
const commands = {
|
|
149
|
+
lineUp: () => { const next = Math.max(0, scrollOffset - 1); setScrollOffset(next); recomputePinned(next); },
|
|
150
|
+
lineDown: () => { const next = Math.min(maxOffset, scrollOffset + 1); setScrollOffset(next); recomputePinned(next); },
|
|
151
|
+
pageUp: () => {
|
|
152
|
+
const prev = [...messageBoundaries].reverse().find((b) => b < scrollOffset);
|
|
153
|
+
const next = prev ?? Math.max(0, scrollOffset - viewportH);
|
|
154
|
+
setScrollOffset(next);
|
|
155
|
+
recomputePinned(next);
|
|
156
|
+
},
|
|
157
|
+
pageDown: () => {
|
|
158
|
+
const nxt = messageBoundaries.find((b) => b > scrollOffset);
|
|
159
|
+
const next = nxt ?? Math.min(maxOffset, scrollOffset + viewportH);
|
|
160
|
+
setScrollOffset(next);
|
|
161
|
+
recomputePinned(next);
|
|
162
|
+
},
|
|
163
|
+
jumpTop: () => { setScrollOffset(0); recomputePinned(0); },
|
|
164
|
+
jumpBottom: () => { setScrollOffset(maxOffset); recomputePinned(maxOffset); },
|
|
165
|
+
};
|
|
166
|
+
return { scrollOffset, setScrollOffset, pinned, pendingNew, isAtBottom, maxOffset, commands };
|
|
167
|
+
}
|
|
95
168
|
export function App(props) {
|
|
96
169
|
const { stdout } = useStdout();
|
|
97
170
|
const [transcript, setTranscript] = useState(props.initialTranscript ?? []);
|
|
@@ -103,7 +176,6 @@ export function App(props) {
|
|
|
103
176
|
const [elapsed, setElapsed] = useState(0);
|
|
104
177
|
const [queuedInputs, setQueuedInputs] = useState([]);
|
|
105
178
|
const [expandedGroups, setExpandedGroups] = useState(new Set());
|
|
106
|
-
const [scrollOffset, setScrollOffset] = useState(0);
|
|
107
179
|
const streamingIdRef = useRef(null);
|
|
108
180
|
const width = stdout?.columns ?? 100;
|
|
109
181
|
const height = stdout?.rows ?? 30;
|
|
@@ -111,8 +183,18 @@ export function App(props) {
|
|
|
111
183
|
const grouped = groupTools(transcript);
|
|
112
184
|
const viewportH = Math.max(5, height - 10);
|
|
113
185
|
const totalRows = grouped.length + (plan.length > 0 ? 1 : 0) + 2;
|
|
114
|
-
const
|
|
115
|
-
|
|
186
|
+
const messageBoundaries = useMemo(() => grouped.map((_, i) => i), [grouped]);
|
|
187
|
+
// Monotonic tick: increments on every render, so the scroll hook fires
|
|
188
|
+
// for every content mutation — including in-place text deltas.
|
|
189
|
+
const tickRef = useRef(0);
|
|
190
|
+
useEffect(() => { tickRef.current += 1; });
|
|
191
|
+
const scroll = useChatScroll({
|
|
192
|
+
totalRows,
|
|
193
|
+
viewportH,
|
|
194
|
+
messageBoundaries,
|
|
195
|
+
tick: tickRef.current,
|
|
196
|
+
});
|
|
197
|
+
const { scrollOffset, isAtBottom, maxOffset, pinned, pendingNew, commands } = scroll;
|
|
116
198
|
const trackH = viewportH;
|
|
117
199
|
const thumbPos = maxOffset === 0 ? 0 : Math.round((scrollOffset / maxOffset) * (trackH - 1));
|
|
118
200
|
const visibleGrouped = isFullscreen ? grouped.slice(scrollOffset, scrollOffset + viewportH) : grouped;
|
|
@@ -132,8 +214,6 @@ export function App(props) {
|
|
|
132
214
|
}, [queuedInputs, status.status, awaitingApproval]);
|
|
133
215
|
useEffect(() => { if (status.status !== 'running')
|
|
134
216
|
return; const start = Date.now() - elapsed; const t = setInterval(() => setElapsed(Date.now() - start), 1000); return () => clearInterval(t); }, [status.status, elapsed]);
|
|
135
|
-
useEffect(() => { if (isAtBottom)
|
|
136
|
-
setScrollOffset(maxOffset); }, [transcript.length, plan.length, maxOffset, isAtBottom]);
|
|
137
217
|
const append = useCallback((item) => { if (item.kind !== 'text' || item.role !== 'assistant')
|
|
138
218
|
streamingIdRef.current = null; setTranscript((prev) => [...prev, item]); }, []);
|
|
139
219
|
const appendDelta = useCallback((text) => {
|
|
@@ -160,28 +240,37 @@ export function App(props) {
|
|
|
160
240
|
n.delete(id);
|
|
161
241
|
else
|
|
162
242
|
n.add(id); return 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));
|
|
165
243
|
useInput((inputStr, key) => {
|
|
166
244
|
if (key.escape && queuedInputs.length > 0) {
|
|
167
245
|
setQueuedInputs((prev) => prev.slice(1));
|
|
168
246
|
return;
|
|
169
247
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
248
|
+
// Scroll keys (work in any mode, including while running).
|
|
249
|
+
if (isFullscreen && maxOffset > 0) {
|
|
250
|
+
if (key.home) {
|
|
251
|
+
commands.jumpTop();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (key.end) {
|
|
255
|
+
commands.jumpBottom();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (key.pageUp || (key.ctrl && inputStr === 'u')) {
|
|
259
|
+
commands.pageUp();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (key.pageDown || (key.ctrl && inputStr === 'd')) {
|
|
263
|
+
commands.pageDown();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (key.upArrow && (key.shift || key.ctrl)) {
|
|
267
|
+
commands.lineUp();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (key.downArrow && (key.shift || key.ctrl)) {
|
|
271
|
+
commands.lineDown();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
185
274
|
}
|
|
186
275
|
if (awaitingApproval)
|
|
187
276
|
return;
|
|
@@ -241,9 +330,9 @@ export function App(props) {
|
|
|
241
330
|
const cost = (status.usageInput / 1000 * 0.003 + status.usageOutput / 1000 * 0.015);
|
|
242
331
|
const totalTokens = status.usageInput + status.usageOutput;
|
|
243
332
|
const ctxPct = totalTokens > 0 ? Math.round((totalTokens / 120_000) * 100) : 0;
|
|
244
|
-
const baseHints = status.status === 'running' ? 'ctrl+c to stop
|
|
245
|
-
const hints = maxOffset > 0 && isFullscreen ? `${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
|
|
333
|
+
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';
|
|
334
|
+
const hints = maxOffset > 0 && isFullscreen ? `${baseHints} · PgUp/Dn scroll` : baseHints;
|
|
335
|
+
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
336
|
if (item.verb) {
|
|
248
337
|
const gr = item;
|
|
249
338
|
const isExpanded = expandedGroups.has(gr.id);
|
|
@@ -276,8 +365,8 @@ export function App(props) {
|
|
|
276
365
|
return `Edited ${gr.items.length} files`;
|
|
277
366
|
return `${gr.verb} ${gr.items.length} items`;
|
|
278
367
|
})();
|
|
279
|
-
const right = gr.status === 'running' ? `${(elapsed / 1000).toFixed(1)}s` : gr.status === 'error' ? '
|
|
280
|
-
const marker = isExpanded ? '
|
|
368
|
+
const right = gr.status === 'running' ? `${(elapsed / 1000).toFixed(1)}s` : gr.status === 'error' ? '✗' : `${gr.totalMs}ms`;
|
|
369
|
+
const marker = isExpanded ? '▼' : '✓';
|
|
281
370
|
const markerColor = gr.status === 'error' ? tokens.colors.err : gr.status === 'running' ? tokens.colors.warn : tokens.colors.ok;
|
|
282
371
|
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) => {
|
|
283
372
|
let friendly = '';
|
|
@@ -305,11 +394,11 @@ export function App(props) {
|
|
|
305
394
|
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);
|
|
306
395
|
}
|
|
307
396
|
if (it.kind === 'text') {
|
|
308
|
-
// prose
|
|
397
|
+
// prose — render markdown, not raw **, with proper wrap and guide
|
|
309
398
|
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));
|
|
310
399
|
}
|
|
311
400
|
if (it.kind === 'error')
|
|
312
|
-
return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.colors.err, children: [" ", g('guide'), "
|
|
401
|
+
return _jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { color: tokens.colors.err, children: [" ", g('guide'), " \u00E2\u0153\u2014 ", it.message] }) }, it.id);
|
|
313
402
|
if (it.kind === 'policy')
|
|
314
403
|
return null;
|
|
315
404
|
if (it.kind === 'file_changed')
|
|
@@ -317,5 +406,5 @@ export function App(props) {
|
|
|
317
406
|
if (it.kind === 'diff')
|
|
318
407
|
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));
|
|
319
408
|
return null;
|
|
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 ? '
|
|
409
|
+
}), 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] }), pinned && pendingNew > 0 ? (_jsx(Box, { justifyContent: "flex-end", paddingX: 1, marginTop: -1, children: _jsxs(Text, { backgroundColor: tokens.colors.accentSoft, color: tokens.colors.accent, bold: true, children: [' ↓ ', pendingNew, ' new ', pendingNew === 1 ? 'message' : 'messages', ' '] }) })) : 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 â—' : ''] })] })] }));
|
|
321
410
|
}
|
package/dist/tui/app.test.js
CHANGED
|
@@ -79,4 +79,146 @@ describe('App', () => {
|
|
|
79
79
|
const call = onSlash.mock.calls[0]?.[0];
|
|
80
80
|
expect(call?.kind).toBe('quit');
|
|
81
81
|
});
|
|
82
|
+
// --- Chat scroll behavior (TUI_DESIGN chat_scroll.md) -----------------
|
|
83
|
+
// Build a 25-item initial transcript. Each item has a unique tag so we can
|
|
84
|
+
// grep `lastFrame()` for it.
|
|
85
|
+
function makeInitialTranscript(n) {
|
|
86
|
+
const out = [];
|
|
87
|
+
for (let i = 0; i < n; i++) {
|
|
88
|
+
out.push({
|
|
89
|
+
id: `seed-${i}`,
|
|
90
|
+
kind: 'text',
|
|
91
|
+
text: `MSG-${i.toString().padStart(2, '0')}-tag`,
|
|
92
|
+
role: 'user',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
// ANSI sequences Ink's parse-keypress recognizes.
|
|
98
|
+
const KEY_HOME = '\x1b[H';
|
|
99
|
+
const KEY_END = '\x1b[F';
|
|
100
|
+
const KEY_PGUP = '\x1b[5~';
|
|
101
|
+
const KEY_PGDN = '\x1b[6~';
|
|
102
|
+
const KEY_SHIFT_UP = '\x1b[1;2A';
|
|
103
|
+
const KEY_SHIFT_DOWN = '\x1b[1;2B';
|
|
104
|
+
it('starts at the bottom (follow-tail) when initial content fills the viewport', async () => {
|
|
105
|
+
const { lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
|
|
106
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
107
|
+
const frame = lastFrame() ?? '';
|
|
108
|
+
// The viewport is 20 rows; the last few seeded items (MSG-22..MSG-24) should
|
|
109
|
+
// be in the visible window. The first item (MSG-00) should NOT be visible.
|
|
110
|
+
expect(frame).toMatch(/MSG-24-tag/);
|
|
111
|
+
expect(frame).toMatch(/MSG-23-tag/);
|
|
112
|
+
expect(frame).not.toMatch(/MSG-00-tag/);
|
|
113
|
+
});
|
|
114
|
+
it('Home jumps to the top; End re-engages follow-tail', async () => {
|
|
115
|
+
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
|
|
116
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
117
|
+
stdin.write(KEY_HOME);
|
|
118
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
119
|
+
const top = lastFrame() ?? '';
|
|
120
|
+
expect(top).toMatch(/MSG-00-tag/);
|
|
121
|
+
expect(top).not.toMatch(/MSG-24-tag/);
|
|
122
|
+
// End re-engages follow-tail.
|
|
123
|
+
stdin.write(KEY_END);
|
|
124
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
125
|
+
const bottom = lastFrame() ?? '';
|
|
126
|
+
expect(bottom).toMatch(/MSG-24-tag/);
|
|
127
|
+
expect(bottom).not.toMatch(/MSG-00-tag/);
|
|
128
|
+
});
|
|
129
|
+
it('PageUp/PageDown snap to message boundaries', async () => {
|
|
130
|
+
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
|
|
131
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
132
|
+
// Go to top, then PageDown 3 times. Each PageDown should land on a message
|
|
133
|
+
// boundary, so visible window starts at one of the seeded indices.
|
|
134
|
+
stdin.write(KEY_HOME);
|
|
135
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
136
|
+
stdin.write(KEY_PGDN);
|
|
137
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
138
|
+
stdin.write(KEY_PGDN);
|
|
139
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
140
|
+
stdin.write(KEY_PGDN);
|
|
141
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
142
|
+
const frame = lastFrame() ?? '';
|
|
143
|
+
// After 3 PageDowns from top, the earliest visible item should be MSG-03
|
|
144
|
+
// (snap-to-message keeps the boundary on the first visible row). We assert
|
|
145
|
+
// that MSG-03 is visible and MSG-00 is not.
|
|
146
|
+
expect(frame).toMatch(/MSG-03-tag/);
|
|
147
|
+
expect(frame).not.toMatch(/MSG-00-tag/);
|
|
148
|
+
});
|
|
149
|
+
it('pins to top: new content does NOT auto-scroll when user has scrolled up', async () => {
|
|
150
|
+
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
|
|
151
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
152
|
+
// Pin: scroll up to top.
|
|
153
|
+
stdin.write(KEY_HOME);
|
|
154
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
155
|
+
const before = lastFrame() ?? '';
|
|
156
|
+
expect(before).toMatch(/MSG-00-tag/);
|
|
157
|
+
expect(before).not.toMatch(/MSG-24-tag/);
|
|
158
|
+
// New content arrives while pinned.
|
|
159
|
+
const g = globalThis;
|
|
160
|
+
g.__klyroAppAppend({
|
|
161
|
+
id: 'late-1',
|
|
162
|
+
kind: 'text',
|
|
163
|
+
text: 'LATE-1-tag',
|
|
164
|
+
role: 'assistant',
|
|
165
|
+
});
|
|
166
|
+
g.__klyroAppAppend({
|
|
167
|
+
id: 'late-2',
|
|
168
|
+
kind: 'text',
|
|
169
|
+
text: 'LATE-2-tag',
|
|
170
|
+
role: 'assistant',
|
|
171
|
+
});
|
|
172
|
+
g.__klyroAppAppend({
|
|
173
|
+
id: 'late-3',
|
|
174
|
+
kind: 'text',
|
|
175
|
+
text: 'LATE-3-tag',
|
|
176
|
+
role: 'assistant',
|
|
177
|
+
});
|
|
178
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
179
|
+
const after = lastFrame() ?? '';
|
|
180
|
+
// Still pinned at top: MSG-00 visible, LATE items not in viewport.
|
|
181
|
+
expect(after).toMatch(/MSG-00-tag/);
|
|
182
|
+
expect(after).not.toMatch(/LATE-1-tag/);
|
|
183
|
+
});
|
|
184
|
+
it('pressing End re-engages follow-tail and reveals new content', async () => {
|
|
185
|
+
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
|
|
186
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
187
|
+
stdin.write(KEY_HOME);
|
|
188
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
189
|
+
const g = globalThis;
|
|
190
|
+
g.__klyroAppAppend({
|
|
191
|
+
id: 'late-1',
|
|
192
|
+
kind: 'text',
|
|
193
|
+
text: 'LATE-1-tag',
|
|
194
|
+
role: 'assistant',
|
|
195
|
+
});
|
|
196
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
197
|
+
expect(lastFrame() ?? '').not.toMatch(/LATE-1-tag/);
|
|
198
|
+
// End re-engages follow-tail and shows the new content.
|
|
199
|
+
stdin.write(KEY_END);
|
|
200
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
201
|
+
const frame = lastFrame() ?? '';
|
|
202
|
+
expect(frame).toMatch(/LATE-1-tag/);
|
|
203
|
+
});
|
|
204
|
+
it('Shift+Up / Shift+Down scroll by one line', async () => {
|
|
205
|
+
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
|
|
206
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
207
|
+
// Jump to top, then shift+down a few times, then back with shift+up.
|
|
208
|
+
stdin.write(KEY_HOME);
|
|
209
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
210
|
+
stdin.write(KEY_SHIFT_DOWN);
|
|
211
|
+
stdin.write(KEY_SHIFT_DOWN);
|
|
212
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
213
|
+
const frame = lastFrame() ?? '';
|
|
214
|
+
// Shift+Down from scrollOffset=0 moves us down by 2. The visible window
|
|
215
|
+
// is now [2..22). MSG-00 should be off-screen, MSG-02 should be on-screen.
|
|
216
|
+
expect(frame).not.toMatch(/MSG-00-tag/);
|
|
217
|
+
expect(frame).toMatch(/MSG-02-tag/);
|
|
218
|
+
// Shift+Up once: scrollOffset back to 1, MSG-01 visible, MSG-02 still visible.
|
|
219
|
+
stdin.write(KEY_SHIFT_UP);
|
|
220
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
221
|
+
const frame2 = lastFrame() ?? '';
|
|
222
|
+
expect(frame2).toMatch(/MSG-01-tag/);
|
|
223
|
+
});
|
|
82
224
|
});
|
package/dist/tui/tokens.d.ts
CHANGED
|
@@ -1,33 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* No backgrounds except diff viewer. fg.dim G��4.5:1 on near-black.
|
|
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
|
-
readonly
|
|
9
|
-
readonly
|
|
7
|
+
readonly bg: "#FFFFFF";
|
|
8
|
+
readonly bgElevated: "#FAF7F2";
|
|
9
|
+
readonly fg: "#1A1A1A";
|
|
10
10
|
readonly soft: "#F5F5F5";
|
|
11
11
|
readonly dim: "#9A9A9A";
|
|
12
12
|
readonly guide: "#3A3A3A";
|
|
13
|
-
readonly
|
|
13
|
+
readonly accent: "#FF6B1A";
|
|
14
|
+
readonly accentSoft: "#FFF1E6";
|
|
15
|
+
readonly ok: "#FF6B1A";
|
|
14
16
|
readonly err: "#E06C6C";
|
|
15
|
-
readonly warn: "#
|
|
16
|
-
readonly info: "#
|
|
17
|
+
readonly warn: "#FF6B1A";
|
|
18
|
+
readonly info: "#6FA8DC";
|
|
17
19
|
readonly diffAddBg: "#12250F";
|
|
18
20
|
readonly diffDelBg: "#2A1212";
|
|
19
21
|
};
|
|
20
22
|
readonly ansi: {
|
|
21
23
|
readonly accent: "yellowBright";
|
|
22
24
|
readonly accentBold: "yellowBright";
|
|
23
|
-
readonly fg: "
|
|
24
|
-
readonly soft: "
|
|
25
|
+
readonly fg: "white";
|
|
26
|
+
readonly soft: "whiteBright";
|
|
25
27
|
readonly dim: "gray";
|
|
26
28
|
readonly guide: "gray";
|
|
27
29
|
readonly ok: "yellowBright";
|
|
28
30
|
readonly err: "red";
|
|
29
31
|
readonly warn: "yellowBright";
|
|
30
|
-
readonly info: "
|
|
32
|
+
readonly info: "blue";
|
|
31
33
|
readonly border: "gray";
|
|
32
34
|
readonly muted: "gray";
|
|
33
35
|
readonly success: "yellowBright";
|
|
@@ -37,34 +39,30 @@ export declare const tokens: {
|
|
|
37
39
|
};
|
|
38
40
|
export declare const glyphs: {
|
|
39
41
|
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: "
|
|
42
|
+
readonly agentBullet: "●";
|
|
43
|
+
readonly collapsed: "▸";
|
|
44
|
+
readonly expanded: "▾";
|
|
45
|
+
readonly guide: "│";
|
|
46
|
+
readonly branch: "├";
|
|
47
|
+
readonly end: "└";
|
|
48
|
+
readonly rule: "─";
|
|
49
|
+
readonly treeBranch: "├──";
|
|
50
|
+
readonly treeEnd: "└──";
|
|
51
|
+
readonly success: "✓";
|
|
52
|
+
readonly failure: "✗";
|
|
51
53
|
readonly warning: "!";
|
|
52
|
-
readonly repair: "
|
|
53
|
-
readonly todoPending: "
|
|
54
|
-
readonly todoActive: "
|
|
55
|
-
readonly todoDone: "
|
|
56
|
-
readonly todoPlan: "
|
|
57
|
-
readonly
|
|
58
|
-
readonly
|
|
59
|
-
readonly
|
|
60
|
-
readonly
|
|
61
|
-
readonly
|
|
62
|
-
readonly
|
|
63
|
-
readonly
|
|
64
|
-
readonly meterEmpty: "G��";
|
|
65
|
-
readonly continuation: "G�";
|
|
66
|
-
readonly brand: "G��";
|
|
67
|
-
readonly compaction: "G��";
|
|
54
|
+
readonly repair: "↻";
|
|
55
|
+
readonly todoPending: "○";
|
|
56
|
+
readonly todoActive: "●";
|
|
57
|
+
readonly todoDone: "✓";
|
|
58
|
+
readonly todoPlan: "◇";
|
|
59
|
+
readonly logoBar: "▌";
|
|
60
|
+
readonly dotFilled: "●";
|
|
61
|
+
readonly dotEmpty: "○";
|
|
62
|
+
readonly brand: "◆";
|
|
63
|
+
readonly compaction: "⟲";
|
|
64
|
+
readonly editsBadge: "✎";
|
|
65
|
+
readonly dot: "·";
|
|
68
66
|
};
|
|
69
67
|
export declare const glyphAscii: {
|
|
70
68
|
readonly prompt: ">";
|
|
@@ -85,11 +83,13 @@ export declare const glyphAscii: {
|
|
|
85
83
|
readonly todoActive: "[>]";
|
|
86
84
|
readonly todoDone: "[x]";
|
|
87
85
|
readonly todoPlan: "#";
|
|
86
|
+
readonly logoBar: "|";
|
|
87
|
+
readonly dotFilled: "*";
|
|
88
|
+
readonly dotEmpty: "o";
|
|
88
89
|
};
|
|
89
90
|
export declare function isAsciiMode(): boolean;
|
|
90
91
|
export declare function g(name: keyof typeof glyphs): string;
|
|
91
92
|
export declare const spacing: {
|
|
92
|
-
readonly
|
|
93
|
-
readonly
|
|
94
|
-
readonly gap: 1;
|
|
93
|
+
readonly sidebar: 28;
|
|
94
|
+
readonly inspector: 36;
|
|
95
95
|
};
|
package/dist/tui/tokens.js
CHANGED
|
@@ -1,33 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* No backgrounds except diff viewer. fg.dim G��4.5:1 on near-black.
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
bg: '#FFFFFF',
|
|
8
|
+
bgElevated: '#FAF7F2',
|
|
9
|
+
fg: '#1A1A1A',
|
|
10
|
+
soft: '#F5F5F5',
|
|
11
|
+
dim: '#9A9A9A',
|
|
12
|
+
guide: '#3A3A3A',
|
|
13
|
+
accent: '#FF6B1A',
|
|
14
|
+
accentSoft: '#FFF1E6',
|
|
15
|
+
ok: '#FF6B1A',
|
|
14
16
|
err: '#E06C6C',
|
|
15
|
-
warn: '#
|
|
16
|
-
info: '#
|
|
17
|
+
warn: '#FF6B1A',
|
|
18
|
+
info: '#6FA8DC',
|
|
17
19
|
diffAddBg: '#12250F',
|
|
18
20
|
diffDelBg: '#2A1212',
|
|
19
21
|
},
|
|
20
22
|
ansi: {
|
|
21
|
-
accent: 'yellowBright',
|
|
23
|
+
accent: 'yellowBright',
|
|
22
24
|
accentBold: 'yellowBright',
|
|
23
|
-
fg: '
|
|
24
|
-
soft: '
|
|
25
|
+
fg: 'white',
|
|
26
|
+
soft: 'whiteBright',
|
|
25
27
|
dim: 'gray',
|
|
26
28
|
guide: 'gray',
|
|
27
|
-
ok: 'yellowBright',
|
|
29
|
+
ok: 'yellowBright',
|
|
28
30
|
err: 'red',
|
|
29
|
-
warn: 'yellowBright',
|
|
30
|
-
info: '
|
|
31
|
+
warn: 'yellowBright',
|
|
32
|
+
info: 'blue',
|
|
31
33
|
border: 'gray',
|
|
32
34
|
muted: 'gray',
|
|
33
35
|
success: 'yellowBright',
|
|
@@ -37,35 +39,30 @@ export const tokens = {
|
|
|
37
39
|
};
|
|
38
40
|
export const glyphs = {
|
|
39
41
|
prompt: '>',
|
|
40
|
-
agentBullet: '
|
|
41
|
-
collapsed: '
|
|
42
|
-
expanded: '
|
|
43
|
-
guide: '
|
|
44
|
-
branch: '
|
|
45
|
-
end: '
|
|
46
|
-
rule: '
|
|
47
|
-
treeBranch: '
|
|
48
|
-
treeEnd: '
|
|
49
|
-
success: '
|
|
50
|
-
failure: '
|
|
42
|
+
agentBullet: '●',
|
|
43
|
+
collapsed: '▸',
|
|
44
|
+
expanded: '▾',
|
|
45
|
+
guide: '│',
|
|
46
|
+
branch: '├',
|
|
47
|
+
end: '└',
|
|
48
|
+
rule: '─',
|
|
49
|
+
treeBranch: '├──',
|
|
50
|
+
treeEnd: '└──',
|
|
51
|
+
success: '✓',
|
|
52
|
+
failure: '✗',
|
|
51
53
|
warning: '!',
|
|
52
|
-
repair: '
|
|
53
|
-
todoPending: '
|
|
54
|
-
todoActive: '
|
|
55
|
-
todoDone: '
|
|
56
|
-
todoPlan: '
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
meterEmpty: 'G��',
|
|
65
|
-
continuation: 'G�',
|
|
66
|
-
// compat
|
|
67
|
-
brand: 'G��',
|
|
68
|
-
compaction: 'G��',
|
|
54
|
+
repair: '↻',
|
|
55
|
+
todoPending: '○',
|
|
56
|
+
todoActive: '●',
|
|
57
|
+
todoDone: '✓',
|
|
58
|
+
todoPlan: '◇',
|
|
59
|
+
logoBar: '▌',
|
|
60
|
+
dotFilled: '●',
|
|
61
|
+
dotEmpty: '○',
|
|
62
|
+
brand: '◆',
|
|
63
|
+
compaction: '⟲',
|
|
64
|
+
editsBadge: '✎',
|
|
65
|
+
dot: '·',
|
|
69
66
|
};
|
|
70
67
|
export const glyphAscii = {
|
|
71
68
|
prompt: '>',
|
|
@@ -86,16 +83,16 @@ export const glyphAscii = {
|
|
|
86
83
|
todoActive: '[>]',
|
|
87
84
|
todoDone: '[x]',
|
|
88
85
|
todoPlan: '#',
|
|
86
|
+
logoBar: '|',
|
|
87
|
+
dotFilled: '*',
|
|
88
|
+
dotEmpty: 'o',
|
|
89
89
|
};
|
|
90
90
|
export function isAsciiMode() {
|
|
91
|
-
return
|
|
92
|
-
process.env.KLYRO_ASCII === '1' ||
|
|
93
|
-
(process.env.LANG !== undefined && !process.env.LANG.toLowerCase().includes('utf-8')) ||
|
|
94
|
-
false);
|
|
91
|
+
return process.env.TERM === 'dumb' || process.env.KLYRO_ASCII === '1' || false;
|
|
95
92
|
}
|
|
96
93
|
export function g(name) {
|
|
97
94
|
if (isAsciiMode())
|
|
98
95
|
return glyphAscii[name] ?? glyphs[name];
|
|
99
96
|
return glyphs[name];
|
|
100
97
|
}
|
|
101
|
-
export const spacing = {
|
|
98
|
+
export const spacing = { sidebar: 28, inspector: 36 };
|