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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-ai",
3
- "version": "7.2.1",
3
+ "version": "7.3.0",
4
4
  "description": "CLARITY — terminal AI agent with local GGUF inference on HF Spaces",
5
5
  "type": "module",
6
6
  "bin": {
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 { Banner } from './components/Banner.js';
6
- import { StatusBar } from './components/StatusBar.js';
7
- import { StreamView } from './components/StreamView.js';
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 FOOTER_H = 2;
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 - bannerH - STATUS_H - DOCK_H - FOOTER_H - 2);
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) setThinkingStart(Date.now());
83
- else if (!state.thinking) setThinkingStart(null);
84
- }, [state.thinking]);
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 (!showPopup) return;
129
- if (key.escape) { closePopup(); return; }
130
- if (key.return) {
131
- const filtered = getFiltered(input);
132
- if (filtered[popupIdx]) handlePopupSelect(filtered[popupIdx].name);
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 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,
143
170
  showPopup
144
- ? h(Box, { position: 'absolute', bottom: DOCK_H + FOOTER_H, alignItems: 'center', width: '100%' },
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(InputDock, {
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: cardW },
158
- h(Banner, { cols }),
159
- h(StatusBar, { status, thinkingStart }),
160
- h(Box, { height: 1 }),
161
- bottomArea
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
- h(Banner, { cols }),
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 StreamView({ messages, streamContent, status, maxLines, width }) {
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 (streamContent && (status === 'streaming' || status === 'thinking')) {
98
- const streamNeeded = 1 + lineCount(streamContent, cw);
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, streamContent, status, maxLines, cw]);
107
+ }, [messages, streamDisplay, status, maxLines, thinkingH, cw]);
107
108
 
108
- return h(Box, { flexDirection: 'column', width: '100%', paddingLeft: 1, paddingRight: 1 },
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') && streamContent
111
- ? h(StreamingBlock, { content: streamContent, width: cw })
111
+ (status === 'streaming' || status === 'thinking') && streamDisplay
112
+ ? h(StreamingBlock, { content: streamDisplay, width: cw })
112
113
  : null,
113
- status === 'thinking' && !streamContent
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
+ }
@@ -21,6 +21,9 @@ export const hex = {
21
21
  text: '#EEEEF0',
22
22
  textDim: '#9999AA',
23
23
  textMuted: '#555566',
24
+ border: '#2A2A3A',
25
+ thinkingBg: '#14141E',
26
+ thinkingText: '#666688',
24
27
  };
25
28
 
26
29
  export const color = {
@@ -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
- }
@@ -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
- }