clarity-ai 6.6.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 +25 -0
- package/bin/clarity.js +21 -2
- package/package.json +8 -2
- package/src/components/AppRoot.js +15 -7
- package/src/components/CodeBlock.js +8 -13
- package/src/components/CommandPicker.js +19 -32
- package/src/components/Composer.js +11 -13
- package/src/components/HeaderBar.js +20 -0
- package/src/components/InputBar.js +59 -0
- package/src/components/Layout.js +15 -14
- package/src/components/LoadingIndicator.js +2 -2
- package/src/components/MessageList.js +21 -42
- package/src/components/ModelPicker.js +26 -34
- package/src/components/StatusBar.js +9 -6
- package/src/components/ThinkingBlock.js +11 -29
- package/src/components/ToolCard.js +6 -15
- package/src/config/layout.js +41 -52
- package/src/config/theme.js +2 -66
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
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
|
+
|
|
17
|
+
## 6.7.0 (2026-06-06)
|
|
18
|
+
|
|
19
|
+
### Premium Ink+React TUI Engine — zero-bleed grid
|
|
20
|
+
- **Hard-locked 3-tier grid**: Header(1) + Viewport(fill) + Dock(4) — absolute layout isolation
|
|
21
|
+
- **Floating picker overlays** via `position: absolute` — CommandPicker/ModelPicker never shift the message stream
|
|
22
|
+
- **Box-drawing borders** (`┌─┐│└─┘`) on all overlay panels
|
|
23
|
+
- **Orange `#FF6B35` selection bars** — full-width active item highlight
|
|
24
|
+
- **Gradient CLARITY header** via gradient-string (orange→blue)
|
|
25
|
+
- **Alternate screen buffer** (`fullscreen: true`) + SIGINT/SIGTERM cleanup
|
|
26
|
+
- **`string-width` render protection** — correct visual width for Unicode/emoji
|
|
27
|
+
- **Permanent ASCII logo** centered in empty viewport
|
|
28
|
+
- **ansi-escapes** for cursor hide/show, screen clear on boot
|
|
29
|
+
|
|
5
30
|
## 6.6.0 (2026-06-06)
|
|
6
31
|
|
|
7
32
|
### Premium UI rebuild + DeepSeek R1 reasoning models
|
package/bin/clarity.js
CHANGED
|
@@ -8,6 +8,10 @@ import { createInterface } from 'readline';
|
|
|
8
8
|
process.stdin.resume();
|
|
9
9
|
process.stdin.setEncoding('utf8');
|
|
10
10
|
|
|
11
|
+
const originalLog = console.log;
|
|
12
|
+
const originalError = console.error;
|
|
13
|
+
const originalWarn = console.warn;
|
|
14
|
+
|
|
11
15
|
async function main() {
|
|
12
16
|
const provider = process.env.CLARITY_PROVIDER || 'groq';
|
|
13
17
|
|
|
@@ -24,16 +28,27 @@ async function main() {
|
|
|
24
28
|
process.stdin.resume();
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
console.log = function sandboxedLog() {};
|
|
32
|
+
console.error = function sandboxedError() {};
|
|
33
|
+
console.warn = function sandboxedWarn() {};
|
|
34
|
+
|
|
35
|
+
let keepAlive;
|
|
36
|
+
|
|
27
37
|
const config = { provider, model: process.env.CLARITY_MODEL || 'groq/llama-3.3-70b-versatile' };
|
|
28
38
|
|
|
29
|
-
const { clear } = render(React.createElement(App, { config }), {
|
|
39
|
+
const { clear, waitUntilExit, rerender } = render(React.createElement(App, { config }), {
|
|
30
40
|
fullscreen: true,
|
|
31
41
|
patchConsole: false,
|
|
42
|
+
exitOnCtrlC: false,
|
|
32
43
|
});
|
|
33
44
|
|
|
34
|
-
setInterval(() => {}, 2 ** 31 - 1);
|
|
45
|
+
keepAlive = setInterval(() => {}, 2 ** 31 - 1);
|
|
35
46
|
|
|
36
47
|
function cleanup() {
|
|
48
|
+
clearInterval(keepAlive);
|
|
49
|
+
console.log = originalLog;
|
|
50
|
+
console.error = originalError;
|
|
51
|
+
console.warn = originalWarn;
|
|
37
52
|
try { clear(); } catch {}
|
|
38
53
|
process.stdout.write('\x1b[?25h\x1b[0m');
|
|
39
54
|
process.exit(0);
|
|
@@ -41,11 +56,15 @@ async function main() {
|
|
|
41
56
|
|
|
42
57
|
process.on('SIGINT', () => cleanup());
|
|
43
58
|
process.on('SIGTERM', () => cleanup());
|
|
59
|
+
process.on('exit', () => { process.stdout.write('\x1b[?25h\x1b[0m'); });
|
|
44
60
|
|
|
45
61
|
await new Promise(() => {});
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
main().catch(err => {
|
|
65
|
+
console.log = originalLog;
|
|
66
|
+
console.error = originalError;
|
|
67
|
+
console.warn = originalWarn;
|
|
49
68
|
process.stdout.write('\x1b[?25h\x1b[0m');
|
|
50
69
|
console.error('\n\x1b[31mFatal error:\x1b[0m', err.message);
|
|
51
70
|
process.exit(1);
|
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": {
|
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
"react": "^18",
|
|
24
24
|
"ink-spinner": "^5",
|
|
25
25
|
"chalk": "^5",
|
|
26
|
-
"wrap-ansi": "^9"
|
|
26
|
+
"wrap-ansi": "^9",
|
|
27
|
+
"gradient-string": "^3",
|
|
28
|
+
"figures": "^6",
|
|
29
|
+
"ansi-escapes": "^7",
|
|
30
|
+
"string-width": "^7",
|
|
31
|
+
"@inkjs/ui": "^2",
|
|
32
|
+
"cli-truncate": "^6"
|
|
27
33
|
}
|
|
28
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
|
|
|
@@ -34,11 +35,9 @@ export function App({ config }) {
|
|
|
34
35
|
const stateRef = useRef(state);
|
|
35
36
|
const modelRef = useRef(model);
|
|
36
37
|
const providerRef = useRef(provider);
|
|
37
|
-
const streamRef = useRef(streamContent);
|
|
38
38
|
stateRef.current = state;
|
|
39
39
|
modelRef.current = model;
|
|
40
40
|
providerRef.current = provider;
|
|
41
|
-
streamRef.current = streamContent;
|
|
42
41
|
|
|
43
42
|
const onSubmit = useCallback(async (input) => {
|
|
44
43
|
if (input === '/exit') { process.exit(0); return; }
|
|
@@ -59,7 +58,7 @@ export function App({ config }) {
|
|
|
59
58
|
await handleSend(stateRef.current, setState, input, modelRef.current, providerRef.current, setStreamContent, ac.signal);
|
|
60
59
|
}, []);
|
|
61
60
|
|
|
62
|
-
function
|
|
61
|
+
function handleCmdSelect(cmdName) {
|
|
63
62
|
setShowCommands(false);
|
|
64
63
|
onSubmit(cmdName);
|
|
65
64
|
}
|
|
@@ -74,11 +73,20 @@ export function App({ config }) {
|
|
|
74
73
|
}));
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
|
|
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%' },
|
|
78
86
|
h(Layout, {
|
|
79
87
|
state, streamContent, model, provider,
|
|
80
88
|
showCommands, showModels,
|
|
81
|
-
onCommandSelect:
|
|
89
|
+
onCommandSelect: handleCmdSelect,
|
|
82
90
|
onModelSelect: handleModelSelect,
|
|
83
91
|
onCloseCommands: () => setShowCommands(false),
|
|
84
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();
|
|
@@ -36,51 +36,38 @@ export function CommandPicker({ query, onSelect, onClose }) {
|
|
|
36
36
|
|
|
37
37
|
const w = Math.min(cols - 4, 48);
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
const isSel = i === idx;
|
|
41
|
-
return h(Box, {
|
|
42
|
-
key: cmd.name, height: 1,
|
|
43
|
-
backgroundColor: isSel ? hex.selectionBg : hex.bg,
|
|
44
|
-
},
|
|
45
|
-
h(Text, {
|
|
46
|
-
color: isSel ? hex.selectionText : hex.text,
|
|
47
|
-
bold: isSel,
|
|
48
|
-
backgroundColor: isSel ? hex.selectionBg : hex.bg,
|
|
49
|
-
}, ' ' + (isSel ? '\u276F ' : ' ') + cmd.name),
|
|
50
|
-
h(Text, {
|
|
51
|
-
color: isSel ? hex.selectionText : hex.textDim,
|
|
52
|
-
backgroundColor: isSel ? hex.selectionBg : hex.bg,
|
|
53
|
-
}, ' ' + cmd.desc)
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const maxH = Math.min(items.length + 3, 16);
|
|
58
|
-
|
|
59
|
-
return h(Box, { flexDirection: 'column', width: w, marginLeft: 2, backgroundColor: hex.bg },
|
|
39
|
+
return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: w },
|
|
60
40
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
61
41
|
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
62
|
-
|
|
42
|
+
'\u250C' + '\u2500'.repeat(w - 2) + '\u2510')
|
|
63
43
|
),
|
|
64
44
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
65
|
-
h(Text, { color: hex.gold, backgroundColor: hex.surfaceAlt }, sym.box.v + ' '),
|
|
66
|
-
h(Text, { color: hex.text, backgroundColor: hex.surfaceAlt }, 'Commands'),
|
|
67
45
|
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
68
|
-
'
|
|
46
|
+
'\u2502 Commands' + ' '.repeat(Math.max(0, w - 12)) + '\u2502')
|
|
69
47
|
),
|
|
70
48
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
71
49
|
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
72
|
-
|
|
50
|
+
'\u2502 ' + (search || '\u2026') + ' '.repeat(Math.max(0, w - 6)) + '\u2502')
|
|
73
51
|
),
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
)
|
|
76
63
|
),
|
|
77
64
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
78
65
|
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
79
|
-
|
|
66
|
+
'\u2514' + '\u2500'.repeat(w - 2) + '\u2518')
|
|
80
67
|
),
|
|
81
68
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
82
69
|
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
83
|
-
'
|
|
70
|
+
' \u2191\u2193 nav \u2192 select Esc close')
|
|
84
71
|
),
|
|
85
72
|
);
|
|
86
73
|
}
|
|
@@ -13,7 +13,7 @@ export function Composer({ provider, model, agentMode, thinking, onSlash, onSubm
|
|
|
13
13
|
r.current = input;
|
|
14
14
|
|
|
15
15
|
const { cols } = getLayout();
|
|
16
|
-
const w = Math.max(10, cols -
|
|
16
|
+
const w = Math.max(10, cols - 6);
|
|
17
17
|
const lineCount = Math.max(1, Math.ceil((input.length || 1) / w));
|
|
18
18
|
const visible = Math.min(lineCount, MAX_ROWS);
|
|
19
19
|
const mShort = model.replace(/^[^/]+\//, '').slice(0, 18);
|
|
@@ -45,11 +45,11 @@ export function Composer({ provider, model, agentMode, thinking, onSlash, onSubm
|
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
const rows = [];
|
|
49
|
+
rows.push(h(Box, { key: 'sep', height: 1, backgroundColor: hex.surfaceAlt },
|
|
50
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
51
|
+
' \u250C' + sym.box.h.repeat(Math.max(0, cols - 6)) + '\u2510')
|
|
52
|
+
));
|
|
53
53
|
|
|
54
54
|
for (let i = 0; i < MAX_ROWS; i++) {
|
|
55
55
|
const start = i * w;
|
|
@@ -60,17 +60,15 @@ export function Composer({ provider, model, agentMode, thinking, onSlash, onSubm
|
|
|
60
60
|
color: isPlaceholder ? hex.textMuted : hex.text,
|
|
61
61
|
backgroundColor: hex.bg,
|
|
62
62
|
wrap: 'truncate-end',
|
|
63
|
-
}, ' \u2502 ' + (seg || (i === 0 && isPlaceholder ? 'type a message...' : ' '))
|
|
63
|
+
}, ' \u2502 ' + (seg || (i === 0 && isPlaceholder ? 'type a message...' : ' ')))
|
|
64
64
|
)
|
|
65
65
|
);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
rows.push(
|
|
69
|
-
h(
|
|
70
|
-
h(
|
|
71
|
-
|
|
72
|
-
)
|
|
73
|
-
);
|
|
68
|
+
rows.push(h(Box, { key: 'st', height: 1, backgroundColor: hex.surfaceAlt },
|
|
69
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
70
|
+
' \u2514' + sym.box.h + ' ' + provider + ' \u00B7 ' + mShort + (agentMode ? ' \u00B7 AGENT' : '') + ' \u00B7 Ctrl+P')
|
|
71
|
+
));
|
|
74
72
|
|
|
75
73
|
return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt }, ...rows);
|
|
76
74
|
}
|
|
@@ -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,29 +1,30 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Box } from 'ink';
|
|
3
3
|
import { hex } from '../config/theme.js';
|
|
4
|
-
import {
|
|
4
|
+
import { HeaderBar } from './HeaderBar.js';
|
|
5
5
|
import { MessageList } from './MessageList.js';
|
|
6
|
-
import {
|
|
6
|
+
import { InputBar } from './InputBar.js';
|
|
7
7
|
import { CommandPicker } from './CommandPicker.js';
|
|
8
8
|
import { ModelPicker } from './ModelPicker.js';
|
|
9
9
|
const { createElement: h } = React;
|
|
10
10
|
|
|
11
11
|
export function Layout({ state, streamContent, model, provider, showCommands, showModels, onCommandSelect, onModelSelect, onCloseCommands, onCloseModels, onSlash, onSubmit }) {
|
|
12
|
-
|
|
13
|
-
h(
|
|
14
|
-
|
|
12
|
+
const picker = showCommands || showModels
|
|
13
|
+
? h(Box, { position: 'absolute', top: 1, left: 2, backgroundColor: hex.bg, flexDirection: 'column' },
|
|
14
|
+
showCommands ? h(CommandPicker, { onSelect: onCommandSelect, onClose: onCloseCommands }) : null,
|
|
15
|
+
showModels ? h(ModelPicker, { onSelect: onModelSelect, onClose: onCloseModels }) : null
|
|
16
|
+
)
|
|
17
|
+
: null;
|
|
18
|
+
|
|
19
|
+
return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: '100%', height: '100%' },
|
|
20
|
+
h(HeaderBar, { model, provider, agentMode: state.agentMode, thinking: state.thinking }),
|
|
21
|
+
h(Box, { flexGrow: 1, flexDirection: 'column', position: 'relative' },
|
|
15
22
|
h(MessageList, {
|
|
16
23
|
messages: state.messages, thinking: state.thinking,
|
|
17
24
|
streamContent, agentStatus: state.agentStatus,
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
}),
|
|
26
|
+
picker
|
|
20
27
|
),
|
|
21
|
-
|
|
22
|
-
? h(Box, { flexDirection: 'column', backgroundColor: hex.bg },
|
|
23
|
-
showCommands ? h(CommandPicker, { query: '', onSelect: onCommandSelect, onClose: onCloseCommands }) : null,
|
|
24
|
-
showModels ? h(ModelPicker, { onSelect: onModelSelect, onClose: onCloseModels }) : null
|
|
25
|
-
)
|
|
26
|
-
: null,
|
|
27
|
-
h(Composer, { provider, model, agentMode: state.agentMode, thinking: state.thinking, onSlash, onSubmit })
|
|
28
|
+
h(InputBar, { provider, model, agentMode: state.agentMode, thinking: state.thinking, onSlash, onSubmit })
|
|
28
29
|
);
|
|
29
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 },
|
|
@@ -16,7 +16,7 @@ function Line({ type, text, data }) {
|
|
|
16
16
|
);
|
|
17
17
|
case 'asst_head':
|
|
18
18
|
return h(Box, { height: 1, backgroundColor: hex.surface },
|
|
19
|
-
h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, '
|
|
19
|
+
h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' \u25C6 CLARITY')
|
|
20
20
|
);
|
|
21
21
|
case 'asst_line':
|
|
22
22
|
return h(Box, { height: 1, backgroundColor: hex.surface },
|
|
@@ -24,27 +24,28 @@ 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 },
|
|
43
|
-
h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, '
|
|
44
|
+
h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' \u25C6 CLARITY')
|
|
44
45
|
);
|
|
45
46
|
case 'stream_status':
|
|
46
47
|
return h(Box, { height: 1, backgroundColor: hex.surface },
|
|
47
|
-
h(Text, { color: hex.blue, backgroundColor: hex.surface }, '
|
|
48
|
+
h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' \u25CF ' + (text || ''))
|
|
48
49
|
);
|
|
49
50
|
case 'stream_line':
|
|
50
51
|
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,47 +66,25 @@ export function MessageList({ messages, thinking, streamContent, agentStatus, to
|
|
|
65
66
|
}));
|
|
66
67
|
}, [messages]);
|
|
67
68
|
|
|
68
|
-
const showLogo = entries.length <= 1 && !thinking && !streamContent;
|
|
69
|
-
|
|
70
69
|
const { slice, clipIndex, clipLines } = useMemo(
|
|
71
|
-
() => sliceToViewport(entries,
|
|
72
|
-
[entries, viewport, contentWidth
|
|
70
|
+
() => sliceToViewport(entries, viewport, contentWidth),
|
|
71
|
+
[entries, viewport, contentWidth]
|
|
73
72
|
);
|
|
74
73
|
|
|
75
|
-
const
|
|
74
|
+
const rawLines = useMemo(
|
|
76
75
|
() => buildLineArray(slice, clipIndex, clipLines, contentWidth),
|
|
77
76
|
[slice, clipIndex, clipLines, contentWidth]
|
|
78
77
|
);
|
|
79
78
|
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
} else {
|
|
86
|
-
for (let i = padded.length; i < viewport; i++) {
|
|
87
|
-
padded.unshift({ type: 'empty' });
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const logoVisible = showLogo;
|
|
92
|
-
const totalHeight = viewport;
|
|
79
|
+
const fillCount = Math.max(0, viewport - rawLines.length);
|
|
80
|
+
const padded = [];
|
|
81
|
+
for (let i = 0; i < fillCount; i++) padded.push({ type: 'empty' });
|
|
82
|
+
for (const ln of rawLines) padded.push(ln);
|
|
93
83
|
|
|
94
|
-
return h(Box, { height:
|
|
84
|
+
return h(Box, { height: viewport, flexDirection: 'column', overflow: 'hidden' },
|
|
95
85
|
padded.map((ln, i) => {
|
|
96
|
-
if (ln.type === 'empty') {
|
|
97
|
-
|
|
98
|
-
const logoLine = LOGO[i] || '';
|
|
99
|
-
return h(Box, { key: 'e' + i, height: 1, backgroundColor: hex.bg },
|
|
100
|
-
h(Text, {
|
|
101
|
-
color: (i === 1 || i === 9 || i === 10 || i === 11) ? hex.accent : hex.textMuted,
|
|
102
|
-
backgroundColor: hex.bg,
|
|
103
|
-
}, ' ' + logoLine)
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
return h(Box, { key: 'e' + i, height: 1, backgroundColor: hex.bg });
|
|
107
|
-
}
|
|
108
|
-
return h(Box, { key: (ln.data?.id || 'l') + '-' + i, height: 1 },
|
|
86
|
+
if (ln.type === 'empty') return h(Box, { key: 'e' + i, height: 1, backgroundColor: hex.bg });
|
|
87
|
+
return h(Box, { key: (ln.data?.id || 'r') + '-' + i, height: 1 },
|
|
109
88
|
h(Line, { type: ln.type, text: ln.text, data: ln.data })
|
|
110
89
|
);
|
|
111
90
|
})
|
|
@@ -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
|
|
|
@@ -39,53 +39,45 @@ export function ModelPicker({ onSelect, onClose }) {
|
|
|
39
39
|
|
|
40
40
|
const w = Math.min(cols - 4, 52);
|
|
41
41
|
|
|
42
|
-
|
|
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 - 8)) + 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 + ']' : '') + ' '.repeat(Math.max(0, w - m.label.length - (m.badge ? m.badge.length + 4 : 0) - 4)) + (isSel ? '\u276F' : ' '))
|
|
59
|
-
);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const count = flat.filter(m => !m._header).length;
|
|
63
|
-
|
|
64
|
-
return h(Box, { flexDirection: 'column', width: w, marginLeft: 2, backgroundColor: hex.bg },
|
|
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.gold, backgroundColor: hex.surfaceAlt }, sym.box.v + ' '),
|
|
71
|
-
h(Text, { color: hex.text, backgroundColor: hex.surfaceAlt }, 'Models'),
|
|
72
48
|
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
73
|
-
' ' +
|
|
49
|
+
'\u2502 Models ' + flat.filter(m => !m._header).length + ' available' + ' '.repeat(Math.max(0, w - 18)) + '\u2502')
|
|
74
50
|
),
|
|
75
51
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
76
52
|
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
77
|
-
|
|
78
|
-
),
|
|
79
|
-
h(Box, { flexDirection: 'column', backgroundColor: hex.bg },
|
|
80
|
-
...items,
|
|
53
|
+
'\u2502 ' + (search || '\u2026') + ' '.repeat(Math.max(0, w - 6)) + '\u2502')
|
|
81
54
|
),
|
|
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
|
+
}),
|
|
82
74
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
83
75
|
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
84
|
-
|
|
76
|
+
'\u2514' + '\u2500'.repeat(w - 2) + '\u2518')
|
|
85
77
|
),
|
|
86
78
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
87
79
|
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
88
|
-
'
|
|
80
|
+
' \u2191\u2193 nav \u2192 select Esc close')
|
|
89
81
|
),
|
|
90
82
|
);
|
|
91
83
|
}
|
|
@@ -1,16 +1,19 @@
|
|
|
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
|
import { getLayout } from '../config/layout.js';
|
|
5
5
|
const { createElement: h } = React;
|
|
6
6
|
|
|
7
7
|
export function StatusBar({ model, provider, agentMode, thinking }) {
|
|
8
8
|
const { cols } = getLayout();
|
|
9
|
-
const m = model.replace(/^[^/]+\//, '').slice(0,
|
|
10
|
-
const left =
|
|
11
|
-
const right = (agentMode ? '\u25C8 AGENT' : '\u25CB USER') + (thinking ? ' '
|
|
12
|
-
const gap = Math.max(1, cols - left.length - right.length -
|
|
9
|
+
const m = model.replace(/^[^/]+\//, '').slice(0, 20);
|
|
10
|
+
const left = '\u25C9 CLARITY \u00B7 ' + m + ' \u00B7 ' + provider;
|
|
11
|
+
const right = (agentMode ? '\u25C8 AGENT' : '\u25CB USER') + (thinking ? ' \u25CF' : '');
|
|
12
|
+
const gap = Math.max(1, cols - left.length - right.length - 2);
|
|
13
|
+
|
|
13
14
|
return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
14
|
-
h(Text, { color: hex.
|
|
15
|
+
h(Text, { color: hex.accent, bold: true, backgroundColor: hex.surfaceAlt }, ' ' + left),
|
|
16
|
+
h(Text, { backgroundColor: hex.surfaceAlt }, ' '.repeat(gap)),
|
|
17
|
+
h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, right + ' ')
|
|
15
18
|
);
|
|
16
19
|
}
|
|
@@ -1,45 +1,27 @@
|
|
|
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')
|
|
13
11
|
: '';
|
|
14
12
|
|
|
15
|
-
const headerText = sym.triR + ' Thought' + (durStr ? ' (' + durStr + ')' : '');
|
|
16
|
-
const icon = collapsed ? sym.triR : sym.triD;
|
|
17
|
-
|
|
18
13
|
return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
|
|
19
14
|
h(Box, { height: 1 },
|
|
20
|
-
h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt },
|
|
15
|
+
h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt },
|
|
16
|
+
' ' + (collapsed ? '\u25B8' : '\u25BE') + ' Thought' + (durStr ? ' (' + durStr + ')' : ''))
|
|
21
17
|
),
|
|
22
18
|
collapsed ? null : h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
|
|
23
|
-
items.map((tr, i) =>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const td = tr.duration ? ' ' + tr.duration + 'ms' : '';
|
|
30
|
-
const line = ' ' + prefix + ' ' + ico + ' ' + tr.name + td;
|
|
31
|
-
const contentLine = tr.content && tr.content.length < 200
|
|
32
|
-
? ' ' + conn + ' ' + String(tr.content).slice(0, cols - 14)
|
|
33
|
-
: null;
|
|
34
|
-
return h(Box, { key: tr.execId || i, flexDirection: 'column' },
|
|
35
|
-
h(Box, { height: 1 },
|
|
36
|
-
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, line)
|
|
37
|
-
),
|
|
38
|
-
contentLine ? h(Box, { height: 1 },
|
|
39
|
-
h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, contentLine)
|
|
40
|
-
) : null
|
|
41
|
-
);
|
|
42
|
-
})
|
|
19
|
+
items.map((tr, i) =>
|
|
20
|
+
h(Box, { key: tr.execId || i, height: 1 },
|
|
21
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
22
|
+
' ' + (i === items.length - 1 ? '\u2570\u2500' : '\u256D\u2500') + ' ' + (tr.status === 'failed' ? '\u2716' : '\u25C9') + ' ' + tr.name + (tr.duration ? ' ' + tr.duration + 'ms' : ''))
|
|
23
|
+
)
|
|
24
|
+
)
|
|
43
25
|
)
|
|
44
26
|
);
|
|
45
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,31 +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 }, '
|
|
37
|
-
)
|
|
38
|
-
: null,
|
|
39
|
-
status === 'failed' && exec.error
|
|
40
|
-
? h(Box, { height: 1 },
|
|
41
|
-
h(Text, { color: hex.red, backgroundColor: hex.surfaceAlt }, ' ' + sym.cross + ' ' + String(exec.error).slice(0, w - 4))
|
|
32
|
+
h(Text, { color: hex.blue, backgroundColor: hex.surfaceAlt }, ' \u25CF running')
|
|
42
33
|
)
|
|
43
34
|
: null,
|
|
44
35
|
);
|
package/src/config/layout.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import wrapAnsi from 'wrap-ansi';
|
|
2
|
+
import stringWidth from 'string-width';
|
|
3
|
+
import cliTruncate from 'cli-truncate';
|
|
2
4
|
|
|
3
5
|
export function getLayout() {
|
|
4
6
|
const rows = process.stdout.rows || 30;
|
|
@@ -6,44 +8,45 @@ export function getLayout() {
|
|
|
6
8
|
return {
|
|
7
9
|
rows,
|
|
8
10
|
cols,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
viewport: Math.max(
|
|
12
|
-
contentWidth: Math.max(
|
|
13
|
-
|
|
14
|
-
padRight: ' ',
|
|
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,
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
export function sw(text) {
|
|
20
|
+
return stringWidth(text || '');
|
|
21
|
+
}
|
|
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
|
-
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);
|
|
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,60 +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
|
-
lines.push({ type: 'asst_line', text: contentLines[ci], data: e });
|
|
87
|
-
}
|
|
88
|
-
} else {
|
|
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 });
|
|
94
89
|
}
|
|
95
|
-
if (e.duration
|
|
96
|
-
|
|
97
|
-
const label = (e.error ? '\u2716 ' : '\u25C9 ') + (e.toolName || 'tool');
|
|
98
|
-
if (e.completed) {
|
|
99
|
-
if (skip <= 0) lines.push({ type: 'tool_line', text: label + ' ' + (e.duration ? e.duration + 'ms' : ''), data: e });
|
|
100
|
-
} else {
|
|
101
|
-
lines.push({ type: 'tool_line', text: label, data: e });
|
|
90
|
+
if (e.duration && skip <= 0) {
|
|
91
|
+
lines.push({ type: 'asst_foot', text: String(e.duration), data: e });
|
|
102
92
|
}
|
|
93
|
+
} else if (nr === 'tool') {
|
|
94
|
+
lines.push({ type: 'tool_line', text: truncate((e.error ? '\u2716 ' : '\u25C9 ') + (e.toolName || 'tool') + (e.duration ? ' ' + e.duration + 'ms' : ''), cw), data: e });
|
|
103
95
|
} else if (nr === 'system') {
|
|
104
|
-
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 });
|
|
105
97
|
} else if (nr === 'error') {
|
|
106
|
-
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 });
|
|
107
99
|
} else if (nr === 'streaming') {
|
|
108
100
|
lines.push({ type: 'stream_head', data: e });
|
|
109
|
-
if (e.status) lines.push({ type: 'stream_status', text: e.status, data: e });
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
const startLine = Math.min(skip, contentLines.length);
|
|
114
|
-
const endLine = Math.min(contentLines.length, startLine + (maxLines - lines.length));
|
|
115
|
-
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++) {
|
|
116
105
|
lines.push({ type: 'stream_line', text: contentLines[ci], data: e });
|
|
117
106
|
}
|
|
118
107
|
}
|
package/src/config/theme.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import gradient from 'gradient-string';
|
|
2
3
|
|
|
3
4
|
export const hex = {
|
|
4
5
|
bg: '#0A0A14',
|
|
@@ -8,7 +9,6 @@ export const hex = {
|
|
|
8
9
|
codeBg: '#0D0D18',
|
|
9
10
|
selectionBg: '#FF6B35',
|
|
10
11
|
selectionText: '#FFFFFF',
|
|
11
|
-
borderLight: '#202050',
|
|
12
12
|
accent: '#FF6B35',
|
|
13
13
|
purple: '#A855F7',
|
|
14
14
|
green: '#22C55E',
|
|
@@ -19,11 +19,6 @@ export const hex = {
|
|
|
19
19
|
text: '#EAEAEE',
|
|
20
20
|
textDim: '#8888AA',
|
|
21
21
|
textMuted: '#555577',
|
|
22
|
-
white: '#FFFFFF',
|
|
23
|
-
black: '#000000',
|
|
24
|
-
modalOverlay: 'rgba(0,0,0,0.85)',
|
|
25
|
-
logoOrange: '#FF6B35',
|
|
26
|
-
logoBlue: '#3B82F6',
|
|
27
22
|
};
|
|
28
23
|
|
|
29
24
|
export const color = {
|
|
@@ -43,65 +38,6 @@ export const color = {
|
|
|
43
38
|
text: chalk.hex(hex.text),
|
|
44
39
|
textDim: chalk.hex(hex.textDim),
|
|
45
40
|
textMuted: chalk.hex(hex.textMuted),
|
|
46
|
-
white: chalk.hex(hex.white),
|
|
47
|
-
black: chalk.hex(hex.black),
|
|
48
41
|
};
|
|
49
42
|
|
|
50
|
-
export const
|
|
51
|
-
diamond: '\u25C6',
|
|
52
|
-
circle: '\u25C9',
|
|
53
|
-
dot: '\u25CF',
|
|
54
|
-
smallDot: '\u25CB',
|
|
55
|
-
triR: '\u25B8',
|
|
56
|
-
triD: '\u25BE',
|
|
57
|
-
bullet: '\u25C9',
|
|
58
|
-
cross: '\u2716',
|
|
59
|
-
ellipsis: '\u2026',
|
|
60
|
-
mdash: '\u2014',
|
|
61
|
-
ndash: '\u2013',
|
|
62
|
-
midDot: '\u00B7',
|
|
63
|
-
arrowR: '\u2192',
|
|
64
|
-
arrowL: '\u2190',
|
|
65
|
-
arrowU: '\u2191',
|
|
66
|
-
arrowD: '\u2193',
|
|
67
|
-
lightV: '\u2502',
|
|
68
|
-
lightH: '\u2500',
|
|
69
|
-
box: {
|
|
70
|
-
tl: '\u250C', tr: '\u2510', bl: '\u2514', br: '\u2518',
|
|
71
|
-
h: '\u2500', v: '\u2502',
|
|
72
|
-
tm: '\u252C', bm: '\u2534', lm: '\u251C', rm: '\u2524',
|
|
73
|
-
cross: '\u253C',
|
|
74
|
-
},
|
|
75
|
-
powerline: {
|
|
76
|
-
padlock: '\uE0B0',
|
|
77
|
-
rpadlock: '\uE0B2',
|
|
78
|
-
},
|
|
79
|
-
treeJ: '\u2514',
|
|
80
|
-
treeT: '\u251C',
|
|
81
|
-
treeCon: '\u2502',
|
|
82
|
-
triR2: '\u25B8',
|
|
83
|
-
triD2: '\u25BE',
|
|
84
|
-
u: { h: '\u2500' },
|
|
85
|
-
treeTip: '\u2570',
|
|
86
|
-
treeFork: '\u256D',
|
|
87
|
-
star: '\u2726',
|
|
88
|
-
asterisk: '\u2731',
|
|
89
|
-
gear: '\u2699',
|
|
90
|
-
thought: '\u25CB',
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
export const LOGO = [
|
|
94
|
-
' ',
|
|
95
|
-
' ██████ ██ █████ ██████ ██ ████████ ██ ██ ',
|
|
96
|
-
' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
|
|
97
|
-
' ██████ ██ ███████ ██████ ██ ██ ██ ██ ',
|
|
98
|
-
' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
|
|
99
|
-
' ██ ███████ ██ ██ ██ ██ ██ ██ ██████ ',
|
|
100
|
-
' ',
|
|
101
|
-
' █████ ██ ',
|
|
102
|
-
' ██ ██ ██ ',
|
|
103
|
-
' ██████ ██ ',
|
|
104
|
-
' ██ ██ ██ ',
|
|
105
|
-
' ██ ██ ███████ ',
|
|
106
|
-
' ',
|
|
107
|
-
];
|
|
43
|
+
export const appGradient = gradient(['#FF6B35', '#3B82F6']);
|