clarity-ai 7.2.0 → 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 +21 -0
- package/bin/clarity.js +3 -3
- package/package.json +1 -1
- package/src/app.js +89 -71
- package/src/components/{StreamView.js → ChatViewport.js} +31 -28
- package/src/components/ComposerDock.js +39 -0
- package/src/components/SlashPopup.js +13 -11
- 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 -41
- package/src/components/Footer.js +0 -24
- package/src/components/InputDock.js +0 -29
- package/src/components/StatusBar.js +0 -45
- package/src/hooks/useMouse.js +0 -45
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
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
|
+
|
|
18
|
+
## 7.2.1 (2026-06-06)
|
|
19
|
+
|
|
20
|
+
### Critical hotfix: mouse bleed elimination, CLARITY branding, TrueColor textures
|
|
21
|
+
- **FIXED input corruption**: Removed XTerm mouse tracking (`\x1b[?1000h`) entirely. Raw `\x1b[<0;54;41M` sequences no longer leak into the chat text buffer. Termux soft keyboard now works reliably without raw-mode conflicts.
|
|
22
|
+
- **FIXED ASCII logo**: Replaced the figlet banner that rendered as "GLARITY" with a clean 5-line solid block matrix (`██████`) that unambiguously spells CLARITY. Three-tier sizing (5-line / 3-line compact / 1-line gradient).
|
|
23
|
+
- **TrueColor panel textures**: Every message block, streaming pane, and popup container now uses explicit `#1C1C1C` graphite backgrounds. No naked terminal defaults. Orange `#FF9F43` full-width selection bar with `#000000` black text on all active popup rows.
|
|
24
|
+
- **Stderr isolation maintained**: `console.log/error/warn` continue routing to `clarity-debug.log` to prevent stray backend traces from corrupting the UI layer.
|
|
25
|
+
|
|
5
26
|
## 7.2.0 (2026-06-06)
|
|
6
27
|
|
|
7
28
|
### Sticky floating overlay engine — touch selection, phase-driven layout, virtualized log
|
package/bin/clarity.js
CHANGED
|
@@ -57,14 +57,14 @@ async function main() {
|
|
|
57
57
|
logFile.write('[CLEANUP] CLARITY exiting\n');
|
|
58
58
|
logFile.end();
|
|
59
59
|
try { clear(); } catch {}
|
|
60
|
-
process.stdout.write('\x1b[?
|
|
60
|
+
process.stdout.write('\x1b[?25h\x1b[0m');
|
|
61
61
|
process.exit(0);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
process.on('SIGINT', () => cleanup());
|
|
65
65
|
process.on('SIGTERM', () => cleanup());
|
|
66
66
|
process.on('exit', () => {
|
|
67
|
-
process.stdout.write('\x1b[?
|
|
67
|
+
process.stdout.write('\x1b[?25h\x1b[0m');
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
await new Promise(() => {});
|
|
@@ -76,7 +76,7 @@ main().catch(err => {
|
|
|
76
76
|
console.warn = originalWarn;
|
|
77
77
|
logFile.write('[FATAL] ' + (err?.message || String(err)) + '\n');
|
|
78
78
|
logFile.end();
|
|
79
|
-
process.stdout.write('\x1b[?
|
|
79
|
+
process.stdout.write('\x1b[?25h\x1b[0m');
|
|
80
80
|
console.error('\n\x1b[31mFatal error:\x1b[0m', err?.message || err);
|
|
81
81
|
process.exit(1);
|
|
82
82
|
});
|
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -2,17 +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
|
-
import { useMouse } from './hooks/useMouse.js';
|
|
12
9
|
const { createElement: h } = React;
|
|
13
10
|
|
|
14
11
|
let abortController = null;
|
|
15
|
-
|
|
16
12
|
export function getAbortController() { return abortController; }
|
|
17
13
|
export function setAbortController(ac) { abortController = ac; }
|
|
18
14
|
export function cancelStream() {
|
|
@@ -20,10 +16,15 @@ export function cancelStream() {
|
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
const DOCK_H = 4;
|
|
23
|
-
const
|
|
24
|
-
const STATUS_H = 1;
|
|
19
|
+
const TOP_H_PRE = 2;
|
|
25
20
|
const POPUP_W = 44;
|
|
26
21
|
|
|
22
|
+
const COMMAND_ITEMS = [
|
|
23
|
+
{ name: '/keys' }, { name: '/model' }, { name: '/provider' },
|
|
24
|
+
{ name: '/agent' }, { name: '/stop' }, { name: '/clear' },
|
|
25
|
+
{ name: '/export' }, { name: '/help' }, { name: '/exit' },
|
|
26
|
+
];
|
|
27
|
+
|
|
27
28
|
function deriveStatus(state, sc) {
|
|
28
29
|
if (!state.thinking) return 'idle';
|
|
29
30
|
return sc ? 'streaming' : 'thinking';
|
|
@@ -33,14 +34,37 @@ function hasRealMessages(msgs) {
|
|
|
33
34
|
return msgs.some(m => m.role === 'user' || m.role === 'assistant');
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
function getFiltered(input) {
|
|
38
|
+
const q = input.replace(/^\//, '').toLowerCase();
|
|
39
|
+
if (!q) return COMMAND_ITEMS;
|
|
40
|
+
return COMMAND_ITEMS.filter(c => c.name.includes(q));
|
|
41
|
+
}
|
|
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
|
+
|
|
36
56
|
export function App({ config }) {
|
|
37
57
|
const { stdout } = useStdout();
|
|
38
58
|
const [dims, setDims] = useState({ rows: stdout.rows || 30, cols: stdout.columns || 80 });
|
|
39
59
|
const [state, setState] = useState(() => createChatState());
|
|
40
60
|
const [streamContent, setStreamContent] = useState('');
|
|
61
|
+
const [streamReasoning, setStreamReasoning] = useState('');
|
|
62
|
+
const [streamDisplay, setStreamDisplay] = useState('');
|
|
41
63
|
const [thinkingStart, setThinkingStart] = useState(null);
|
|
64
|
+
const [thinkingMs, setThinkingMs] = useState(0);
|
|
42
65
|
const [input, setInput] = useState('');
|
|
43
66
|
const [popupIdx, setPopupIdx] = useState(0);
|
|
67
|
+
const [showThinking, setShowThinking] = useState(true);
|
|
44
68
|
const defaultModel = (config.model || '').replace(/^[^/]+\//, '') || 'llama-3.3-70b-versatile';
|
|
45
69
|
const [model, setModel] = useState(defaultModel);
|
|
46
70
|
const [provider, setProvider] = useState(config.provider || 'groq');
|
|
@@ -54,23 +78,36 @@ export function App({ config }) {
|
|
|
54
78
|
|
|
55
79
|
const rows = dims.rows;
|
|
56
80
|
const cols = dims.cols;
|
|
57
|
-
const bannerH = cols < 50 ? 2 :
|
|
81
|
+
const bannerH = cols < 50 ? 2 : (cols < 60 ? 3 : 5);
|
|
82
|
+
const topH = bannerH + TOP_H_PRE;
|
|
58
83
|
const status = deriveStatus(state, streamContent);
|
|
59
84
|
const isChat = hasRealMessages(state.messages);
|
|
60
85
|
const cardW = Math.min(cols - 4, 56);
|
|
61
86
|
const showPopup = input.startsWith('/') && status === 'idle';
|
|
62
|
-
const availLines = Math.max(2, rows -
|
|
87
|
+
const availLines = Math.max(2, rows - topH - DOCK_H - 1);
|
|
63
88
|
|
|
64
89
|
useEffect(() => {
|
|
65
|
-
function onResize() { setDims({
|
|
90
|
+
function onResize() { setDims({ rows: process.stdout.rows || 30, cols: process.stdout.columns || 80 }); }
|
|
66
91
|
process.stdout.on('resize', onResize);
|
|
67
92
|
return () => process.stdout.removeListener('resize', onResize);
|
|
68
93
|
}, []);
|
|
69
94
|
|
|
70
95
|
useEffect(() => {
|
|
71
|
-
if (state.thinking && !thinkingStart)
|
|
72
|
-
|
|
73
|
-
|
|
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]);
|
|
74
111
|
|
|
75
112
|
const onSubmit = useCallback(async (val) => {
|
|
76
113
|
if (val === '/exit') { process.exit(0); return; }
|
|
@@ -85,8 +122,8 @@ export function App({ config }) {
|
|
|
85
122
|
}, []);
|
|
86
123
|
|
|
87
124
|
const onCommand = useCallback(async (val) => {
|
|
88
|
-
if (val
|
|
89
|
-
if (val
|
|
125
|
+
if (val === '/stop') { cancelStream(); return; }
|
|
126
|
+
if (val === '/exit') { process.exit(0); return; }
|
|
90
127
|
await handleCommand(val, stateRef.current, setState, setModel, setProvider, modelRef.current, providerRef.current);
|
|
91
128
|
}, []);
|
|
92
129
|
|
|
@@ -99,86 +136,67 @@ export function App({ config }) {
|
|
|
99
136
|
else onSubmit(t);
|
|
100
137
|
}
|
|
101
138
|
|
|
102
|
-
function handleInputChange(val) {
|
|
103
|
-
setInput(val);
|
|
104
|
-
if (val.startsWith('/')) setPopupIdx(0);
|
|
105
|
-
}
|
|
139
|
+
function handleInputChange(val) { setInput(val); if (val.startsWith('/')) setPopupIdx(0); }
|
|
106
140
|
|
|
107
141
|
function handlePopupSelect(cmd) {
|
|
108
|
-
setInput('');
|
|
109
|
-
setPopupIdx(0);
|
|
142
|
+
setInput(''); setPopupIdx(0);
|
|
110
143
|
if (cmd === '/exit') process.exit(0);
|
|
111
|
-
if (cmd === '/stop') { cancelStream(); return; }
|
|
112
144
|
onCommand(cmd);
|
|
113
145
|
}
|
|
114
146
|
|
|
115
147
|
function closePopup() { setInput(''); setPopupIdx(0); }
|
|
116
148
|
|
|
117
149
|
useInput((ch, key) => {
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
const filtered = q ? COMMANDS.filter(c => c.name.includes(q)) : COMMANDS;
|
|
124
|
-
if (filtered[popupIdx]) handlePopupSelect(filtered[popupIdx].name);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
if (key.upArrow) { setPopupIdx(i => Math.max(0, i - 1)); return; }
|
|
128
|
-
if (key.downArrow) {
|
|
129
|
-
const COMMANDS = [{ name: '/keys' }, { name: '/model' }, { name: '/provider' }, { name: '/agent' }, { name: '/stop' }, { name: '/clear' }, { name: '/export' }, { name: '/help' }, { name: '/exit' }];
|
|
130
|
-
const q = input.replace(/^\//, '').toLowerCase();
|
|
131
|
-
const filtered = q ? COMMANDS.filter(c => c.name.includes(q)) : COMMANDS;
|
|
132
|
-
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; }
|
|
133
155
|
}
|
|
156
|
+
if (key.ctrl && key.t) { setShowThinking(p => !p); }
|
|
134
157
|
});
|
|
135
158
|
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
}, [showPopup, cols, rows]);
|
|
149
|
-
|
|
150
|
-
useMouse(handleClick);
|
|
151
|
-
|
|
152
|
-
const bottomArea = h(Box, { flexDirection: 'column', alignItems: 'center', width: '100%' },
|
|
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,
|
|
153
170
|
showPopup
|
|
154
|
-
? h(Box, { position: 'absolute', bottom: DOCK_H +
|
|
155
|
-
h(SlashPopup, { search: input, selectedIdx: popupIdx,
|
|
171
|
+
? h(Box, { position: 'absolute', bottom: DOCK_H + 1, alignItems: 'center', width: '100%' },
|
|
172
|
+
h(SlashPopup, { search: input, selectedIdx: popupIdx, width: POPUP_W })
|
|
156
173
|
)
|
|
157
174
|
: null,
|
|
158
|
-
h(
|
|
175
|
+
h(ComposerDock, {
|
|
159
176
|
width: cardW, provider, model, agentMode: state.agentMode, status,
|
|
160
177
|
input, onInputChange: handleInputChange, onSubmit: handleSubmit,
|
|
161
178
|
}),
|
|
162
|
-
h(Footer, { cols })
|
|
163
179
|
);
|
|
164
180
|
|
|
165
181
|
if (!isChat) {
|
|
166
182
|
return h(Box, { width: '100%', height: rows, flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: hex.bg },
|
|
167
|
-
h(Box, { flexDirection: 'column', alignItems: 'center', width:
|
|
168
|
-
h(
|
|
169
|
-
h(
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
}),
|
|
172
195
|
)
|
|
173
196
|
);
|
|
174
197
|
}
|
|
175
198
|
|
|
176
199
|
return h(Box, { width: '100%', height: rows, flexDirection: 'column', alignItems: 'center', backgroundColor: hex.bg },
|
|
177
|
-
|
|
178
|
-
h(StatusBar, { status, thinkingStart }),
|
|
179
|
-
h(Box, { flexGrow: 1, width: cardW, flexDirection: 'column', overflow: 'hidden' },
|
|
180
|
-
h(StreamView, { messages: state.messages, streamContent, status, maxLines: availLines, width: cardW })
|
|
181
|
-
),
|
|
182
|
-
bottomArea
|
|
200
|
+
centerArea
|
|
183
201
|
);
|
|
184
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
|
|
|
@@ -23,7 +22,7 @@ function wrap(text, width) {
|
|
|
23
22
|
return wrapAnsi(String(text), Math.max(4, width), { trim: false, hard: true }).split('\n');
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
function
|
|
25
|
+
function RoleIcon({ role }) {
|
|
27
26
|
switch (role) {
|
|
28
27
|
case 'user': return h(Text, { color: hex.orange, bold: true }, '\u276F ');
|
|
29
28
|
case 'assistant': return h(Text, { color: hex.purple, bold: true }, '\u25C6 ');
|
|
@@ -36,20 +35,21 @@ function MsgRole({ role }) {
|
|
|
36
35
|
|
|
37
36
|
function MsgBlock({ msg, width }) {
|
|
38
37
|
const lines = useMemo(() => wrap(msg.content, width), [msg.content, width]);
|
|
38
|
+
const bg = msg.role === 'user' ? hex.surface : hex.cardBg;
|
|
39
39
|
|
|
40
|
-
return h(Box, { flexDirection: 'column' },
|
|
41
|
-
h(Box, { height: 1 },
|
|
42
|
-
h(
|
|
43
|
-
h(Text, { color: hex.text }, lines[0] || '')
|
|
40
|
+
return h(Box, { flexDirection: 'column', backgroundColor: bg },
|
|
41
|
+
h(Box, { height: 1, backgroundColor: bg },
|
|
42
|
+
h(RoleIcon, { role: msg.role }),
|
|
43
|
+
h(Text, { color: hex.text, backgroundColor: bg }, lines[0] || '')
|
|
44
44
|
),
|
|
45
45
|
lines.slice(1).map((l, i) =>
|
|
46
|
-
h(Box, { key: i, height: 1 },
|
|
47
|
-
h(Text, { color: hex.text }, ' ' + l)
|
|
46
|
+
h(Box, { key: i, height: 1, backgroundColor: bg },
|
|
47
|
+
h(Text, { color: hex.text, backgroundColor: bg }, ' ' + l)
|
|
48
48
|
)
|
|
49
49
|
),
|
|
50
50
|
msg.role === 'assistant' && msg.duration
|
|
51
|
-
? h(Box, { height: 1 },
|
|
52
|
-
h(Text, { color: hex.textMuted },
|
|
51
|
+
? h(Box, { height: 1, backgroundColor: bg },
|
|
52
|
+
h(Text, { color: hex.textMuted, backgroundColor: bg },
|
|
53
53
|
' ' + (msg.duration < 1000 ? msg.duration + 'ms' : (msg.duration / 1000).toFixed(1) + 's'))
|
|
54
54
|
)
|
|
55
55
|
: null
|
|
@@ -58,28 +58,30 @@ function MsgBlock({ msg, width }) {
|
|
|
58
58
|
|
|
59
59
|
function StreamingBlock({ content, width }) {
|
|
60
60
|
const lines = useMemo(() => wrap(content, width), [content, width]);
|
|
61
|
-
return h(Box, { flexDirection: 'column' },
|
|
62
|
-
h(Box, { height: 1 },
|
|
63
|
-
h(Text, { color: hex.blue, bold: true }, '\u25CF '),
|
|
64
|
-
h(Text, { color: hex.text }, lines[0] || '')
|
|
61
|
+
return h(Box, { flexDirection: 'column', backgroundColor: hex.cardBg },
|
|
62
|
+
h(Box, { height: 1, backgroundColor: hex.cardBg },
|
|
63
|
+
h(Text, { color: hex.blue, bold: true, backgroundColor: hex.cardBg }, '\u25CF '),
|
|
64
|
+
h(Text, { color: hex.text, backgroundColor: hex.cardBg }, lines[0] || '')
|
|
65
65
|
),
|
|
66
66
|
lines.slice(1).map((l, i) =>
|
|
67
|
-
h(Box, { key: i, height: 1 },
|
|
68
|
-
h(Text, { color: hex.text }, ' ' + l)
|
|
67
|
+
h(Box, { key: i, height: 1, backgroundColor: hex.cardBg },
|
|
68
|
+
h(Text, { color: hex.text, backgroundColor: hex.cardBg }, ' ' + l)
|
|
69
69
|
)
|
|
70
70
|
)
|
|
71
71
|
);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
export function
|
|
74
|
+
export function ChatViewport({ messages, streamContent, streamDisplay, reasoning, status, showThinking, onToggleThinking, maxLines, width }) {
|
|
75
75
|
const cw = Math.max(4, width - 2);
|
|
76
76
|
|
|
77
|
+
const thinkingH = showThinking && reasoning ? Math.min(10, 3 + wrapAnsi(String(reasoning), cw, { trim: false, hard: true }).split('\n').length) : 0;
|
|
78
|
+
|
|
77
79
|
const visible = useMemo(() => {
|
|
78
80
|
if (messages.length === 0) {
|
|
79
81
|
return [{ id: 'welcome', role: 'system', content: 'CLARITY AI ready \u00B7 /help for commands' }];
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
let avail = maxLines;
|
|
84
|
+
let avail = maxLines - thinkingH;
|
|
83
85
|
const result = [];
|
|
84
86
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
85
87
|
const needed = measure(messages[i], cw);
|
|
@@ -93,8 +95,8 @@ export function StreamView({ messages, streamContent, status, maxLines, width })
|
|
|
93
95
|
result.unshift(messages[i]);
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
if (
|
|
97
|
-
const streamNeeded = 1 + lineCount(
|
|
98
|
+
if (streamDisplay && (status === 'streaming' || status === 'thinking')) {
|
|
99
|
+
const streamNeeded = 1 + lineCount(streamDisplay, cw);
|
|
98
100
|
while (streamNeeded > avail && result.length > 0) {
|
|
99
101
|
avail += measure(result[0], cw);
|
|
100
102
|
result.shift();
|
|
@@ -102,17 +104,18 @@ export function StreamView({ messages, streamContent, status, maxLines, width })
|
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
return result;
|
|
105
|
-
}, [messages,
|
|
107
|
+
}, [messages, streamDisplay, status, maxLines, thinkingH, cw]);
|
|
106
108
|
|
|
107
|
-
return h(Box, { flexDirection: 'column', width: '100%'
|
|
109
|
+
return h(Box, { flexDirection: 'column', width: '100%' },
|
|
108
110
|
visible.map(m => h(MsgBlock, { key: m.id, msg: m, width: cw })),
|
|
109
|
-
(status === 'streaming' || status === 'thinking') &&
|
|
110
|
-
? h(StreamingBlock, { content:
|
|
111
|
+
(status === 'streaming' || status === 'thinking') && streamDisplay
|
|
112
|
+
? h(StreamingBlock, { content: streamDisplay, width: cw })
|
|
111
113
|
: null,
|
|
112
|
-
status === 'thinking' && !
|
|
113
|
-
? h(Box, { height: 1 },
|
|
114
|
-
h(Text, { color: hex.textMuted }, ' \u25CF processing...')
|
|
114
|
+
status === 'thinking' && !streamDisplay
|
|
115
|
+
? h(Box, { height: 1, backgroundColor: hex.cardBg },
|
|
116
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.cardBg }, ' \u25CF processing...')
|
|
115
117
|
)
|
|
116
118
|
: null,
|
|
119
|
+
h(ThinkingPanel, { reasoning, visible: showThinking, width, onToggle: onToggleThinking }),
|
|
117
120
|
);
|
|
118
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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { hex } from '../config/theme.js';
|
|
4
4
|
const { createElement: h } = React;
|
|
@@ -15,7 +15,7 @@ const COMMANDS = [
|
|
|
15
15
|
{ name: '/exit', desc: 'Exit CLARITY' },
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
-
export function SlashPopup({ search, selectedIdx,
|
|
18
|
+
export function SlashPopup({ search, selectedIdx, width }) {
|
|
19
19
|
const filtered = useMemo(() => {
|
|
20
20
|
const q = search.replace(/^\//, '').toLowerCase();
|
|
21
21
|
if (!q) return COMMANDS;
|
|
@@ -25,31 +25,33 @@ export function SlashPopup({ search, selectedIdx, onHover, width }) {
|
|
|
25
25
|
}, [search]);
|
|
26
26
|
|
|
27
27
|
const innerW = Math.max(10, width - 4);
|
|
28
|
-
const
|
|
28
|
+
const label = (cmd, i) => {
|
|
29
29
|
const sel = i === selectedIdx;
|
|
30
|
-
const
|
|
31
|
-
return
|
|
30
|
+
const text = ' ' + cmd.name + ' ' + cmd.desc;
|
|
31
|
+
return text.length > innerW ? text.slice(0, innerW - 2) + '\u2026' : text;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
return h(Box, { flexDirection: 'column', width, backgroundColor: hex.
|
|
34
|
+
return h(Box, { flexDirection: 'column', width, backgroundColor: hex.cardBg },
|
|
35
35
|
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
36
|
-
h(Text, { color: hex.textMuted
|
|
36
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
37
|
+
' ' + (search || '/') + ' '.repeat(Math.max(0, innerW - (search || '/').length)))
|
|
37
38
|
),
|
|
38
39
|
filtered.map((cmd, i) =>
|
|
39
40
|
h(Box, {
|
|
40
41
|
key: cmd.name,
|
|
41
42
|
height: 1,
|
|
42
|
-
backgroundColor: i === selectedIdx ? hex.orange :
|
|
43
|
+
backgroundColor: i === selectedIdx ? hex.orange : hex.cardBg,
|
|
43
44
|
},
|
|
44
45
|
h(Text, {
|
|
45
46
|
color: i === selectedIdx ? '#000000' : hex.text,
|
|
46
47
|
bold: i === selectedIdx,
|
|
47
|
-
|
|
48
|
+
backgroundColor: i === selectedIdx ? hex.orange : hex.cardBg,
|
|
49
|
+
}, label(cmd, i))
|
|
48
50
|
)
|
|
49
51
|
),
|
|
50
52
|
filtered.length === 0
|
|
51
|
-
? h(Box, { height: 1 },
|
|
52
|
-
h(Text, { color: hex.textMuted }, ' No matching commands'))
|
|
53
|
+
? h(Box, { height: 1, backgroundColor: hex.cardBg },
|
|
54
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.cardBg }, ' No matching commands'))
|
|
53
55
|
: null,
|
|
54
56
|
);
|
|
55
57
|
}
|
|
@@ -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,41 +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_FULL = [
|
|
7
|
-
' ██████╗██╗ █████╗ ██████╗ ██╗████████╗██╗ ██╗',
|
|
8
|
-
'██╔════╝██║ ██╔══██╗██╔══██╗██║╚══██╔══╝╚██╗ ██╔╝',
|
|
9
|
-
'██║ ███╗██║ ███████║██████╔╝██║ ██║ ╚████╔╝ ',
|
|
10
|
-
'██║ ██║██║ ██╔══██║██╔══██╗██║ ██║ ╚██╔╝ ',
|
|
11
|
-
'╚██████╔╝███████╗██║ ██║██║ ██║██║ ██║ ██║ ',
|
|
12
|
-
' ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ',
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
const BANNER_MED = [
|
|
16
|
-
'██████╗ ██╗ █████╗ ██████╗ ██╗████████╗██╗ ██╗',
|
|
17
|
-
'██╔════╝ ██║ ██╔══██╗██╔══██╗██║╚══██╔══╝╚██╗ ██╔╝',
|
|
18
|
-
'███████╗ ██║ ███████║██████╔╝██║ ██║ ╚████╔╝ ',
|
|
19
|
-
'╚════██║ ██║ ██╔══██║██╔══██╗██║ ██║ ╚██╔╝ ',
|
|
20
|
-
'██████╔╝ ███████╗██║ ██║██║ ██║██║ ██║ ██║ ',
|
|
21
|
-
'╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ',
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
export function Banner({ cols }) {
|
|
25
|
-
if (cols < 50) {
|
|
26
|
-
return h(Box, { height: 2, width: '100%', alignItems: 'center', justifyContent: 'center' },
|
|
27
|
-
h(Text, { bold: true }, appGradient('CLARITY'))
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const lines = cols < 60 ? BANNER_MED : BANNER_FULL;
|
|
32
|
-
const bannerHeight = lines.length;
|
|
33
|
-
|
|
34
|
-
return h(Box, { height: bannerHeight, width: '100%', alignItems: 'center', justifyContent: 'center', flexDirection: 'column' },
|
|
35
|
-
lines.map((line, i) =>
|
|
36
|
-
h(Box, { key: i, height: 1 },
|
|
37
|
-
h(Text, { bold: true }, appGradient.multiline(line))
|
|
38
|
-
)
|
|
39
|
-
)
|
|
40
|
-
);
|
|
41
|
-
}
|
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
|
-
}
|
package/src/hooks/useMouse.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react';
|
|
2
|
-
import { useStdin } from 'ink';
|
|
3
|
-
|
|
4
|
-
export function useMouse(handler) {
|
|
5
|
-
const { stdin } = useStdin();
|
|
6
|
-
const handlerRef = useRef(handler);
|
|
7
|
-
handlerRef.current = handler;
|
|
8
|
-
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
process.stdout.write('\x1b[?1000h\x1b[?1006h');
|
|
11
|
-
|
|
12
|
-
function onData(chunk) {
|
|
13
|
-
const str = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
|
|
14
|
-
|
|
15
|
-
const sgr = str.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
|
|
16
|
-
if (sgr) {
|
|
17
|
-
const code = parseInt(sgr[1]);
|
|
18
|
-
const col = parseInt(sgr[2]);
|
|
19
|
-
const row = parseInt(sgr[3]);
|
|
20
|
-
const press = sgr[4] === 'M';
|
|
21
|
-
if (press && handlerRef.current) {
|
|
22
|
-
handlerRef.current({ col, row, button: code & 3 });
|
|
23
|
-
}
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const legacy = str.match(/\x1b\[M(.{3})/);
|
|
28
|
-
if (legacy) {
|
|
29
|
-
const chars = legacy[1];
|
|
30
|
-
const cb = chars.charCodeAt(0) - 32;
|
|
31
|
-
const col = chars.charCodeAt(1) - 32;
|
|
32
|
-
const row = chars.charCodeAt(2) - 32;
|
|
33
|
-
if (handlerRef.current) {
|
|
34
|
-
handlerRef.current({ col, row, button: cb & 3 });
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
stdin.on('data', onData);
|
|
40
|
-
return () => {
|
|
41
|
-
stdin.removeListener('data', onData);
|
|
42
|
-
process.stdout.write('\x1b[?1000l\x1b[?1006l');
|
|
43
|
-
};
|
|
44
|
-
}, [stdin]);
|
|
45
|
-
}
|