clarity-ai 6.7.0 → 6.8.0
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/CHANGELOG.md +12 -0
- package/bin/clarity.js +20 -12
- package/package.json +4 -2
- package/src/components/AppRoot.js +15 -5
- package/src/components/CodeBlock.js +8 -13
- package/src/components/CommandPicker.js +20 -22
- package/src/components/HeaderBar.js +20 -0
- package/src/components/InputBar.js +59 -0
- package/src/components/Layout.js +7 -11
- package/src/components/LoadingIndicator.js +2 -2
- package/src/components/MessageList.js +12 -27
- package/src/components/ModelPicker.js +26 -30
- package/src/components/ThinkingBlock.js +9 -16
- package/src/components/ToolCard.js +6 -10
- package/src/config/layout.js +39 -44
- package/src/config/theme.js +1 -51
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## 6.8.0 (2026-06-06)
|
|
6
|
+
|
|
7
|
+
### Clean-slate TUI: dynamic dimension defense, sandboxed streams, @inkjs/ui
|
|
8
|
+
- **Dimension defense**: monitors terminal resize; renders fallback if < 60x20
|
|
9
|
+
- **stdout/stderr sandbox**: console.log/error/warn intercepted to prevent AI model noise from corrupting the viewport
|
|
10
|
+
- **Hard-locked 3-tier Yoga grid**: headerBar(1) + messageViewport(flexGrow) + inputDock(3)
|
|
11
|
+
- **@inkjs/ui + cli-truncate**: wrap-ansi + string-width + cli-truncate for zero-bleed text
|
|
12
|
+
- **Single-line gradient header**: `CLARITY AI` with provider/model/mode badges — no ASCII art
|
|
13
|
+
- **Floating picker overlays** with `position: absolute` — never shift the message stream
|
|
14
|
+
- **Alternate screen buffer** with full SIGINT/SIGTERM cleanup and cursor restore
|
|
15
|
+
- **Orange selection bar** (`#FF6B35`) on all active items
|
|
16
|
+
|
|
5
17
|
## 6.7.0 (2026-06-06)
|
|
6
18
|
|
|
7
19
|
### Premium Ink+React TUI Engine — zero-bleed grid
|
package/bin/clarity.js
CHANGED
|
@@ -4,11 +4,14 @@ import { render } from 'ink';
|
|
|
4
4
|
import { App } from '../src/components/AppRoot.js';
|
|
5
5
|
import { hasKey } from '../src/config/keys.js';
|
|
6
6
|
import { createInterface } from 'readline';
|
|
7
|
-
import ansiEscapes from 'ansi-escapes';
|
|
8
7
|
|
|
9
8
|
process.stdin.resume();
|
|
10
9
|
process.stdin.setEncoding('utf8');
|
|
11
10
|
|
|
11
|
+
const originalLog = console.log;
|
|
12
|
+
const originalError = console.error;
|
|
13
|
+
const originalWarn = console.warn;
|
|
14
|
+
|
|
12
15
|
async function main() {
|
|
13
16
|
const provider = process.env.CLARITY_PROVIDER || 'groq';
|
|
14
17
|
|
|
@@ -25,39 +28,44 @@ async function main() {
|
|
|
25
28
|
process.stdin.resume();
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
console.log = function sandboxedLog() {};
|
|
32
|
+
console.error = function sandboxedError() {};
|
|
33
|
+
console.warn = function sandboxedWarn() {};
|
|
34
|
+
|
|
35
|
+
let keepAlive;
|
|
30
36
|
|
|
31
37
|
const config = { provider, model: process.env.CLARITY_MODEL || 'groq/llama-3.3-70b-versatile' };
|
|
32
38
|
|
|
33
|
-
const { clear, waitUntilExit } = render(React.createElement(App, { config }), {
|
|
39
|
+
const { clear, waitUntilExit, rerender } = render(React.createElement(App, { config }), {
|
|
34
40
|
fullscreen: true,
|
|
35
41
|
patchConsole: false,
|
|
36
42
|
exitOnCtrlC: false,
|
|
37
43
|
});
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
keepAlive = setInterval(() => {}, 2 ** 31 - 1);
|
|
40
46
|
|
|
41
47
|
function cleanup() {
|
|
42
48
|
clearInterval(keepAlive);
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
console.log = originalLog;
|
|
50
|
+
console.error = originalError;
|
|
51
|
+
console.warn = originalWarn;
|
|
45
52
|
try { clear(); } catch {}
|
|
53
|
+
process.stdout.write('\x1b[?25h\x1b[0m');
|
|
46
54
|
process.exit(0);
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
process.on('SIGINT', () => cleanup());
|
|
50
58
|
process.on('SIGTERM', () => cleanup());
|
|
51
|
-
process.on('exit', () => {
|
|
52
|
-
process.stdout.write(ansiEscapes.cursorShow);
|
|
53
|
-
});
|
|
59
|
+
process.on('exit', () => { process.stdout.write('\x1b[?25h\x1b[0m'); });
|
|
54
60
|
|
|
55
61
|
await new Promise(() => {});
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
main().catch(err => {
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
console.log = originalLog;
|
|
66
|
+
console.error = originalError;
|
|
67
|
+
console.warn = originalWarn;
|
|
68
|
+
process.stdout.write('\x1b[?25h\x1b[0m');
|
|
61
69
|
console.error('\n\x1b[31mFatal error:\x1b[0m', err.message);
|
|
62
70
|
process.exit(1);
|
|
63
71
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clarity-ai",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.8.0",
|
|
4
4
|
"description": "CLARITY — terminal AI agent with local GGUF inference on HF Spaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"gradient-string": "^3",
|
|
28
28
|
"figures": "^6",
|
|
29
29
|
"ansi-escapes": "^7",
|
|
30
|
-
"string-width": "^7"
|
|
30
|
+
"string-width": "^7",
|
|
31
|
+
"@inkjs/ui": "^2",
|
|
32
|
+
"cli-truncate": "^6"
|
|
31
33
|
}
|
|
32
34
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React, { useState, useCallback, useRef } from 'react';
|
|
2
|
-
import { Box } from 'ink';
|
|
1
|
+
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
3
|
import { createChatState, handleSend, handleCommand } from '../chat.js';
|
|
4
4
|
import { hex } from '../config/theme.js';
|
|
5
|
+
import { getLayout } from '../config/layout.js';
|
|
5
6
|
import { Layout } from './Layout.js';
|
|
6
7
|
const { createElement: h } = React;
|
|
7
8
|
|
|
@@ -57,7 +58,7 @@ export function App({ config }) {
|
|
|
57
58
|
await handleSend(stateRef.current, setState, input, modelRef.current, providerRef.current, setStreamContent, ac.signal);
|
|
58
59
|
}, []);
|
|
59
60
|
|
|
60
|
-
function
|
|
61
|
+
function handleCmdSelect(cmdName) {
|
|
61
62
|
setShowCommands(false);
|
|
62
63
|
onSubmit(cmdName);
|
|
63
64
|
}
|
|
@@ -72,11 +73,20 @@ export function App({ config }) {
|
|
|
72
73
|
}));
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
const layout = getLayout();
|
|
77
|
+
|
|
78
|
+
if (!layout.isLargeEnough) {
|
|
79
|
+
return h(Box, { width: '100%', height: '100%', alignItems: 'center', justifyContent: 'center', backgroundColor: hex.bg },
|
|
80
|
+
h(Text, { color: hex.textDim },
|
|
81
|
+
'Terminal size too small. Please expand. (' + layout.cols + 'x' + layout.rows + ' needed: 60x20)')
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: '100%', height: '100%' },
|
|
76
86
|
h(Layout, {
|
|
77
87
|
state, streamContent, model, provider,
|
|
78
88
|
showCommands, showModels,
|
|
79
|
-
onCommandSelect:
|
|
89
|
+
onCommandSelect: handleCmdSelect,
|
|
80
90
|
onModelSelect: handleModelSelect,
|
|
81
91
|
onCloseCommands: () => setShowCommands(false),
|
|
82
92
|
onCloseModels: () => setShowModels(false),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import { hex
|
|
3
|
+
import { hex } from '../config/theme.js';
|
|
4
4
|
import { getLayout } from '../config/layout.js';
|
|
5
5
|
const { createElement: h } = React;
|
|
6
6
|
|
|
@@ -9,39 +9,34 @@ const LANG_COLORS = {
|
|
|
9
9
|
py: '#3572A5', rb: '#CC342D', go: '#00ADD8', rs: '#DEA584',
|
|
10
10
|
java: '#B07219', kt: '#7F52FF', swift: '#FFAC45',
|
|
11
11
|
html: '#E34F26', css: '#1572B6', scss: '#CC6699',
|
|
12
|
-
sh: '#89E051', bash: '#89E051',
|
|
13
|
-
json: '#292929', yaml: '#CB171E', md: '#083FA1', sql: '#E38C00',
|
|
12
|
+
sh: '#89E051', bash: '#89E051',
|
|
14
13
|
};
|
|
15
14
|
|
|
16
15
|
export function CodeBlock({ code, language }) {
|
|
17
16
|
const lang = language || 'code';
|
|
18
17
|
const lines = useMemo(() => String(code).split('\n'), [code]);
|
|
19
18
|
const langColor = LANG_COLORS[lang] || '#555';
|
|
20
|
-
const lnW = String(lines.length).length;
|
|
21
19
|
const { cols } = getLayout();
|
|
22
|
-
const maxLines =
|
|
20
|
+
const maxLines = 15;
|
|
23
21
|
const visible = lines.slice(0, maxLines);
|
|
24
22
|
const codeWidth = cols - 10;
|
|
23
|
+
const lnW = String(lines.length).length;
|
|
25
24
|
|
|
26
25
|
return h(Box, { flexDirection: 'column', backgroundColor: hex.codeBg },
|
|
27
26
|
h(Box, { height: 1, backgroundColor: hex.codeBg },
|
|
28
27
|
h(Text, { color: langColor, bold: true, backgroundColor: hex.codeBg }, ' ' + lang),
|
|
29
|
-
h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg }, ' ' + String(lines.length) + ' lines
|
|
28
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg }, ' ' + String(lines.length) + ' lines'),
|
|
30
29
|
),
|
|
31
30
|
h(Box, { flexDirection: 'column', backgroundColor: hex.codeBg },
|
|
32
31
|
visible.map((line, i) =>
|
|
33
32
|
h(Box, { key: i, height: 1, backgroundColor: hex.codeBg },
|
|
34
|
-
h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg },
|
|
35
|
-
|
|
36
|
-
),
|
|
37
|
-
h(Text, { color: '#C9D1D9', backgroundColor: hex.codeBg, wrap: 'truncate-end' },
|
|
38
|
-
(line || ' ').slice(0, codeWidth)
|
|
39
|
-
)
|
|
33
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg }, ' ' + String(i + 1).padStart(lnW) + ' '),
|
|
34
|
+
h(Text, { color: '#C9D1D9', backgroundColor: hex.codeBg, wrap: 'truncate-end' }, (line || ' ').slice(0, codeWidth))
|
|
40
35
|
)
|
|
41
36
|
),
|
|
42
37
|
lines.length > maxLines
|
|
43
38
|
? h(Box, { height: 1, backgroundColor: hex.codeBg },
|
|
44
|
-
h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg }, '
|
|
39
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg }, ' \u2026 ' + (lines.length - maxLines) + ' more lines')
|
|
45
40
|
)
|
|
46
41
|
: null
|
|
47
42
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
|
-
import { hex
|
|
3
|
+
import { hex } from '../config/theme.js';
|
|
4
4
|
import { getLayout } from '../config/layout.js';
|
|
5
5
|
const { createElement: h } = React;
|
|
6
6
|
|
|
@@ -16,7 +16,7 @@ const COMMANDS = [
|
|
|
16
16
|
{ name: '/exit', desc: 'Exit CLARITY' },
|
|
17
17
|
];
|
|
18
18
|
|
|
19
|
-
export function CommandPicker({
|
|
19
|
+
export function CommandPicker({ onSelect, onClose }) {
|
|
20
20
|
const [search, setSearch] = useState('');
|
|
21
21
|
const [idx, setIdx] = useState(0);
|
|
22
22
|
const { cols } = getLayout();
|
|
@@ -29,43 +29,41 @@ export function CommandPicker({ query, onSelect, onClose }) {
|
|
|
29
29
|
if (key.upArrow) setIdx(i => Math.max(0, i - 1));
|
|
30
30
|
if (key.downArrow) setIdx(i => Math.min(filtered.length - 1, i + 1));
|
|
31
31
|
if (key.return && filtered[idx]) onSelect(filtered[idx].name);
|
|
32
|
-
if (key.escape
|
|
32
|
+
if (key.escape) onClose();
|
|
33
33
|
if (key.backspace) setSearch(s => s.slice(0, -1));
|
|
34
34
|
else if (input && !key.ctrl && !key.meta) setSearch(s => s + input);
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
const w = Math.min(cols - 4, 48);
|
|
38
38
|
|
|
39
|
-
const items = filtered.map((cmd, i) =>
|
|
40
|
-
h(Box, {
|
|
41
|
-
key: cmd.name, height: 1,
|
|
42
|
-
backgroundColor: i === idx ? hex.selectionBg : hex.bg,
|
|
43
|
-
},
|
|
44
|
-
h(Text, {
|
|
45
|
-
color: i === idx ? hex.selectionText : hex.text,
|
|
46
|
-
bold: i === idx,
|
|
47
|
-
backgroundColor: i === idx ? hex.selectionBg : hex.bg,
|
|
48
|
-
}, ' ' + (i === idx ? '\u276F ' : ' ') + cmd.name + ' ' + cmd.desc)
|
|
49
|
-
)
|
|
50
|
-
);
|
|
51
|
-
|
|
52
39
|
return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: w },
|
|
53
40
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
54
41
|
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
55
|
-
|
|
42
|
+
'\u250C' + '\u2500'.repeat(w - 2) + '\u2510')
|
|
56
43
|
),
|
|
57
44
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
58
|
-
h(Text, { color: hex.
|
|
59
|
-
|
|
45
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
46
|
+
'\u2502 Commands' + ' '.repeat(Math.max(0, w - 12)) + '\u2502')
|
|
60
47
|
),
|
|
61
48
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
62
49
|
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
63
|
-
|
|
50
|
+
'\u2502 ' + (search || '\u2026') + ' '.repeat(Math.max(0, w - 6)) + '\u2502')
|
|
51
|
+
),
|
|
52
|
+
filtered.map((cmd, i) =>
|
|
53
|
+
h(Box, {
|
|
54
|
+
key: cmd.name, height: 1,
|
|
55
|
+
backgroundColor: i === idx ? hex.selectionBg : hex.bg,
|
|
56
|
+
},
|
|
57
|
+
h(Text, {
|
|
58
|
+
color: i === idx ? hex.selectionText : hex.text,
|
|
59
|
+
bold: i === idx,
|
|
60
|
+
backgroundColor: i === idx ? hex.selectionBg : hex.bg,
|
|
61
|
+
}, (i === idx ? '\u276F ' : ' ') + cmd.name + ' ' + cmd.desc)
|
|
62
|
+
)
|
|
64
63
|
),
|
|
65
|
-
h(Box, { flexDirection: 'column', backgroundColor: hex.bg }, ...items),
|
|
66
64
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
67
65
|
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
68
|
-
|
|
66
|
+
'\u2514' + '\u2500'.repeat(w - 2) + '\u2518')
|
|
69
67
|
),
|
|
70
68
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
71
69
|
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { hex, appGradient } from '../config/theme.js';
|
|
4
|
+
import { getLayout } from '../config/layout.js';
|
|
5
|
+
const { createElement: h } = React;
|
|
6
|
+
|
|
7
|
+
export function HeaderBar({ model, provider, agentMode, thinking }) {
|
|
8
|
+
const { cols } = getLayout();
|
|
9
|
+
const m = model.replace(/^[^/]+\//, '').slice(0, Math.floor((cols - 20) / 2));
|
|
10
|
+
const status = thinking ? ' \u25CF' : ' \u25CB';
|
|
11
|
+
const mode = agentMode ? '\u25C8 AGENT' : '\u25CB USER';
|
|
12
|
+
const label = '[' + provider + '] ' + m + ' ' + status + ' ' + mode;
|
|
13
|
+
const labelLen = label.length + 14;
|
|
14
|
+
const gap = Math.max(1, cols - labelLen);
|
|
15
|
+
|
|
16
|
+
return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
17
|
+
h(Text, { backgroundColor: hex.surfaceAlt }, appGradient(' CLARITY AI ')),
|
|
18
|
+
h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, ' '.repeat(gap) + label + ' ')
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { hex } from '../config/theme.js';
|
|
4
|
+
import { getLayout } from '../config/layout.js';
|
|
5
|
+
const { createElement: h } = React;
|
|
6
|
+
|
|
7
|
+
export function InputBar({ provider, model, agentMode, thinking, onSlash, onSubmit }) {
|
|
8
|
+
const [input, setInput] = useState('');
|
|
9
|
+
const [cursor, setCursor] = useState(0);
|
|
10
|
+
const { cols } = getLayout();
|
|
11
|
+
const w = Math.max(10, cols - 6);
|
|
12
|
+
const mShort = model.replace(/^[^/]+\//, '').slice(0, 16);
|
|
13
|
+
|
|
14
|
+
useInput((ch, key) => {
|
|
15
|
+
if (key.ctrl && key.p) { onSlash(); return; }
|
|
16
|
+
if (key.escape) { onSubmit('/exit'); return; }
|
|
17
|
+
if (key.return && !key.shift) {
|
|
18
|
+
if (input.trim()) { const t = input; setInput(''); setCursor(0); onSubmit(t); }
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (key.return && key.shift) {
|
|
22
|
+
setInput(p => p.slice(0, cursor) + '\n' + p.slice(cursor));
|
|
23
|
+
setCursor(c => c + 1);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (key.backspace || key.delete) {
|
|
27
|
+
if (cursor > 0) { setInput(p => p.slice(0, cursor - 1) + p.slice(cursor)); setCursor(c => c - 1); }
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (key.leftArrow && cursor > 0) { setCursor(c => c - 1); return; }
|
|
31
|
+
if (key.rightArrow && cursor < input.length) { setCursor(c => c + 1); return; }
|
|
32
|
+
if (key.home) { setCursor(0); return; }
|
|
33
|
+
if (key.end) { setCursor(input.length); return; }
|
|
34
|
+
if (ch && ch.length === 1 && ch.charCodeAt(0) >= 32) {
|
|
35
|
+
setInput(p => p.slice(0, cursor) + ch + p.slice(cursor));
|
|
36
|
+
setCursor(c => c + 1);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const isPlaceholder = !input && !thinking;
|
|
41
|
+
|
|
42
|
+
return h(Box, { flexDirection: 'column' },
|
|
43
|
+
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
44
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
45
|
+
' \u250C' + '\u2500'.repeat(Math.max(0, cols - 6)) + '\u2510')
|
|
46
|
+
),
|
|
47
|
+
h(Box, { height: 1, backgroundColor: hex.bg },
|
|
48
|
+
h(Text, {
|
|
49
|
+
color: isPlaceholder ? hex.textMuted : hex.text,
|
|
50
|
+
backgroundColor: hex.bg,
|
|
51
|
+
wrap: 'truncate-end',
|
|
52
|
+
}, ' \u2502 ' + (input || (thinking ? '\u25CF processing...' : 'type a message...')) + ' ')
|
|
53
|
+
),
|
|
54
|
+
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
55
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
56
|
+
' \u2514' + '\u2500' + ' ' + provider + ' \u00B7 ' + mShort + (agentMode ? ' \u00B7 AGENT' : '') + ' \u00B7 Ctrl+P')
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
}
|
package/src/components/Layout.js
CHANGED
|
@@ -1,34 +1,30 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Box } from 'ink';
|
|
3
3
|
import { hex } from '../config/theme.js';
|
|
4
|
-
import {
|
|
5
|
-
import { StatusBar } from './StatusBar.js';
|
|
4
|
+
import { HeaderBar } from './HeaderBar.js';
|
|
6
5
|
import { MessageList } from './MessageList.js';
|
|
7
|
-
import {
|
|
6
|
+
import { InputBar } from './InputBar.js';
|
|
8
7
|
import { CommandPicker } from './CommandPicker.js';
|
|
9
8
|
import { ModelPicker } from './ModelPicker.js';
|
|
10
9
|
const { createElement: h } = React;
|
|
11
10
|
|
|
12
11
|
export function Layout({ state, streamContent, model, provider, showCommands, showModels, onCommandSelect, onModelSelect, onCloseCommands, onCloseModels, onSlash, onSubmit }) {
|
|
13
|
-
const { headerHeight, dockHeight, cols } = getLayout();
|
|
14
|
-
|
|
15
12
|
const picker = showCommands || showModels
|
|
16
|
-
? h(Box, { position: 'absolute', top:
|
|
17
|
-
showCommands ? h(CommandPicker, {
|
|
13
|
+
? h(Box, { position: 'absolute', top: 1, left: 2, backgroundColor: hex.bg, flexDirection: 'column' },
|
|
14
|
+
showCommands ? h(CommandPicker, { onSelect: onCommandSelect, onClose: onCloseCommands }) : null,
|
|
18
15
|
showModels ? h(ModelPicker, { onSelect: onModelSelect, onClose: onCloseModels }) : null
|
|
19
16
|
)
|
|
20
17
|
: null;
|
|
21
18
|
|
|
22
|
-
return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, height: '100%' },
|
|
23
|
-
h(
|
|
19
|
+
return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: '100%', height: '100%' },
|
|
20
|
+
h(HeaderBar, { model, provider, agentMode: state.agentMode, thinking: state.thinking }),
|
|
24
21
|
h(Box, { flexGrow: 1, flexDirection: 'column', position: 'relative' },
|
|
25
22
|
h(MessageList, {
|
|
26
23
|
messages: state.messages, thinking: state.thinking,
|
|
27
24
|
streamContent, agentStatus: state.agentStatus,
|
|
28
|
-
toolExecutions: state.toolExecutions,
|
|
29
25
|
}),
|
|
30
26
|
picker
|
|
31
27
|
),
|
|
32
|
-
h(
|
|
28
|
+
h(InputBar, { provider, model, agentMode: state.agentMode, thinking: state.thinking, onSlash, onSubmit })
|
|
33
29
|
);
|
|
34
30
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import { hex
|
|
3
|
+
import { hex } from '../config/theme.js';
|
|
4
4
|
const { createElement: h } = React;
|
|
5
5
|
|
|
6
6
|
export function LoadingIndicator({ label }) {
|
|
7
7
|
return h(Box, { height: 1, backgroundColor: hex.surface },
|
|
8
|
-
h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' '
|
|
8
|
+
h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' \u25CF'),
|
|
9
9
|
h(Text, { color: hex.textDim, backgroundColor: hex.surface }, ' ' + (label || 'processing'))
|
|
10
10
|
);
|
|
11
11
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import { hex
|
|
3
|
+
import { hex } from '../config/theme.js';
|
|
4
4
|
import { getLayout, sliceToViewport, buildLineArray } from '../config/layout.js';
|
|
5
5
|
const { createElement: h } = React;
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ function Line({ type, text, data }) {
|
|
|
8
8
|
switch (type) {
|
|
9
9
|
case 'user_head':
|
|
10
10
|
return h(Box, { height: 1, backgroundColor: hex.userBg },
|
|
11
|
-
h(Text, { color: hex.accent, bold: true, backgroundColor: hex.userBg }, ' \u276F
|
|
11
|
+
h(Text, { color: hex.accent, bold: true, backgroundColor: hex.userBg }, ' \u276F YOU')
|
|
12
12
|
);
|
|
13
13
|
case 'user_line':
|
|
14
14
|
return h(Box, { height: 1, backgroundColor: hex.userBg },
|
|
@@ -24,19 +24,20 @@ function Line({ type, text, data }) {
|
|
|
24
24
|
);
|
|
25
25
|
case 'asst_foot':
|
|
26
26
|
return h(Box, { height: 1, backgroundColor: hex.surface },
|
|
27
|
-
h(Text, { color: hex.textDim, backgroundColor: hex.surface },
|
|
27
|
+
h(Text, { color: hex.textDim, backgroundColor: hex.surface },
|
|
28
|
+
' \u25B8 ' + (parseInt(text) < 1000 ? text + 'ms' : (parseInt(text) / 1000).toFixed(1) + 's'))
|
|
28
29
|
);
|
|
29
30
|
case 'tool_line':
|
|
30
31
|
return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
31
|
-
h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, '
|
|
32
|
+
h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, ' \u25C9 ' + (text || ''))
|
|
32
33
|
);
|
|
33
34
|
case 'sys_line':
|
|
34
35
|
return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
35
|
-
h(Text, { color: hex.green, backgroundColor: hex.surfaceAlt }, '
|
|
36
|
+
h(Text, { color: hex.green, backgroundColor: hex.surfaceAlt }, ' \u25C9 ' + (text || ''))
|
|
36
37
|
);
|
|
37
38
|
case 'err_line':
|
|
38
39
|
return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
39
|
-
h(Text, { color: hex.red, backgroundColor: hex.surfaceAlt }, '
|
|
40
|
+
h(Text, { color: hex.red, backgroundColor: hex.surfaceAlt }, ' \u2716 ' + (text || ''))
|
|
40
41
|
);
|
|
41
42
|
case 'stream_head':
|
|
42
43
|
return h(Box, { height: 1, backgroundColor: hex.surface },
|
|
@@ -55,7 +56,7 @@ function Line({ type, text, data }) {
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
export function MessageList({ messages, thinking, streamContent, agentStatus
|
|
59
|
+
export function MessageList({ messages, thinking, streamContent, agentStatus }) {
|
|
59
60
|
const { viewport, contentWidth } = getLayout();
|
|
60
61
|
|
|
61
62
|
const entries = useMemo(() => {
|
|
@@ -65,12 +66,9 @@ export function MessageList({ messages, thinking, streamContent, agentStatus, to
|
|
|
65
66
|
}));
|
|
66
67
|
}, [messages]);
|
|
67
68
|
|
|
68
|
-
const showLogo = entries.length <= 1 && !thinking && !streamContent;
|
|
69
|
-
const effectiveViewport = showLogo ? Math.max(viewport - 14, 4) : viewport;
|
|
70
|
-
|
|
71
69
|
const { slice, clipIndex, clipLines } = useMemo(
|
|
72
|
-
() => sliceToViewport(entries,
|
|
73
|
-
[entries,
|
|
70
|
+
() => sliceToViewport(entries, viewport, contentWidth),
|
|
71
|
+
[entries, viewport, contentWidth]
|
|
74
72
|
);
|
|
75
73
|
|
|
76
74
|
const rawLines = useMemo(
|
|
@@ -78,27 +76,14 @@ export function MessageList({ messages, thinking, streamContent, agentStatus, to
|
|
|
78
76
|
[slice, clipIndex, clipLines, contentWidth]
|
|
79
77
|
);
|
|
80
78
|
|
|
79
|
+
const fillCount = Math.max(0, viewport - rawLines.length);
|
|
81
80
|
const padded = [];
|
|
82
|
-
|
|
83
|
-
const fillRows = Math.max(0, effectiveViewport - rawLines.length - logoRows);
|
|
84
|
-
for (let i = 0; i < fillRows; i++) padded.push({ type: 'empty' });
|
|
85
|
-
if (showLogo) {
|
|
86
|
-
for (let i = 0; i < 14; i++) padded.push({ type: 'logo', line: i });
|
|
87
|
-
}
|
|
81
|
+
for (let i = 0; i < fillCount; i++) padded.push({ type: 'empty' });
|
|
88
82
|
for (const ln of rawLines) padded.push(ln);
|
|
89
83
|
|
|
90
84
|
return h(Box, { height: viewport, flexDirection: 'column', overflow: 'hidden' },
|
|
91
85
|
padded.map((ln, i) => {
|
|
92
86
|
if (ln.type === 'empty') return h(Box, { key: 'e' + i, height: 1, backgroundColor: hex.bg });
|
|
93
|
-
if (ln.type === 'logo') {
|
|
94
|
-
const logoLine = LOGO[ln.line] || '';
|
|
95
|
-
return h(Box, { key: 'l' + i, height: 1, backgroundColor: hex.bg },
|
|
96
|
-
h(Text, {
|
|
97
|
-
color: (ln.line >= 1 && ln.line <= 5) || ln.line === 9 ? hex.accent : hex.textMuted,
|
|
98
|
-
backgroundColor: hex.bg,
|
|
99
|
-
}, ' ' + logoLine)
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
87
|
return h(Box, { key: (ln.data?.id || 'r') + '-' + i, height: 1 },
|
|
103
88
|
h(Line, { type: ln.type, text: ln.text, data: ln.data })
|
|
104
89
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useState, useMemo } from 'react';
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
3
|
import { ALL_MODELS } from '../config/models.js';
|
|
4
|
-
import { hex
|
|
4
|
+
import { hex } from '../config/theme.js';
|
|
5
5
|
import { getLayout } from '../config/layout.js';
|
|
6
6
|
const { createElement: h } = React;
|
|
7
7
|
|
|
@@ -32,52 +32,48 @@ export function ModelPicker({ onSelect, onClose }) {
|
|
|
32
32
|
if (key.upArrow) setIdx(i => Math.max(0, i - 1));
|
|
33
33
|
if (key.downArrow) setIdx(i => Math.min(flat.length - 1, i + 1));
|
|
34
34
|
if (key.return) { const m = flat[idx]; if (m && !m._header) onSelect(m.id); return; }
|
|
35
|
-
if (key.escape
|
|
35
|
+
if (key.escape) onClose();
|
|
36
36
|
if (key.backspace) setSearch(s => s.slice(0, -1));
|
|
37
37
|
else if (input && !key.ctrl && !key.meta) setSearch(s => s + input);
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
const w = Math.min(cols - 4, 52);
|
|
41
41
|
|
|
42
|
-
const items = flat.map((m, i) => {
|
|
43
|
-
if (m._header) {
|
|
44
|
-
return h(Box, { key: 'h-' + m._provider, height: 1, backgroundColor: hex.surfaceAlt },
|
|
45
|
-
h(Text, { color: hex.blue, bold: true, backgroundColor: hex.surfaceAlt }, sym.box.v + ' ' + m._provider.toUpperCase()),
|
|
46
|
-
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' '.repeat(Math.max(0, w - m._provider.length - 6)) + sym.box.v)
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
const isSel = i === idx;
|
|
50
|
-
return h(Box, {
|
|
51
|
-
key: m.id, height: 1,
|
|
52
|
-
backgroundColor: isSel ? hex.selectionBg : hex.bg,
|
|
53
|
-
},
|
|
54
|
-
h(Text, {
|
|
55
|
-
color: isSel ? hex.selectionText : hex.text,
|
|
56
|
-
bold: isSel,
|
|
57
|
-
backgroundColor: isSel ? hex.selectionBg : hex.bg,
|
|
58
|
-
}, (isSel ? '\u276F ' : ' ') + m.label + (m.badge ? ' [' + m.badge + ']' : ''))
|
|
59
|
-
);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const count = flat.filter(m => !m._header).length;
|
|
63
|
-
|
|
64
42
|
return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: w },
|
|
65
43
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
66
44
|
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
67
|
-
|
|
45
|
+
'\u250C' + '\u2500'.repeat(w - 2) + '\u2510')
|
|
68
46
|
),
|
|
69
47
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
70
|
-
h(Text, { color: hex.
|
|
71
|
-
|
|
48
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
49
|
+
'\u2502 Models ' + flat.filter(m => !m._header).length + ' available' + ' '.repeat(Math.max(0, w - 18)) + '\u2502')
|
|
72
50
|
),
|
|
73
51
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
74
52
|
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
75
|
-
|
|
53
|
+
'\u2502 ' + (search || '\u2026') + ' '.repeat(Math.max(0, w - 6)) + '\u2502')
|
|
76
54
|
),
|
|
77
|
-
|
|
55
|
+
flat.map((m, i) => {
|
|
56
|
+
if (m._header) {
|
|
57
|
+
return h(Box, { key: 'h-' + m._provider, height: 1, backgroundColor: hex.surfaceAlt },
|
|
58
|
+
h(Text, { color: hex.blue, bold: true, backgroundColor: hex.surfaceAlt },
|
|
59
|
+
'\u2502 ' + m._provider.toUpperCase() + ' '.repeat(Math.max(0, w - m._provider.length - 5)) + '\u2502')
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const isSel = i === idx;
|
|
63
|
+
return h(Box, {
|
|
64
|
+
key: m.id, height: 1,
|
|
65
|
+
backgroundColor: isSel ? hex.selectionBg : hex.bg,
|
|
66
|
+
},
|
|
67
|
+
h(Text, {
|
|
68
|
+
color: isSel ? hex.selectionText : hex.text,
|
|
69
|
+
bold: isSel,
|
|
70
|
+
backgroundColor: isSel ? hex.selectionBg : hex.bg,
|
|
71
|
+
}, (isSel ? '\u276F ' : ' ') + m.label + (m.badge ? ' [' + m.badge + ']' : ''))
|
|
72
|
+
);
|
|
73
|
+
}),
|
|
78
74
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
79
75
|
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
80
|
-
|
|
76
|
+
'\u2514' + '\u2500'.repeat(w - 2) + '\u2518')
|
|
81
77
|
),
|
|
82
78
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
83
79
|
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import { hex
|
|
4
|
-
import { getLayout } from '../config/layout.js';
|
|
3
|
+
import { hex } from '../config/theme.js';
|
|
5
4
|
const { createElement: h } = React;
|
|
6
5
|
|
|
7
|
-
export function ThinkingBlock({ toolResults, duration }) {
|
|
8
|
-
const [collapsed, setCollapsed] = useState(true);
|
|
9
|
-
const { cols } = getLayout();
|
|
6
|
+
export function ThinkingBlock({ toolResults, duration, collapsed: initialCollapsed }) {
|
|
7
|
+
const [collapsed, setCollapsed] = useState(initialCollapsed !== undefined ? initialCollapsed : true);
|
|
10
8
|
const items = toolResults || [];
|
|
11
9
|
const durStr = duration
|
|
12
10
|
? (duration < 1000 ? duration + 'ms' : (duration / 1000).toFixed(1) + 's')
|
|
@@ -15,20 +13,15 @@ export function ThinkingBlock({ toolResults, duration }) {
|
|
|
15
13
|
return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
|
|
16
14
|
h(Box, { height: 1 },
|
|
17
15
|
h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt },
|
|
18
|
-
' ' + (collapsed ?
|
|
16
|
+
' ' + (collapsed ? '\u25B8' : '\u25BE') + ' Thought' + (durStr ? ' (' + durStr + ')' : ''))
|
|
19
17
|
),
|
|
20
18
|
collapsed ? null : h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
|
|
21
|
-
items.map((tr, i) =>
|
|
22
|
-
|
|
23
|
-
const prefix = isLast ? sym.treeTip + '\u2500' : sym.treeFork + '\u2500';
|
|
24
|
-
const conn = isLast ? ' ' : sym.treeCon;
|
|
25
|
-
const ico = tr.status === 'failed' ? sym.cross : sym.circle;
|
|
26
|
-
const td = tr.duration ? ' ' + tr.duration + 'ms' : '';
|
|
27
|
-
return h(Box, { key: tr.execId || i, height: 1 },
|
|
19
|
+
items.map((tr, i) =>
|
|
20
|
+
h(Box, { key: tr.execId || i, height: 1 },
|
|
28
21
|
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
29
|
-
' ' +
|
|
30
|
-
)
|
|
31
|
-
|
|
22
|
+
' ' + (i === items.length - 1 ? '\u2570\u2500' : '\u256D\u2500') + ' ' + (tr.status === 'failed' ? '\u2716' : '\u25C9') + ' ' + tr.name + (tr.duration ? ' ' + tr.duration + 'ms' : ''))
|
|
23
|
+
)
|
|
24
|
+
)
|
|
32
25
|
)
|
|
33
26
|
);
|
|
34
27
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import { hex
|
|
4
|
-
import {
|
|
3
|
+
import { hex } from '../config/theme.js';
|
|
4
|
+
import { truncate } from '../config/layout.js';
|
|
5
5
|
const { createElement: h } = React;
|
|
6
6
|
|
|
7
7
|
export function ToolCard({ exec, isActive }) {
|
|
8
|
-
const { cols } = getLayout();
|
|
9
8
|
const name = exec.name || 'tool';
|
|
10
9
|
const status = exec.status || 'running';
|
|
11
10
|
const dur = exec.duration ? (exec.duration < 1000 ? exec.duration + 'ms' : (exec.duration / 1000).toFixed(1) + 's') : '';
|
|
@@ -14,26 +13,23 @@ export function ToolCard({ exec, isActive }) {
|
|
|
14
13
|
|
|
15
14
|
if (isDone) {
|
|
16
15
|
const c = status === 'failed' ? hex.red : hex.green;
|
|
17
|
-
const icon = status === 'failed' ? sym.cross : sym.bullet;
|
|
18
16
|
return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
19
|
-
h(Text, { color: c, backgroundColor: hex.surfaceAlt }, ' ' +
|
|
17
|
+
h(Text, { color: c, backgroundColor: hex.surfaceAlt }, ' ' + (status === 'failed' ? '\u2716' : '\u25C9') + ' ' + name + (dur ? ' ' + dur : ''))
|
|
20
18
|
);
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
const w = Math.min(cols - 8, 56);
|
|
24
|
-
|
|
25
21
|
return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
|
|
26
22
|
h(Box, { height: 1 },
|
|
27
|
-
h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, '
|
|
23
|
+
h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, ' \u25C9 ' + name + (dur ? ' ' + dur : ''))
|
|
28
24
|
),
|
|
29
25
|
args.length < 80
|
|
30
26
|
? h(Box, { height: 1 },
|
|
31
|
-
h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, '
|
|
27
|
+
h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, ' \u25B8 ' + truncate(args, 56))
|
|
32
28
|
)
|
|
33
29
|
: null,
|
|
34
30
|
status === 'running'
|
|
35
31
|
? h(Box, { height: 1 },
|
|
36
|
-
h(Text, { color: hex.blue, backgroundColor: hex.surfaceAlt }, '
|
|
32
|
+
h(Text, { color: hex.blue, backgroundColor: hex.surfaceAlt }, ' \u25CF running')
|
|
37
33
|
)
|
|
38
34
|
: null,
|
|
39
35
|
);
|
package/src/config/layout.js
CHANGED
|
@@ -1,49 +1,52 @@
|
|
|
1
1
|
import wrapAnsi from 'wrap-ansi';
|
|
2
2
|
import stringWidth from 'string-width';
|
|
3
|
+
import cliTruncate from 'cli-truncate';
|
|
3
4
|
|
|
4
5
|
export function getLayout() {
|
|
5
6
|
const rows = process.stdout.rows || 30;
|
|
6
7
|
const cols = process.stdout.columns || 80;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
return {
|
|
9
|
+
rows,
|
|
10
|
+
cols,
|
|
11
|
+
headerHeight: 1,
|
|
12
|
+
dockHeight: 3,
|
|
13
|
+
viewport: Math.max(4, rows - 4),
|
|
14
|
+
contentWidth: Math.max(10, cols - 4),
|
|
15
|
+
isLargeEnough: cols >= 60 && rows >= 20,
|
|
16
|
+
};
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
export function sw(text) {
|
|
15
20
|
return stringWidth(text || '');
|
|
16
21
|
}
|
|
17
22
|
|
|
23
|
+
export function truncate(text, width) {
|
|
24
|
+
return cliTruncate(text || '', width, { position: 'end' });
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
export function wrapText(text, width) {
|
|
19
28
|
if (!text) return '';
|
|
20
|
-
return wrapAnsi(String(text), width, { trim: false, hard: true });
|
|
29
|
+
return wrapAnsi(String(text), Math.max(1, width), { trim: false, hard: true });
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
export function countLines(text, width) {
|
|
24
33
|
if (!text) return 1;
|
|
25
|
-
return wrapAnsi(String(text), width, { trim: false, hard: true }).split('\n').length;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function truncateText(text, maxLines, width) {
|
|
29
|
-
const wrapped = wrapText(text, width);
|
|
30
|
-
const lines = wrapped.split('\n');
|
|
31
|
-
if (lines.length <= maxLines) return wrapped;
|
|
32
|
-
return lines.slice(0, maxLines).join('\n');
|
|
34
|
+
return wrapAnsi(String(text), Math.max(1, width), { trim: false, hard: true }).split('\n').length;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export function measureEntry(entry, w) {
|
|
36
38
|
const nr = entry.role;
|
|
37
|
-
|
|
38
|
-
if (nr === '
|
|
39
|
+
const cw = Math.max(1, w);
|
|
40
|
+
if (nr === 'user') return 1 + countLines(entry.content, cw);
|
|
41
|
+
if (nr === 'assistant') return 2 + countLines(entry.content, cw) + (entry.duration ? 1 : 0);
|
|
39
42
|
if (nr === 'tool') return 1;
|
|
40
43
|
if (nr === 'system' || nr === 'error') return 1;
|
|
41
|
-
if (nr === 'streaming') return 2 + Math.min(
|
|
44
|
+
if (nr === 'streaming') return 2 + Math.min(30, countLines(entry.content, cw));
|
|
42
45
|
return 1;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
export function sliceToViewport(entries, viewportRows, w) {
|
|
46
|
-
const heights = entries.map(e => measureEntry(e, w));
|
|
49
|
+
const heights = entries.map(e => measureEntry(e, Math.max(1, w)));
|
|
47
50
|
let used = 0;
|
|
48
51
|
let start = entries.length;
|
|
49
52
|
let clipLines = 0;
|
|
@@ -59,54 +62,46 @@ export function sliceToViewport(entries, viewportRows, w) {
|
|
|
59
62
|
used += heights[i];
|
|
60
63
|
start = i;
|
|
61
64
|
}
|
|
62
|
-
|
|
63
|
-
return { slice, clipLines, clipIndex: clipLines > 0 ? 0 : -1 };
|
|
65
|
+
return { slice: entries.slice(start), clipLines, clipIndex: clipLines > 0 ? 0 : -1 };
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
export function buildLineArray(slice, clipIndex, clipLines, w) {
|
|
67
69
|
const lines = [];
|
|
70
|
+
const cw = Math.max(1, w);
|
|
68
71
|
for (let idx = 0; idx < slice.length; idx++) {
|
|
69
72
|
const e = slice[idx];
|
|
70
73
|
const nr = e.role;
|
|
71
74
|
let skip = 0;
|
|
72
75
|
if (idx === clipIndex && clipLines > 0) skip = clipLines;
|
|
76
|
+
|
|
73
77
|
if (nr === 'user') {
|
|
74
78
|
lines.push({ type: 'user_head', data: e });
|
|
75
|
-
const
|
|
76
|
-
const contentLines = wrapped.split('\n');
|
|
79
|
+
const contentLines = wrapText(e.content, cw).split('\n');
|
|
77
80
|
for (let ci = skip; ci < contentLines.length; ci++) {
|
|
78
81
|
lines.push({ type: 'user_line', text: contentLines[ci], data: e });
|
|
79
82
|
}
|
|
80
83
|
} else if (nr === 'assistant') {
|
|
81
84
|
lines.push({ type: 'asst_head', data: e });
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const wrapped = wrapText(e.content, w);
|
|
90
|
-
const contentLines = wrapped.split('\n');
|
|
91
|
-
for (let ci = skip - 1; ci < contentLines.length; ci++) {
|
|
92
|
-
lines.push({ type: 'asst_line', text: contentLines[ci], data: e });
|
|
93
|
-
}
|
|
85
|
+
const contentLines = wrapText(e.content, cw).split('\n');
|
|
86
|
+
const startIdx = skip > 0 ? skip - 1 : 0;
|
|
87
|
+
for (let ci = startIdx; ci < contentLines.length; ci++) {
|
|
88
|
+
lines.push({ type: 'asst_line', text: contentLines[ci], data: e });
|
|
89
|
+
}
|
|
90
|
+
if (e.duration && skip <= 0) {
|
|
91
|
+
lines.push({ type: 'asst_foot', text: String(e.duration), data: e });
|
|
94
92
|
}
|
|
95
|
-
if (e.duration) lines.push({ type: 'asst_foot', text: String(e.duration), data: e });
|
|
96
93
|
} else if (nr === 'tool') {
|
|
97
|
-
lines.push({ type: 'tool_line', text: (e.error ? '\u2716 ' : '\u25C9 ') + (e.toolName || 'tool') + (e.duration ? ' ' + e.duration + 'ms' : ''), data: e });
|
|
94
|
+
lines.push({ type: 'tool_line', text: truncate((e.error ? '\u2716 ' : '\u25C9 ') + (e.toolName || 'tool') + (e.duration ? ' ' + e.duration + 'ms' : ''), cw), data: e });
|
|
98
95
|
} else if (nr === 'system') {
|
|
99
|
-
if (skip <= 0) lines.push({ type: 'sys_line', text: e.content, data: e });
|
|
96
|
+
if (skip <= 0) lines.push({ type: 'sys_line', text: truncate(e.content, cw), data: e });
|
|
100
97
|
} else if (nr === 'error') {
|
|
101
|
-
if (skip <= 0) lines.push({ type: 'err_line', text: e.content, data: e });
|
|
98
|
+
if (skip <= 0) lines.push({ type: 'err_line', text: truncate(e.content, cw), data: e });
|
|
102
99
|
} else if (nr === 'streaming') {
|
|
103
100
|
lines.push({ type: 'stream_head', data: e });
|
|
104
|
-
if (e.status) lines.push({ type: 'stream_status', text: e.status, data: e });
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
const endLine = Math.min(contentLines.length, startLine + 40);
|
|
109
|
-
for (let ci = startLine; ci < endLine; ci++) {
|
|
101
|
+
if (e.status && skip <= 0) lines.push({ type: 'stream_status', text: e.status, data: e });
|
|
102
|
+
const contentLines = wrapText(e.content || '', cw).split('\n');
|
|
103
|
+
const startIdx = Math.min(skip, contentLines.length);
|
|
104
|
+
for (let ci = startIdx; ci < Math.min(contentLines.length, startIdx + 30); ci++) {
|
|
110
105
|
lines.push({ type: 'stream_line', text: contentLines[ci], data: e });
|
|
111
106
|
}
|
|
112
107
|
}
|
package/src/config/theme.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import gradient from 'gradient-string';
|
|
3
|
-
import figures from 'figures';
|
|
4
3
|
|
|
5
4
|
export const hex = {
|
|
6
5
|
bg: '#0A0A14',
|
|
@@ -10,7 +9,6 @@ export const hex = {
|
|
|
10
9
|
codeBg: '#0D0D18',
|
|
11
10
|
selectionBg: '#FF6B35',
|
|
12
11
|
selectionText: '#FFFFFF',
|
|
13
|
-
borderLight: '#202050',
|
|
14
12
|
accent: '#FF6B35',
|
|
15
13
|
purple: '#A855F7',
|
|
16
14
|
green: '#22C55E',
|
|
@@ -21,8 +19,6 @@ export const hex = {
|
|
|
21
19
|
text: '#EAEAEE',
|
|
22
20
|
textDim: '#8888AA',
|
|
23
21
|
textMuted: '#555577',
|
|
24
|
-
white: '#FFFFFF',
|
|
25
|
-
black: '#000000',
|
|
26
22
|
};
|
|
27
23
|
|
|
28
24
|
export const color = {
|
|
@@ -42,52 +38,6 @@ export const color = {
|
|
|
42
38
|
text: chalk.hex(hex.text),
|
|
43
39
|
textDim: chalk.hex(hex.textDim),
|
|
44
40
|
textMuted: chalk.hex(hex.textMuted),
|
|
45
|
-
white: chalk.hex(hex.white),
|
|
46
|
-
black: chalk.hex(hex.black),
|
|
47
41
|
};
|
|
48
42
|
|
|
49
|
-
export const
|
|
50
|
-
circle: '\u25C9',
|
|
51
|
-
dot: '\u25CF',
|
|
52
|
-
triR: '\u25B8',
|
|
53
|
-
triD: '\u25BE',
|
|
54
|
-
bullet: '\u25C9',
|
|
55
|
-
cross: '\u2716',
|
|
56
|
-
ellipsis: '\u2026',
|
|
57
|
-
mdash: '\u2014',
|
|
58
|
-
midDot: '\u00B7',
|
|
59
|
-
arrowR: '\u2192',
|
|
60
|
-
arrowU: '\u2191',
|
|
61
|
-
arrowD: '\u2193',
|
|
62
|
-
box: {
|
|
63
|
-
tl: '\u250C', tr: '\u2510', bl: '\u2514', br: '\u2518',
|
|
64
|
-
h: '\u2500', v: '\u2502',
|
|
65
|
-
},
|
|
66
|
-
treeJ: '\u2514',
|
|
67
|
-
treeT: '\u251C',
|
|
68
|
-
treeCon: '\u2502',
|
|
69
|
-
treeTip: '\u2570',
|
|
70
|
-
treeFork: '\u256D',
|
|
71
|
-
star: '\u2726',
|
|
72
|
-
gear: '\u2699',
|
|
73
|
-
pointer: '\u276F',
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
export const gradientText = gradient(['#FF6B35', '#3B82F6']);
|
|
77
|
-
export const gradientLine = gradient(['#FF6B35', '#3B82F6']);
|
|
78
|
-
|
|
79
|
-
export const LOGO = [
|
|
80
|
-
' ',
|
|
81
|
-
' ██████ ██ █████ ██████ ██ ████████ ██ ██ ',
|
|
82
|
-
' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
|
|
83
|
-
' ██████ ██ ███████ ██████ ██ ██ ██ ██ ',
|
|
84
|
-
' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
|
|
85
|
-
' ██ ███████ ██ ██ ██ ██ ██ ██ ██ ██████▄ ',
|
|
86
|
-
' ',
|
|
87
|
-
' █████ ██ ',
|
|
88
|
-
' ██ ██ ██ ',
|
|
89
|
-
' ██████ ██ ',
|
|
90
|
-
' ██ ██ ██ ',
|
|
91
|
-
' ██ ██ ███████ ',
|
|
92
|
-
' ',
|
|
93
|
-
];
|
|
43
|
+
export const appGradient = gradient(['#FF6B35', '#3B82F6']);
|