clarity-ai 7.2.1 → 7.3.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 +13 -0
- package/package.json +1 -1
- package/src/app.js +72 -44
- package/src/components/{StreamView.js → ChatViewport.js} +13 -11
- package/src/components/ComposerDock.js +39 -0
- package/src/components/ThinkingPanel.js +40 -0
- package/src/components/TopPanel.js +85 -0
- package/src/config/theme.js +3 -0
- package/src/components/Banner.js +0 -36
- package/src/components/Footer.js +0 -24
- package/src/components/InputDock.js +0 -29
- package/src/components/StatusBar.js +0 -45
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## 7.3.0 (2026-06-06)
|
|
6
|
+
|
|
7
|
+
### Panel-driven TUI overhaul — structured frame layout with live thinking stream
|
|
8
|
+
- **Bordered TopPanel**: ASCII banner + status chip + hotkey row wrapped in `┌─┐│├─┤` frame. No more floating text lines. Status badge, hotkey hints, and gradient logo all share a single structured surface.
|
|
9
|
+
- **Anchored ComposerDock**: Box-drawn (`┌─┐│└─┘`) input panel hard-locked to bottom rows. Contains TextInput, provider/model/AGENT status line, and right-aligned `tab agents ctrl+p` hotkey hints. Never moves during streaming.
|
|
10
|
+
- **Live thinking stream engine**: Real-time `<think>...</think>` tag parser splits incoming token stream into:
|
|
11
|
+
- `reasoning` → collapsible low-contrast `#14141E` panel with dim gray text
|
|
12
|
+
- `display` → main chat viewport (cleaned, no reasoning noise)
|
|
13
|
+
- `Ctrl+T` toggles thinking panel visibility
|
|
14
|
+
- **ThinkingPanel component**: Bordered frame with chevron toggle, character count, and last 8 wrapped lines of reasoning text. Sits inside chat viewport without polluting the final output.
|
|
15
|
+
- **Box-drawing panel boundaries**: `┌ ─ ┐│├ ─ ┤└ ─ ┘` Unicode frames on all structural panels. Border color `#2A2A3A` for subtle but professional separation.
|
|
16
|
+
- **Orange selection accent**: `#FF9F43` full-width highlight bars with `#000000` black text on all active popup rows and status chips.
|
|
17
|
+
|
|
5
18
|
## 7.2.1 (2026-06-06)
|
|
6
19
|
|
|
7
20
|
### Critical hotfix: mouse bleed elimination, CLARITY branding, TrueColor textures
|
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -2,16 +2,13 @@ import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
|
2
2
|
import { Box, useStdout, useInput } from 'ink';
|
|
3
3
|
import { createChatState, handleSend, handleCommand } from './chat.js';
|
|
4
4
|
import { hex } from './config/theme.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { InputDock } from './components/InputDock.js';
|
|
5
|
+
import { TopPanel } from './components/TopPanel.js';
|
|
6
|
+
import { ChatViewport } from './components/ChatViewport.js';
|
|
7
|
+
import { ComposerDock } from './components/ComposerDock.js';
|
|
9
8
|
import { SlashPopup } from './components/SlashPopup.js';
|
|
10
|
-
import { Footer } from './components/Footer.js';
|
|
11
9
|
const { createElement: h } = React;
|
|
12
10
|
|
|
13
11
|
let abortController = null;
|
|
14
|
-
|
|
15
12
|
export function getAbortController() { return abortController; }
|
|
16
13
|
export function setAbortController(ac) { abortController = ac; }
|
|
17
14
|
export function cancelStream() {
|
|
@@ -19,8 +16,7 @@ export function cancelStream() {
|
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
const DOCK_H = 4;
|
|
22
|
-
const
|
|
23
|
-
const STATUS_H = 1;
|
|
19
|
+
const TOP_H_PRE = 2;
|
|
24
20
|
const POPUP_W = 44;
|
|
25
21
|
|
|
26
22
|
const COMMAND_ITEMS = [
|
|
@@ -44,14 +40,31 @@ function getFiltered(input) {
|
|
|
44
40
|
return COMMAND_ITEMS.filter(c => c.name.includes(q));
|
|
45
41
|
}
|
|
46
42
|
|
|
43
|
+
function parseStream(text) {
|
|
44
|
+
let reasoning = '';
|
|
45
|
+
let display = '';
|
|
46
|
+
let inThink = false;
|
|
47
|
+
let i = 0;
|
|
48
|
+
while (i < text.length) {
|
|
49
|
+
if (text.startsWith('<think>', i)) { inThink = true; i += 7; }
|
|
50
|
+
else if (text.startsWith('</think>', i)) { inThink = false; i += 8; }
|
|
51
|
+
else { if (inThink) reasoning += text[i]; else display += text[i]; i++; }
|
|
52
|
+
}
|
|
53
|
+
return { reasoning, display };
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
export function App({ config }) {
|
|
48
57
|
const { stdout } = useStdout();
|
|
49
58
|
const [dims, setDims] = useState({ rows: stdout.rows || 30, cols: stdout.columns || 80 });
|
|
50
59
|
const [state, setState] = useState(() => createChatState());
|
|
51
60
|
const [streamContent, setStreamContent] = useState('');
|
|
61
|
+
const [streamReasoning, setStreamReasoning] = useState('');
|
|
62
|
+
const [streamDisplay, setStreamDisplay] = useState('');
|
|
52
63
|
const [thinkingStart, setThinkingStart] = useState(null);
|
|
64
|
+
const [thinkingMs, setThinkingMs] = useState(0);
|
|
53
65
|
const [input, setInput] = useState('');
|
|
54
66
|
const [popupIdx, setPopupIdx] = useState(0);
|
|
67
|
+
const [showThinking, setShowThinking] = useState(true);
|
|
55
68
|
const defaultModel = (config.model || '').replace(/^[^/]+\//, '') || 'llama-3.3-70b-versatile';
|
|
56
69
|
const [model, setModel] = useState(defaultModel);
|
|
57
70
|
const [provider, setProvider] = useState(config.provider || 'groq');
|
|
@@ -66,11 +79,12 @@ export function App({ config }) {
|
|
|
66
79
|
const rows = dims.rows;
|
|
67
80
|
const cols = dims.cols;
|
|
68
81
|
const bannerH = cols < 50 ? 2 : (cols < 60 ? 3 : 5);
|
|
82
|
+
const topH = bannerH + TOP_H_PRE;
|
|
69
83
|
const status = deriveStatus(state, streamContent);
|
|
70
84
|
const isChat = hasRealMessages(state.messages);
|
|
71
85
|
const cardW = Math.min(cols - 4, 56);
|
|
72
86
|
const showPopup = input.startsWith('/') && status === 'idle';
|
|
73
|
-
const availLines = Math.max(2, rows -
|
|
87
|
+
const availLines = Math.max(2, rows - topH - DOCK_H - 1);
|
|
74
88
|
|
|
75
89
|
useEffect(() => {
|
|
76
90
|
function onResize() { setDims({ rows: process.stdout.rows || 30, cols: process.stdout.columns || 80 }); }
|
|
@@ -79,9 +93,21 @@ export function App({ config }) {
|
|
|
79
93
|
}, []);
|
|
80
94
|
|
|
81
95
|
useEffect(() => {
|
|
82
|
-
if (state.thinking && !thinkingStart)
|
|
83
|
-
|
|
84
|
-
|
|
96
|
+
if (state.thinking && !thinkingStart) {
|
|
97
|
+
setThinkingStart(Date.now());
|
|
98
|
+
const id = setInterval(() => setThinkingMs(Date.now() - thinkingStart), 100);
|
|
99
|
+
return () => clearInterval(id);
|
|
100
|
+
} else if (!state.thinking) {
|
|
101
|
+
setThinkingStart(null);
|
|
102
|
+
setThinkingMs(0);
|
|
103
|
+
}
|
|
104
|
+
}, [state.thinking, thinkingStart]);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const { reasoning, display } = parseStream(streamContent || '');
|
|
108
|
+
setStreamReasoning(reasoning);
|
|
109
|
+
setStreamDisplay(display);
|
|
110
|
+
}, [streamContent]);
|
|
85
111
|
|
|
86
112
|
const onSubmit = useCallback(async (val) => {
|
|
87
113
|
if (val === '/exit') { process.exit(0); return; }
|
|
@@ -110,14 +136,10 @@ export function App({ config }) {
|
|
|
110
136
|
else onSubmit(t);
|
|
111
137
|
}
|
|
112
138
|
|
|
113
|
-
function handleInputChange(val) {
|
|
114
|
-
setInput(val);
|
|
115
|
-
if (val.startsWith('/')) setPopupIdx(0);
|
|
116
|
-
}
|
|
139
|
+
function handleInputChange(val) { setInput(val); if (val.startsWith('/')) setPopupIdx(0); }
|
|
117
140
|
|
|
118
141
|
function handlePopupSelect(cmd) {
|
|
119
|
-
setInput('');
|
|
120
|
-
setPopupIdx(0);
|
|
142
|
+
setInput(''); setPopupIdx(0);
|
|
121
143
|
if (cmd === '/exit') process.exit(0);
|
|
122
144
|
onCommand(cmd);
|
|
123
145
|
}
|
|
@@ -125,50 +147,56 @@ export function App({ config }) {
|
|
|
125
147
|
function closePopup() { setInput(''); setPopupIdx(0); }
|
|
126
148
|
|
|
127
149
|
useInput((ch, key) => {
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (key.upArrow) { setPopupIdx(i => Math.max(0, i - 1)); return; }
|
|
136
|
-
if (key.downArrow) {
|
|
137
|
-
const filtered = getFiltered(input);
|
|
138
|
-
setPopupIdx(i => Math.min(filtered.length - 1, i + 1));
|
|
150
|
+
if (showPopup) {
|
|
151
|
+
if (key.escape) { closePopup(); return; }
|
|
152
|
+
if (key.return) { const f = getFiltered(input); if (f[popupIdx]) handlePopupSelect(f[popupIdx].name); return; }
|
|
153
|
+
if (key.upArrow) { setPopupIdx(i => Math.max(0, i - 1)); return; }
|
|
154
|
+
if (key.downArrow) { const f = getFiltered(input); setPopupIdx(i => Math.min(f.length - 1, i + 1)); return; }
|
|
139
155
|
}
|
|
156
|
+
if (key.ctrl && key.t) { setShowThinking(p => !p); }
|
|
140
157
|
});
|
|
141
158
|
|
|
142
|
-
const
|
|
159
|
+
const centerArea = h(Box, { flexDirection: 'column', alignItems: 'center', width: '100%' },
|
|
160
|
+
h(TopPanel, { cols, status, thinkingMs }),
|
|
161
|
+
isChat
|
|
162
|
+
? h(Box, { flexGrow: 1, width: cardW, flexDirection: 'column', overflow: 'hidden' },
|
|
163
|
+
h(ChatViewport, {
|
|
164
|
+
messages: state.messages, streamContent, streamDisplay, reasoning: streamReasoning,
|
|
165
|
+
status, showThinking, onToggleThinking: () => setShowThinking(p => !p),
|
|
166
|
+
maxLines: availLines, width: cardW,
|
|
167
|
+
})
|
|
168
|
+
)
|
|
169
|
+
: null,
|
|
143
170
|
showPopup
|
|
144
|
-
? h(Box, { position: 'absolute', bottom: DOCK_H +
|
|
171
|
+
? h(Box, { position: 'absolute', bottom: DOCK_H + 1, alignItems: 'center', width: '100%' },
|
|
145
172
|
h(SlashPopup, { search: input, selectedIdx: popupIdx, width: POPUP_W })
|
|
146
173
|
)
|
|
147
174
|
: null,
|
|
148
|
-
h(
|
|
175
|
+
h(ComposerDock, {
|
|
149
176
|
width: cardW, provider, model, agentMode: state.agentMode, status,
|
|
150
177
|
input, onInputChange: handleInputChange, onSubmit: handleSubmit,
|
|
151
178
|
}),
|
|
152
|
-
h(Footer, { cols })
|
|
153
179
|
);
|
|
154
180
|
|
|
155
181
|
if (!isChat) {
|
|
156
182
|
return h(Box, { width: '100%', height: rows, flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: hex.bg },
|
|
157
|
-
h(Box, { flexDirection: 'column', alignItems: 'center', width:
|
|
158
|
-
h(
|
|
159
|
-
h(
|
|
160
|
-
|
|
161
|
-
|
|
183
|
+
h(Box, { flexDirection: 'column', alignItems: 'center', width: '100%' },
|
|
184
|
+
h(TopPanel, { cols, status, thinkingMs }),
|
|
185
|
+
h(Box, { flexGrow: 1 }),
|
|
186
|
+
showPopup
|
|
187
|
+
? h(Box, { position: 'absolute', bottom: DOCK_H + 1, alignItems: 'center', width: '100%' },
|
|
188
|
+
h(SlashPopup, { search: input, selectedIdx: popupIdx, width: POPUP_W })
|
|
189
|
+
)
|
|
190
|
+
: null,
|
|
191
|
+
h(ComposerDock, {
|
|
192
|
+
width: cardW, provider, model, agentMode: state.agentMode, status,
|
|
193
|
+
input, onInputChange: handleInputChange, onSubmit: handleSubmit,
|
|
194
|
+
}),
|
|
162
195
|
)
|
|
163
196
|
);
|
|
164
197
|
}
|
|
165
198
|
|
|
166
199
|
return h(Box, { width: '100%', height: rows, flexDirection: 'column', alignItems: 'center', backgroundColor: hex.bg },
|
|
167
|
-
|
|
168
|
-
h(StatusBar, { status, thinkingStart }),
|
|
169
|
-
h(Box, { flexGrow: 1, width: cardW, flexDirection: 'column', overflow: 'hidden' },
|
|
170
|
-
h(StreamView, { messages: state.messages, streamContent, status, maxLines: availLines, width: cardW })
|
|
171
|
-
),
|
|
172
|
-
bottomArea
|
|
200
|
+
centerArea
|
|
173
201
|
);
|
|
174
202
|
}
|
|
@@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { hex } from '../config/theme.js';
|
|
4
4
|
import wrapAnsi from 'wrap-ansi';
|
|
5
|
+
import { ThinkingPanel } from './ThinkingPanel.js';
|
|
5
6
|
const { createElement: h } = React;
|
|
6
7
|
|
|
7
8
|
function lineCount(text, width) {
|
|
@@ -13,8 +14,6 @@ function measure(msg, width) {
|
|
|
13
14
|
const cw = Math.max(4, width);
|
|
14
15
|
if (msg.role === 'user') return 1 + lineCount(msg.content, cw);
|
|
15
16
|
if (msg.role === 'assistant') return 1 + lineCount(msg.content, cw) + (msg.duration ? 1 : 0);
|
|
16
|
-
if (msg.role === 'tool') return 1;
|
|
17
|
-
if (msg.role === 'system' || msg.role === 'error') return 1;
|
|
18
17
|
return 1;
|
|
19
18
|
}
|
|
20
19
|
|
|
@@ -72,15 +71,17 @@ function StreamingBlock({ content, width }) {
|
|
|
72
71
|
);
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
export function
|
|
74
|
+
export function ChatViewport({ messages, streamContent, streamDisplay, reasoning, status, showThinking, onToggleThinking, maxLines, width }) {
|
|
76
75
|
const cw = Math.max(4, width - 2);
|
|
77
76
|
|
|
77
|
+
const thinkingH = showThinking && reasoning ? Math.min(10, 3 + wrapAnsi(String(reasoning), cw, { trim: false, hard: true }).split('\n').length) : 0;
|
|
78
|
+
|
|
78
79
|
const visible = useMemo(() => {
|
|
79
80
|
if (messages.length === 0) {
|
|
80
81
|
return [{ id: 'welcome', role: 'system', content: 'CLARITY AI ready \u00B7 /help for commands' }];
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
let avail = maxLines;
|
|
84
|
+
let avail = maxLines - thinkingH;
|
|
84
85
|
const result = [];
|
|
85
86
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
86
87
|
const needed = measure(messages[i], cw);
|
|
@@ -94,8 +95,8 @@ export function StreamView({ messages, streamContent, status, maxLines, width })
|
|
|
94
95
|
result.unshift(messages[i]);
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
if (
|
|
98
|
-
const streamNeeded = 1 + lineCount(
|
|
98
|
+
if (streamDisplay && (status === 'streaming' || status === 'thinking')) {
|
|
99
|
+
const streamNeeded = 1 + lineCount(streamDisplay, cw);
|
|
99
100
|
while (streamNeeded > avail && result.length > 0) {
|
|
100
101
|
avail += measure(result[0], cw);
|
|
101
102
|
result.shift();
|
|
@@ -103,17 +104,18 @@ export function StreamView({ messages, streamContent, status, maxLines, width })
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
return result;
|
|
106
|
-
}, [messages,
|
|
107
|
+
}, [messages, streamDisplay, status, maxLines, thinkingH, cw]);
|
|
107
108
|
|
|
108
|
-
return h(Box, { flexDirection: 'column', width: '100%'
|
|
109
|
+
return h(Box, { flexDirection: 'column', width: '100%' },
|
|
109
110
|
visible.map(m => h(MsgBlock, { key: m.id, msg: m, width: cw })),
|
|
110
|
-
(status === 'streaming' || status === 'thinking') &&
|
|
111
|
-
? h(StreamingBlock, { content:
|
|
111
|
+
(status === 'streaming' || status === 'thinking') && streamDisplay
|
|
112
|
+
? h(StreamingBlock, { content: streamDisplay, width: cw })
|
|
112
113
|
: null,
|
|
113
|
-
status === 'thinking' && !
|
|
114
|
+
status === 'thinking' && !streamDisplay
|
|
114
115
|
? h(Box, { height: 1, backgroundColor: hex.cardBg },
|
|
115
116
|
h(Text, { color: hex.textMuted, backgroundColor: hex.cardBg }, ' \u25CF processing...')
|
|
116
117
|
)
|
|
117
118
|
: null,
|
|
119
|
+
h(ThinkingPanel, { reasoning, visible: showThinking, width, onToggle: onToggleThinking }),
|
|
118
120
|
);
|
|
119
121
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import { hex } from '../config/theme.js';
|
|
5
|
+
const { createElement: h } = React;
|
|
6
|
+
|
|
7
|
+
function borderLine(w, left, mid, right) {
|
|
8
|
+
return left + mid.repeat(Math.max(0, w - 2)) + right;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ComposerDock({ width, provider, model, agentMode, status, input, onInputChange, onSubmit }) {
|
|
12
|
+
const isLocked = status !== 'idle';
|
|
13
|
+
const mShort = model.replace(/^[^/]+\//, '').slice(0, 16);
|
|
14
|
+
const innerW = Math.max(4, width - 4);
|
|
15
|
+
const statusText = (provider || 'groq') + ' \u00B7 ' + mShort + (agentMode ? ' \u00B7 AGENT' : '');
|
|
16
|
+
|
|
17
|
+
return h(Box, { flexDirection: 'column', width, backgroundColor: hex.cardBg },
|
|
18
|
+
h(Box, { height: 1 },
|
|
19
|
+
h(Text, { color: hex.border }, borderLine(width, '\u251C', '\u2500', '\u2524'))
|
|
20
|
+
),
|
|
21
|
+
h(Box, { height: 1, paddingLeft: 2, paddingRight: 2, backgroundColor: hex.cardBg },
|
|
22
|
+
h(TextInput, {
|
|
23
|
+
value: input,
|
|
24
|
+
onChange: onInputChange,
|
|
25
|
+
onSubmit,
|
|
26
|
+
placeholder: 'Ask anything...',
|
|
27
|
+
focus: !isLocked,
|
|
28
|
+
})
|
|
29
|
+
),
|
|
30
|
+
h(Box, { height: 1, paddingLeft: 2, paddingRight: 2, backgroundColor: hex.cardBg },
|
|
31
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.cardBg }, statusText),
|
|
32
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.cardBg },
|
|
33
|
+
' '.repeat(Math.max(0, innerW - statusText.length)) + 'tab agents ctrl+p')
|
|
34
|
+
),
|
|
35
|
+
h(Box, { height: 1 },
|
|
36
|
+
h(Text, { color: hex.border }, borderLine(width, '\u2514', '\u2500', '\u2518'))
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { hex } from '../config/theme.js';
|
|
4
|
+
import wrapAnsi from 'wrap-ansi';
|
|
5
|
+
const { createElement: h } = React;
|
|
6
|
+
|
|
7
|
+
function borderLine(w, left, mid, right) {
|
|
8
|
+
return left + mid.repeat(Math.max(0, w - 2)) + right;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ThinkingPanel({ reasoning, visible, width, onToggle }) {
|
|
12
|
+
if (!visible || !reasoning) return null;
|
|
13
|
+
|
|
14
|
+
const innerW = Math.max(4, width - 4);
|
|
15
|
+
const wrapped = wrapAnsi(String(reasoning), innerW, { trim: false, hard: true });
|
|
16
|
+
const lines = wrapped.split('\n').slice(-8);
|
|
17
|
+
const panelH = Math.min(lines.length + 3, 10);
|
|
18
|
+
|
|
19
|
+
return h(Box, { flexDirection: 'column', width, backgroundColor: hex.thinkingBg },
|
|
20
|
+
h(Box, { height: 1 },
|
|
21
|
+
h(Text, { color: hex.border }, borderLine(width, '\u250C', '\u2500', '\u2510'))
|
|
22
|
+
),
|
|
23
|
+
h(Box, { height: 1, backgroundColor: hex.thinkingBg },
|
|
24
|
+
h(Text, { color: hex.border, backgroundColor: hex.thinkingBg }, '\u2502'),
|
|
25
|
+
h(Text, { color: hex.thinkingText, italic: true, backgroundColor: hex.thinkingBg }, ' \u25BE thinking '),
|
|
26
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.thinkingBg }, '(' + reasoning.length + ' chars)'),
|
|
27
|
+
h(Text, { color: hex.border, backgroundColor: hex.thinkingBg }, ' '.repeat(Math.max(0, innerW - reasoning.length.toString().length - 18)) + '\u2502')
|
|
28
|
+
),
|
|
29
|
+
lines.map((l, i) =>
|
|
30
|
+
h(Box, { key: i, height: 1, backgroundColor: hex.thinkingBg },
|
|
31
|
+
h(Text, { color: hex.border, backgroundColor: hex.thinkingBg }, '\u2502'),
|
|
32
|
+
h(Text, { color: hex.thinkingText, backgroundColor: hex.thinkingBg }, ' ' + l),
|
|
33
|
+
h(Text, { color: hex.border, backgroundColor: hex.thinkingBg }, ' '.repeat(Math.max(0, innerW - l.length)) + '\u2502')
|
|
34
|
+
)
|
|
35
|
+
),
|
|
36
|
+
h(Box, { height: 1 },
|
|
37
|
+
h(Text, { color: hex.border }, borderLine(width, '\u2514', '\u2500', '\u2518'))
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { appGradient, hex } from '../config/theme.js';
|
|
4
|
+
const { createElement: h } = React;
|
|
5
|
+
|
|
6
|
+
const BANNER = [
|
|
7
|
+
'██████ ██ █████ ██████ ██ ████████ ██ ██',
|
|
8
|
+
'██ ██ ██ ██ ██ ██ ██ ██ ██ ██',
|
|
9
|
+
'██ ██ ███████ ██████ ██ ██ ████',
|
|
10
|
+
'██ ██ ██ ██ ██ ██ ██ ██ ██',
|
|
11
|
+
'██████ ███████ ██ ██ ██ ██ ██ ██ ██',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const BANNER_COMPACT = [
|
|
15
|
+
'██████ ██ █████ ██████ ██ ████████ ██ ██',
|
|
16
|
+
'██ ██ ██ ██ ██ ██ ██ ██ ██ ██',
|
|
17
|
+
'██████ ███████ ██ ██ ██ ██ ██ ██ ██',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function StatusChip({ status, thinkingMs }) {
|
|
21
|
+
if (status === 'idle') {
|
|
22
|
+
return h(Box, { backgroundColor: hex.surfaceAlt, paddingX: 1 },
|
|
23
|
+
h(Text, { color: hex.textMuted }, ' IDLE ')
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
if (status === 'thinking') {
|
|
27
|
+
return h(Box, { backgroundColor: hex.surfaceAlt, paddingX: 1 },
|
|
28
|
+
h(Text, { color: hex.orange }, ' \u25CF ' + thinkingMs + 'ms ')
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return h(Box, { backgroundColor: hex.surfaceAlt, paddingX: 1 },
|
|
32
|
+
h(Text, { color: hex.blue }, ' \u25CF STREAMING ')
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function borderLine(w, left, mid, right) {
|
|
37
|
+
return left + mid.repeat(Math.max(0, w - 2)) + right;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function TopPanel({ cols, status, thinkingMs }) {
|
|
41
|
+
const innerW = Math.max(4, cols - 4);
|
|
42
|
+
|
|
43
|
+
if (cols < 50) {
|
|
44
|
+
return h(Box, { flexDirection: 'column', width: cols, backgroundColor: hex.cardBg },
|
|
45
|
+
h(Box, { height: 1 },
|
|
46
|
+
h(Text, { color: hex.border }, borderLine(cols, '\u250C', '\u2500', '\u2510'))
|
|
47
|
+
),
|
|
48
|
+
h(Box, { height: 1, paddingLeft: 1, paddingRight: 1, backgroundColor: hex.cardBg },
|
|
49
|
+
h(Text, { bold: true, backgroundColor: hex.cardBg }, appGradient('CLARITY')),
|
|
50
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.cardBg }, ' '),
|
|
51
|
+
h(StatusChip, { status, thinkingMs }),
|
|
52
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.cardBg }, ' '.repeat(Math.max(0, innerW - 16)))
|
|
53
|
+
),
|
|
54
|
+
h(Box, { height: 1 },
|
|
55
|
+
h(Text, { color: hex.border }, borderLine(cols, '\u2514', '\u2500', '\u2518'))
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const lines = cols < 60 ? BANNER_COMPACT : BANNER;
|
|
61
|
+
const panelH = lines.length + 2;
|
|
62
|
+
|
|
63
|
+
return h(Box, { flexDirection: 'column', width: cols, backgroundColor: hex.cardBg },
|
|
64
|
+
h(Box, { height: 1 },
|
|
65
|
+
h(Text, { color: hex.border }, borderLine(cols, '\u250C', '\u2500', '\u2510'))
|
|
66
|
+
),
|
|
67
|
+
lines.map((line, i) =>
|
|
68
|
+
h(Box, { key: i, height: 1, backgroundColor: hex.cardBg },
|
|
69
|
+
h(Text, { color: hex.border, backgroundColor: hex.cardBg }, '\u2502'),
|
|
70
|
+
h(Text, { bold: true, backgroundColor: hex.cardBg }, appGradient.multiline(line)),
|
|
71
|
+
h(Text, { color: hex.border, backgroundColor: hex.cardBg },
|
|
72
|
+
' '.repeat(Math.max(0, cols - line.length - 2)) + '\u2502')
|
|
73
|
+
)
|
|
74
|
+
),
|
|
75
|
+
h(Box, { height: 1, backgroundColor: hex.cardBg },
|
|
76
|
+
h(Text, { color: hex.border, backgroundColor: hex.cardBg }, '\u2502'),
|
|
77
|
+
h(StatusChip, { status, thinkingMs }),
|
|
78
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.cardBg },
|
|
79
|
+
' tab agents ctrl+p commands' + ' '.repeat(Math.max(0, innerW - 46)) + '\u2502')
|
|
80
|
+
),
|
|
81
|
+
h(Box, { height: 1 },
|
|
82
|
+
h(Text, { color: hex.border }, borderLine(cols, '\u251C', '\u2500', '\u2524'))
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
}
|
package/src/config/theme.js
CHANGED
package/src/components/Banner.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
import { appGradient } from '../config/theme.js';
|
|
4
|
-
const { createElement: h } = React;
|
|
5
|
-
|
|
6
|
-
const BANNER = [
|
|
7
|
-
'██████ ██ █████ ██████ ██ ████████ ██ ██',
|
|
8
|
-
'██ ██ ██ ██ ██ ██ ██ ██ ██ ██',
|
|
9
|
-
'██ ██ ███████ ██████ ██ ██ ████',
|
|
10
|
-
'██ ██ ██ ██ ██ ██ ██ ██ ██',
|
|
11
|
-
'██████ ███████ ██ ██ ██ ██ ██ ██ ██',
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
const BANNER_COMPACT = [
|
|
15
|
-
'██████ ██ █████ ██████ ██ ████████ ██ ██',
|
|
16
|
-
'██ ██ ██ ██ ██ ██ ██ ██ ██ ██',
|
|
17
|
-
'██████ ███████ ██ ██ ██ ██ ██ ██ ██',
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
export function Banner({ cols }) {
|
|
21
|
-
if (cols < 50) {
|
|
22
|
-
return h(Box, { height: 2, width: '100%', alignItems: 'center', justifyContent: 'center' },
|
|
23
|
-
h(Text, { bold: true }, appGradient('CLARITY'))
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const lines = cols < 60 ? BANNER_COMPACT : BANNER;
|
|
28
|
-
|
|
29
|
-
return h(Box, { height: lines.length, width: '100%', alignItems: 'center', justifyContent: 'center', flexDirection: 'column' },
|
|
30
|
-
lines.map((line, i) =>
|
|
31
|
-
h(Box, { key: i, height: 1 },
|
|
32
|
-
h(Text, { bold: true }, appGradient.multiline(line))
|
|
33
|
-
)
|
|
34
|
-
)
|
|
35
|
-
);
|
|
36
|
-
}
|
package/src/components/Footer.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
import { hex } from '../config/theme.js';
|
|
4
|
-
const { createElement: h } = React;
|
|
5
|
-
|
|
6
|
-
export function Footer({ cols }) {
|
|
7
|
-
const showFull = cols >= 58;
|
|
8
|
-
const keybindText = 'tab agents ctrl+p commands';
|
|
9
|
-
|
|
10
|
-
return h(Box, { width: '100%', flexDirection: 'column', backgroundColor: hex.bg },
|
|
11
|
-
h(Box, { height: 1, justifyContent: 'flex-end', paddingRight: 2, backgroundColor: hex.bg },
|
|
12
|
-
h(Text, { color: hex.textMuted }, keybindText)
|
|
13
|
-
),
|
|
14
|
-
h(Box, { height: 1, paddingLeft: 2, paddingRight: 2, backgroundColor: hex.bg },
|
|
15
|
-
h(Text, { color: hex.textDim },
|
|
16
|
-
'\u2580 ',
|
|
17
|
-
h(Text, { color: hex.orange }, 'Tip'),
|
|
18
|
-
h(Text, { color: hex.textDim }, ': ' + (showFull
|
|
19
|
-
? 'Use /help to view system commands and switch active models'
|
|
20
|
-
: 'Use /help for commands'))
|
|
21
|
-
)
|
|
22
|
-
)
|
|
23
|
-
);
|
|
24
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
import TextInput from 'ink-text-input';
|
|
4
|
-
import { hex } from '../config/theme.js';
|
|
5
|
-
const { createElement: h } = React;
|
|
6
|
-
|
|
7
|
-
export function InputDock({ width, provider, model, agentMode, status, input, onInputChange, onSubmit }) {
|
|
8
|
-
const isLocked = status !== 'idle';
|
|
9
|
-
const mShort = model.replace(/^[^/]+\//, '').slice(0, 18);
|
|
10
|
-
|
|
11
|
-
return h(Box, { width, flexDirection: 'column', backgroundColor: hex.cardBg },
|
|
12
|
-
h(Box, { height: 1, backgroundColor: hex.orange }),
|
|
13
|
-
h(Box, { height: 1, paddingLeft: 2, paddingRight: 2, backgroundColor: hex.cardBg },
|
|
14
|
-
h(TextInput, {
|
|
15
|
-
value: input,
|
|
16
|
-
onChange: onInputChange,
|
|
17
|
-
onSubmit,
|
|
18
|
-
placeholder: 'Ask anything...',
|
|
19
|
-
focus: !isLocked,
|
|
20
|
-
})
|
|
21
|
-
),
|
|
22
|
-
h(Box, { height: 1, paddingLeft: 2, paddingRight: 2, backgroundColor: hex.cardBg },
|
|
23
|
-
h(Text, { color: hex.textMuted },
|
|
24
|
-
(provider || 'groq') + ' \u00B7 ' + mShort + (agentMode ? ' \u00B7 AGENT' : '')
|
|
25
|
-
)
|
|
26
|
-
),
|
|
27
|
-
h(Box, { height: 1, backgroundColor: hex.cardBg }),
|
|
28
|
-
);
|
|
29
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
import { hex } from '../config/theme.js';
|
|
4
|
-
const { createElement: h } = React;
|
|
5
|
-
|
|
6
|
-
export function StatusBar({ status, thinkingStart }) {
|
|
7
|
-
const [elapsed, setElapsed] = useState(0);
|
|
8
|
-
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
if (status === 'idle') { setElapsed(0); return; }
|
|
11
|
-
const id = setInterval(() => {
|
|
12
|
-
setElapsed(thinkingStart ? Date.now() - thinkingStart : 0);
|
|
13
|
-
}, 100);
|
|
14
|
-
return () => clearInterval(id);
|
|
15
|
-
}, [status, thinkingStart]);
|
|
16
|
-
|
|
17
|
-
const isThinking = status === 'thinking';
|
|
18
|
-
const isStreaming = status === 'streaming';
|
|
19
|
-
|
|
20
|
-
if (status === 'idle') {
|
|
21
|
-
return h(Box, { height: 1, width: '100%', alignItems: 'center', justifyContent: 'center' },
|
|
22
|
-
h(Box, { backgroundColor: hex.surfaceAlt, paddingX: 1 },
|
|
23
|
-
h(Text, { color: hex.textMuted }, ' IDLE ')
|
|
24
|
-
)
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (isThinking) {
|
|
29
|
-
return h(Box, { height: 1, width: '100%', alignItems: 'center', justifyContent: 'center' },
|
|
30
|
-
h(Box, { backgroundColor: hex.surfaceAlt, paddingX: 1 },
|
|
31
|
-
h(Text, { color: hex.orange }, ' \u25CF THINKING ' + elapsed + 'ms ')
|
|
32
|
-
)
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (isStreaming) {
|
|
37
|
-
return h(Box, { height: 1, width: '100%', alignItems: 'center', justifyContent: 'center' },
|
|
38
|
-
h(Box, { backgroundColor: hex.surfaceAlt, paddingX: 1 },
|
|
39
|
-
h(Text, { color: hex.blue }, ' \u25CF STREAMING ' + elapsed + 'ms ')
|
|
40
|
-
)
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return null;
|
|
45
|
-
}
|