clarity-ai 7.0.0 → 7.2.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 +20 -9
- package/package.json +1 -2
- package/src/app.js +131 -52
- package/src/components/Banner.js +41 -0
- package/src/components/Footer.js +7 -6
- package/src/components/InputDock.js +29 -0
- package/src/components/SlashPopup.js +55 -0
- package/src/components/StatusBar.js +45 -0
- package/src/components/StreamView.js +91 -51
- package/src/config/theme.js +17 -13
- package/src/hooks/useMouse.js +45 -0
- package/src/components/Header.js +0 -11
- package/src/components/PromptCard.js +0 -50
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## 7.2.0 (2026-06-06)
|
|
6
|
+
|
|
7
|
+
### Sticky floating overlay engine — touch selection, phase-driven layout, virtualized log
|
|
8
|
+
- **XTerm SGR mouse tracking**: `\x1b[?1000h\x1b[?1006h` on boot, full cleanup on exit. Parses `\x1b[<code;col;rowM` sequences to map terminal grid clicks to popup items.
|
|
9
|
+
- **Phase-driven layout transition**: `initial` (centered logo + input, `justifyContent: center`) → `chat` (banner at top, messages flex-grow, dock hard-locked to bottom). Triggers instantly on first user/assistant message.
|
|
10
|
+
- **Solid sticky bottom dock**: hard-locked 4-row InputDock at `rows - 4`. Graphite `#1C1C1C` fill with orange `#FF9F43` accent bar. Never slides or bounces during streaming.
|
|
11
|
+
- **Virtualized viewport**: `StreamView` measures each message's line count via `wrap-ansi` against current width. Slices message array to `rows - bannerH - statusH - dockH - footerH`. Pushes old lines off render block when overflowed.
|
|
12
|
+
- **Slash-command floating popup**: Absolute-positioned overlay above input dock. Auto-opens when input starts with `/`. Orange `#FF9F43` full-width selection bar with black text. Arrow-key navigation + mouse click selection.
|
|
13
|
+
- **Stderr log routing**: All `console.log/error/warn` redirected to `clarity-debug.log` file to prevent stray async output from corrupting the UI layer.
|
|
14
|
+
- **Concurrency lock**: Input disabled during `THINKING`/`STREAMING` states; keyboard fully captured until idle.
|
|
15
|
+
|
|
16
|
+
## 7.1.0 (2026-06-06)
|
|
17
|
+
|
|
18
|
+
### Premium UI/UX overhaul — high-texture capsule design with live streaming
|
|
19
|
+
- **Massive ASCII art banner**: 6-line figlet-style "CLARITY" in orange→purple→blue gradient (auto-falls back for narrow terminals)
|
|
20
|
+
- **Solid Ink capsule cards**: zero wire borders (`|`, `[`, `]`, `-` banned). All containers use `backgroundColor` blocks with graphite (#1C1C1C) surfaces
|
|
21
|
+
- **Orange accent selection**: `#FF9F43` full-width highlight bars with black text for all active/focus elements
|
|
22
|
+
- **State machine**: `IDLE` → `THINKING` (live elapsed counter in ms) → `STREAMING` — each with distinct badge coloring
|
|
23
|
+
- **Live token streaming**: every chunk runs through `wrap-ansi` + `string-width` against current terminal width; no pre-packaged truncation
|
|
24
|
+
- **Dynamic viewport hardening**: `process.stdout.on('resize')` tracks live dimensions; auto-scrolls old content up while keeping input anchored
|
|
25
|
+
|
|
5
26
|
## 7.0.0 (2026-06-06)
|
|
6
27
|
|
|
7
28
|
### Ground-up architectural rebuild — centered minimalist terminal platform
|
package/bin/clarity.js
CHANGED
|
@@ -4,10 +4,12 @@ import { render } from 'ink';
|
|
|
4
4
|
import { App } from '../src/app.js';
|
|
5
5
|
import { hasKey } from '../src/config/keys.js';
|
|
6
6
|
import { createInterface } from 'readline';
|
|
7
|
+
import { createWriteStream } from 'fs';
|
|
7
8
|
|
|
8
9
|
process.stdin.resume();
|
|
9
10
|
process.stdin.setEncoding('utf8');
|
|
10
11
|
|
|
12
|
+
const logFile = createWriteStream('clarity-debug.log', { flags: 'a' });
|
|
11
13
|
const originalLog = console.log;
|
|
12
14
|
const originalError = console.error;
|
|
13
15
|
const originalWarn = console.warn;
|
|
@@ -28,15 +30,18 @@ async function main() {
|
|
|
28
30
|
process.stdin.resume();
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
console.log =
|
|
32
|
-
console.error =
|
|
33
|
-
console.warn =
|
|
33
|
+
console.log = (...args) => logFile.write('[LOG] ' + args.map(String).join(' ') + '\n');
|
|
34
|
+
console.error = (...args) => logFile.write('[ERR] ' + args.map(String).join(' ') + '\n');
|
|
35
|
+
console.warn = (...args) => logFile.write('[WARN] ' + args.map(String).join(' ') + '\n');
|
|
34
36
|
|
|
35
37
|
let keepAlive;
|
|
36
38
|
|
|
37
|
-
const config = {
|
|
39
|
+
const config = {
|
|
40
|
+
provider,
|
|
41
|
+
model: process.env.CLARITY_MODEL || 'groq/llama-3.3-70b-versatile',
|
|
42
|
+
};
|
|
38
43
|
|
|
39
|
-
const { clear
|
|
44
|
+
const { clear } = render(React.createElement(App, { config }), {
|
|
40
45
|
fullscreen: true,
|
|
41
46
|
patchConsole: false,
|
|
42
47
|
exitOnCtrlC: false,
|
|
@@ -49,14 +54,18 @@ async function main() {
|
|
|
49
54
|
console.log = originalLog;
|
|
50
55
|
console.error = originalError;
|
|
51
56
|
console.warn = originalWarn;
|
|
57
|
+
logFile.write('[CLEANUP] CLARITY exiting\n');
|
|
58
|
+
logFile.end();
|
|
52
59
|
try { clear(); } catch {}
|
|
53
|
-
process.stdout.write('\x1b[?25h\x1b[0m');
|
|
60
|
+
process.stdout.write('\x1b[?1000l\x1b[?1006l\x1b[?25h\x1b[0m');
|
|
54
61
|
process.exit(0);
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
process.on('SIGINT', () => cleanup());
|
|
58
65
|
process.on('SIGTERM', () => cleanup());
|
|
59
|
-
process.on('exit', () => {
|
|
66
|
+
process.on('exit', () => {
|
|
67
|
+
process.stdout.write('\x1b[?1000l\x1b[?1006l\x1b[?25h\x1b[0m');
|
|
68
|
+
});
|
|
60
69
|
|
|
61
70
|
await new Promise(() => {});
|
|
62
71
|
}
|
|
@@ -65,7 +74,9 @@ main().catch(err => {
|
|
|
65
74
|
console.log = originalLog;
|
|
66
75
|
console.error = originalError;
|
|
67
76
|
console.warn = originalWarn;
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
logFile.write('[FATAL] ' + (err?.message || String(err)) + '\n');
|
|
78
|
+
logFile.end();
|
|
79
|
+
process.stdout.write('\x1b[?1000l\x1b[?1006l\x1b[?25h\x1b[0m');
|
|
80
|
+
console.error('\n\x1b[31mFatal error:\x1b[0m', err?.message || err);
|
|
70
81
|
process.exit(1);
|
|
71
82
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clarity-ai",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.2.0",
|
|
4
4
|
"description": "CLARITY — terminal AI agent with local GGUF inference on HF Spaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
"gradient-string": "^3",
|
|
29
29
|
"ink": "^5",
|
|
30
30
|
"ink-spinner": "^5",
|
|
31
|
-
"ink-text-input": "^6.0.0",
|
|
32
31
|
"react": "^18",
|
|
33
32
|
"string-width": "^7",
|
|
34
33
|
"wrap-ansi": "^9"
|
package/src/app.js
CHANGED
|
@@ -1,29 +1,36 @@
|
|
|
1
1
|
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
-
import { Box,
|
|
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 {
|
|
5
|
+
import { Banner } from './components/Banner.js';
|
|
6
|
+
import { StatusBar } from './components/StatusBar.js';
|
|
7
7
|
import { StreamView } from './components/StreamView.js';
|
|
8
|
-
import {
|
|
8
|
+
import { InputDock } from './components/InputDock.js';
|
|
9
|
+
import { SlashPopup } from './components/SlashPopup.js';
|
|
9
10
|
import { Footer } from './components/Footer.js';
|
|
11
|
+
import { useMouse } from './hooks/useMouse.js';
|
|
10
12
|
const { createElement: h } = React;
|
|
11
13
|
|
|
12
14
|
let abortController = null;
|
|
13
15
|
|
|
14
|
-
export function getAbortController() {
|
|
15
|
-
|
|
16
|
+
export function getAbortController() { return abortController; }
|
|
17
|
+
export function setAbortController(ac) { abortController = ac; }
|
|
18
|
+
export function cancelStream() {
|
|
19
|
+
if (abortController) { try { abortController.abort(); } catch {} abortController = null; }
|
|
16
20
|
}
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
const DOCK_H = 4;
|
|
23
|
+
const FOOTER_H = 2;
|
|
24
|
+
const STATUS_H = 1;
|
|
25
|
+
const POPUP_W = 44;
|
|
26
|
+
|
|
27
|
+
function deriveStatus(state, sc) {
|
|
28
|
+
if (!state.thinking) return 'idle';
|
|
29
|
+
return sc ? 'streaming' : 'thinking';
|
|
20
30
|
}
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
try { abortController.abort(); } catch {}
|
|
25
|
-
abortController = null;
|
|
26
|
-
}
|
|
32
|
+
function hasRealMessages(msgs) {
|
|
33
|
+
return msgs.some(m => m.role === 'user' || m.role === 'assistant');
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
export function App({ config }) {
|
|
@@ -31,7 +38,10 @@ export function App({ config }) {
|
|
|
31
38
|
const [dims, setDims] = useState({ rows: stdout.rows || 30, cols: stdout.columns || 80 });
|
|
32
39
|
const [state, setState] = useState(() => createChatState());
|
|
33
40
|
const [streamContent, setStreamContent] = useState('');
|
|
34
|
-
const
|
|
41
|
+
const [thinkingStart, setThinkingStart] = useState(null);
|
|
42
|
+
const [input, setInput] = useState('');
|
|
43
|
+
const [popupIdx, setPopupIdx] = useState(0);
|
|
44
|
+
const defaultModel = (config.model || '').replace(/^[^/]+\//, '') || 'llama-3.3-70b-versatile';
|
|
35
45
|
const [model, setModel] = useState(defaultModel);
|
|
36
46
|
const [provider, setProvider] = useState(config.provider || 'groq');
|
|
37
47
|
|
|
@@ -42,22 +52,28 @@ export function App({ config }) {
|
|
|
42
52
|
modelRef.current = model;
|
|
43
53
|
providerRef.current = provider;
|
|
44
54
|
|
|
55
|
+
const rows = dims.rows;
|
|
56
|
+
const cols = dims.cols;
|
|
57
|
+
const bannerH = cols < 50 ? 2 : 6;
|
|
58
|
+
const status = deriveStatus(state, streamContent);
|
|
59
|
+
const isChat = hasRealMessages(state.messages);
|
|
60
|
+
const cardW = Math.min(cols - 4, 56);
|
|
61
|
+
const showPopup = input.startsWith('/') && status === 'idle';
|
|
62
|
+
const availLines = Math.max(2, rows - bannerH - STATUS_H - DOCK_H - FOOTER_H - 2);
|
|
63
|
+
|
|
45
64
|
useEffect(() => {
|
|
46
|
-
function onResize() {
|
|
47
|
-
setDims({ rows: process.stdout.rows || 30, cols: process.stdout.columns || 80 });
|
|
48
|
-
}
|
|
65
|
+
function onResize() { setDims({ r: process.stdout.rows || 30, c: process.stdout.columns || 80 }); }
|
|
49
66
|
process.stdout.on('resize', onResize);
|
|
50
67
|
return () => process.stdout.removeListener('resize', onResize);
|
|
51
68
|
}, []);
|
|
52
69
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const contentPad = isCompact ? 1 : 2;
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (state.thinking && !thinkingStart) setThinkingStart(Date.now());
|
|
72
|
+
else if (!state.thinking) setThinkingStart(null);
|
|
73
|
+
}, [state.thinking]);
|
|
58
74
|
|
|
59
|
-
const onSubmit = useCallback(async (
|
|
60
|
-
if (
|
|
75
|
+
const onSubmit = useCallback(async (val) => {
|
|
76
|
+
if (val === '/exit') { process.exit(0); return; }
|
|
61
77
|
cancelStream();
|
|
62
78
|
const ac = new AbortController();
|
|
63
79
|
setAbortController(ac);
|
|
@@ -65,41 +81,104 @@ export function App({ config }) {
|
|
|
65
81
|
setState(s => ({ ...s, thinking: false, streamContent: '', agentStatus: '', toolExecutions: [], thoughtTimer: null }));
|
|
66
82
|
setStreamContent('');
|
|
67
83
|
});
|
|
68
|
-
await handleSend(stateRef.current, setState,
|
|
84
|
+
await handleSend(stateRef.current, setState, val, modelRef.current, providerRef.current, setStreamContent, ac.signal);
|
|
69
85
|
}, []);
|
|
70
86
|
|
|
71
|
-
const onCommand = useCallback(async (
|
|
72
|
-
if (
|
|
73
|
-
if (
|
|
74
|
-
await handleCommand(
|
|
87
|
+
const onCommand = useCallback(async (val) => {
|
|
88
|
+
if (val.startsWith('/stop')) { cancelStream(); return; }
|
|
89
|
+
if (val.startsWith('/exit')) { process.exit(0); return; }
|
|
90
|
+
await handleCommand(val, stateRef.current, setState, setModel, setProvider, modelRef.current, providerRef.current);
|
|
75
91
|
}, []);
|
|
76
92
|
|
|
77
|
-
function
|
|
78
|
-
const
|
|
79
|
-
if (!
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
93
|
+
function handleSubmit(val) {
|
|
94
|
+
const t = val.trim();
|
|
95
|
+
if (!t) return;
|
|
96
|
+
setInput('');
|
|
97
|
+
setPopupIdx(0);
|
|
98
|
+
if (t.startsWith('/')) onCommand(t);
|
|
99
|
+
else onSubmit(t);
|
|
85
100
|
}
|
|
86
101
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
function handleInputChange(val) {
|
|
103
|
+
setInput(val);
|
|
104
|
+
if (val.startsWith('/')) setPopupIdx(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function handlePopupSelect(cmd) {
|
|
108
|
+
setInput('');
|
|
109
|
+
setPopupIdx(0);
|
|
110
|
+
if (cmd === '/exit') process.exit(0);
|
|
111
|
+
if (cmd === '/stop') { cancelStream(); return; }
|
|
112
|
+
onCommand(cmd);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function closePopup() { setInput(''); setPopupIdx(0); }
|
|
116
|
+
|
|
117
|
+
useInput((ch, key) => {
|
|
118
|
+
if (!showPopup) return;
|
|
119
|
+
if (key.escape) { closePopup(); return; }
|
|
120
|
+
if (key.return) {
|
|
121
|
+
const COMMANDS = [{ name: '/keys' }, { name: '/model' }, { name: '/provider' }, { name: '/agent' }, { name: '/stop' }, { name: '/clear' }, { name: '/export' }, { name: '/help' }, { name: '/exit' }];
|
|
122
|
+
const q = input.replace(/^\//, '').toLowerCase();
|
|
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));
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const handleClick = useCallback(({ col, row }) => {
|
|
137
|
+
if (!showPopup) return;
|
|
138
|
+
const popupLeft = Math.floor((cols - POPUP_W) / 2) + 1;
|
|
139
|
+
const popupH = 1 + 9 + 1;
|
|
140
|
+
const popupTop = rows - DOCK_H - FOOTER_H - popupH;
|
|
141
|
+
if (col >= popupLeft && col < popupLeft + POPUP_W && row >= popupTop && row < popupTop + popupH) {
|
|
142
|
+
const itemRow = row - popupTop - 1;
|
|
143
|
+
const COMMANDS = [{ name: '/keys' }, { name: '/model' }, { name: '/provider' }, { name: '/agent' }, { name: '/stop' }, { name: '/clear' }, { name: '/export' }, { name: '/help' }, { name: '/exit' }];
|
|
144
|
+
if (itemRow >= 0 && itemRow < COMMANDS.length) {
|
|
145
|
+
handlePopupSelect(COMMANDS[itemRow].name);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}, [showPopup, cols, rows]);
|
|
149
|
+
|
|
150
|
+
useMouse(handleClick);
|
|
151
|
+
|
|
152
|
+
const bottomArea = h(Box, { flexDirection: 'column', alignItems: 'center', width: '100%' },
|
|
153
|
+
showPopup
|
|
154
|
+
? h(Box, { position: 'absolute', bottom: DOCK_H + FOOTER_H, alignItems: 'center', width: '100%' },
|
|
155
|
+
h(SlashPopup, { search: input, selectedIdx: popupIdx, onHover: setPopupIdx, width: POPUP_W })
|
|
156
|
+
)
|
|
157
|
+
: null,
|
|
158
|
+
h(InputDock, {
|
|
159
|
+
width: cardW, provider, model, agentMode: state.agentMode, status,
|
|
160
|
+
input, onInputChange: handleInputChange, onSubmit: handleSubmit,
|
|
101
161
|
}),
|
|
102
|
-
h(Box, { flexGrow: 1, minHeight: 1 }),
|
|
103
162
|
h(Footer, { cols })
|
|
104
163
|
);
|
|
164
|
+
|
|
165
|
+
if (!isChat) {
|
|
166
|
+
return h(Box, { width: '100%', height: rows, flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: hex.bg },
|
|
167
|
+
h(Box, { flexDirection: 'column', alignItems: 'center', width: cardW },
|
|
168
|
+
h(Banner, { cols }),
|
|
169
|
+
h(StatusBar, { status, thinkingStart }),
|
|
170
|
+
h(Box, { height: 1 }),
|
|
171
|
+
bottomArea
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return h(Box, { width: '100%', height: rows, flexDirection: 'column', alignItems: 'center', backgroundColor: hex.bg },
|
|
177
|
+
h(Banner, { cols }),
|
|
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
|
|
183
|
+
);
|
|
105
184
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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
CHANGED
|
@@ -4,19 +4,20 @@ import { hex } from '../config/theme.js';
|
|
|
4
4
|
const { createElement: h } = React;
|
|
5
5
|
|
|
6
6
|
export function Footer({ cols }) {
|
|
7
|
-
const showFull = cols >=
|
|
7
|
+
const showFull = cols >= 58;
|
|
8
8
|
const keybindText = 'tab agents ctrl+p commands';
|
|
9
|
-
const tipText = 'Tip: Use /help to view system commands and switch active models';
|
|
10
9
|
|
|
11
10
|
return h(Box, { width: '100%', flexDirection: 'column', backgroundColor: hex.bg },
|
|
12
|
-
h(Box, { height: 1, justifyContent: 'flex-end', paddingRight: 2 },
|
|
11
|
+
h(Box, { height: 1, justifyContent: 'flex-end', paddingRight: 2, backgroundColor: hex.bg },
|
|
13
12
|
h(Text, { color: hex.textMuted }, keybindText)
|
|
14
13
|
),
|
|
15
|
-
h(Box, { height: 1, paddingLeft: 2, paddingRight: 2 },
|
|
14
|
+
h(Box, { height: 1, paddingLeft: 2, paddingRight: 2, backgroundColor: hex.bg },
|
|
16
15
|
h(Text, { color: hex.textDim },
|
|
17
16
|
'\u2580 ',
|
|
18
|
-
h(Text, { color: hex.
|
|
19
|
-
h(Text, { color: hex.textDim }, ': ' + (showFull
|
|
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'))
|
|
20
21
|
)
|
|
21
22
|
)
|
|
22
23
|
);
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { hex } from '../config/theme.js';
|
|
4
|
+
const { createElement: h } = React;
|
|
5
|
+
|
|
6
|
+
const COMMANDS = [
|
|
7
|
+
{ name: '/keys', desc: 'Set API key' },
|
|
8
|
+
{ name: '/model', desc: 'Switch model' },
|
|
9
|
+
{ name: '/provider', desc: 'Switch provider' },
|
|
10
|
+
{ name: '/agent', desc: 'Toggle agent mode' },
|
|
11
|
+
{ name: '/stop', desc: 'Cancel running stream' },
|
|
12
|
+
{ name: '/clear', desc: 'Clear conversation' },
|
|
13
|
+
{ name: '/export', desc: 'Export conversation' },
|
|
14
|
+
{ name: '/help', desc: 'Show all commands' },
|
|
15
|
+
{ name: '/exit', desc: 'Exit CLARITY' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function SlashPopup({ search, selectedIdx, onHover, width }) {
|
|
19
|
+
const filtered = useMemo(() => {
|
|
20
|
+
const q = search.replace(/^\//, '').toLowerCase();
|
|
21
|
+
if (!q) return COMMANDS;
|
|
22
|
+
return COMMANDS.filter(c =>
|
|
23
|
+
c.name.toLowerCase().includes(q) || c.desc.toLowerCase().includes(q)
|
|
24
|
+
);
|
|
25
|
+
}, [search]);
|
|
26
|
+
|
|
27
|
+
const innerW = Math.max(10, width - 4);
|
|
28
|
+
const itemLabel = (cmd, i) => {
|
|
29
|
+
const sel = i === selectedIdx;
|
|
30
|
+
const label = ' ' + cmd.name + ' ' + cmd.desc;
|
|
31
|
+
return label.length > innerW ? label.slice(0, innerW - 2) + '\u2026' : label;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return h(Box, { flexDirection: 'column', width, backgroundColor: hex.surface },
|
|
35
|
+
h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
|
|
36
|
+
h(Text, { color: hex.textMuted }, ' ' + (search || '/') + ' '.repeat(Math.max(0, innerW - (search || '/').length)))
|
|
37
|
+
),
|
|
38
|
+
filtered.map((cmd, i) =>
|
|
39
|
+
h(Box, {
|
|
40
|
+
key: cmd.name,
|
|
41
|
+
height: 1,
|
|
42
|
+
backgroundColor: i === selectedIdx ? hex.orange : 'transparent',
|
|
43
|
+
},
|
|
44
|
+
h(Text, {
|
|
45
|
+
color: i === selectedIdx ? '#000000' : hex.text,
|
|
46
|
+
bold: i === selectedIdx,
|
|
47
|
+
}, itemLabel(cmd, i))
|
|
48
|
+
)
|
|
49
|
+
),
|
|
50
|
+
filtered.length === 0
|
|
51
|
+
? h(Box, { height: 1 },
|
|
52
|
+
h(Text, { color: hex.textMuted }, ' No matching commands'))
|
|
53
|
+
: null,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
}
|
|
@@ -2,76 +2,116 @@ 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 cliTruncate from 'cli-truncate';
|
|
6
5
|
const { createElement: h } = React;
|
|
7
6
|
|
|
8
|
-
function
|
|
9
|
-
if (!text) return
|
|
10
|
-
return wrapAnsi(String(text), Math.max(
|
|
7
|
+
function lineCount(text, width) {
|
|
8
|
+
if (!text) return 1;
|
|
9
|
+
return wrapAnsi(String(text), Math.max(4, width), { trim: false, hard: true }).split('\n').length;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
function
|
|
14
|
-
|
|
12
|
+
function measure(msg, width) {
|
|
13
|
+
const cw = Math.max(4, width);
|
|
14
|
+
if (msg.role === 'user') return 1 + lineCount(msg.content, cw);
|
|
15
|
+
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
|
+
return 1;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
case '
|
|
29
|
-
|
|
30
|
-
h(Text, { color: hex.purple, bold: true }, ' \u25C6 '),
|
|
31
|
-
h(Text, { color: hex.text }, trunc(line.content, width - 4))
|
|
32
|
-
);
|
|
33
|
-
case 'tool':
|
|
34
|
-
return h(Box, { height: 1 },
|
|
35
|
-
h(Text, { color: hex.green }, ' \u25C9 ' + trunc((line.toolName || 'tool') + (line.duration ? ' ' + line.duration + 'ms' : ''), width - 6))
|
|
36
|
-
);
|
|
37
|
-
case 'error':
|
|
38
|
-
return h(Box, { height: 1 },
|
|
39
|
-
h(Text, { color: hex.red }, ' \u2716 ' + trunc(line.content, width - 4))
|
|
40
|
-
);
|
|
41
|
-
default:
|
|
42
|
-
return null;
|
|
21
|
+
function wrap(text, width) {
|
|
22
|
+
if (!text) return [];
|
|
23
|
+
return wrapAnsi(String(text), Math.max(4, width), { trim: false, hard: true }).split('\n');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function MsgRole({ role }) {
|
|
27
|
+
switch (role) {
|
|
28
|
+
case 'user': return h(Text, { color: hex.orange, bold: true }, '\u276F ');
|
|
29
|
+
case 'assistant': return h(Text, { color: hex.purple, bold: true }, '\u25C6 ');
|
|
30
|
+
case 'system': return h(Text, { color: hex.blue }, '\u25C9 ');
|
|
31
|
+
case 'error': return h(Text, { color: hex.red }, '\u2716 ');
|
|
32
|
+
case 'tool': return h(Text, { color: hex.green }, '\u25C9 ');
|
|
33
|
+
default: return null;
|
|
43
34
|
}
|
|
44
35
|
}
|
|
45
36
|
|
|
46
|
-
function
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
return h(Box, {
|
|
50
|
-
h(
|
|
51
|
-
|
|
37
|
+
function MsgBlock({ msg, width }) {
|
|
38
|
+
const lines = useMemo(() => wrap(msg.content, width), [msg.content, width]);
|
|
39
|
+
|
|
40
|
+
return h(Box, { flexDirection: 'column' },
|
|
41
|
+
h(Box, { height: 1 },
|
|
42
|
+
h(MsgRole, { role: msg.role }),
|
|
43
|
+
h(Text, { color: hex.text }, lines[0] || '')
|
|
44
|
+
),
|
|
45
|
+
lines.slice(1).map((l, i) =>
|
|
46
|
+
h(Box, { key: i, height: 1 },
|
|
47
|
+
h(Text, { color: hex.text }, ' ' + l)
|
|
48
|
+
)
|
|
49
|
+
),
|
|
50
|
+
msg.role === 'assistant' && msg.duration
|
|
51
|
+
? h(Box, { height: 1 },
|
|
52
|
+
h(Text, { color: hex.textMuted },
|
|
53
|
+
' ' + (msg.duration < 1000 ? msg.duration + 'ms' : (msg.duration / 1000).toFixed(1) + 's'))
|
|
54
|
+
)
|
|
55
|
+
: null
|
|
52
56
|
);
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
const
|
|
59
|
+
function StreamingBlock({ content, width }) {
|
|
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] || '')
|
|
65
|
+
),
|
|
66
|
+
lines.slice(1).map((l, i) =>
|
|
67
|
+
h(Box, { key: i, height: 1 },
|
|
68
|
+
h(Text, { color: hex.text }, ' ' + l)
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function StreamView({ messages, streamContent, status, maxLines, width }) {
|
|
75
|
+
const cw = Math.max(4, width - 2);
|
|
57
76
|
|
|
58
|
-
const
|
|
77
|
+
const visible = useMemo(() => {
|
|
59
78
|
if (messages.length === 0) {
|
|
60
|
-
return [{ id: '
|
|
79
|
+
return [{ id: 'welcome', role: 'system', content: 'CLARITY AI ready \u00B7 /help for commands' }];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let avail = maxLines;
|
|
83
|
+
const result = [];
|
|
84
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
85
|
+
const needed = measure(messages[i], cw);
|
|
86
|
+
if (needed > avail) {
|
|
87
|
+
if (result.length === 0 && needed > avail) {
|
|
88
|
+
result.unshift(messages[i]);
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
avail -= needed;
|
|
93
|
+
result.unshift(messages[i]);
|
|
61
94
|
}
|
|
62
|
-
|
|
63
|
-
|
|
95
|
+
|
|
96
|
+
if (streamContent && (status === 'streaming' || status === 'thinking')) {
|
|
97
|
+
const streamNeeded = 1 + lineCount(streamContent, cw);
|
|
98
|
+
while (streamNeeded > avail && result.length > 0) {
|
|
99
|
+
avail += measure(result[0], cw);
|
|
100
|
+
result.shift();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
}, [messages, streamContent, status, maxLines, cw]);
|
|
64
106
|
|
|
65
107
|
return h(Box, { flexDirection: 'column', width: '100%', paddingLeft: 1, paddingRight: 1 },
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
thinking && streamContent
|
|
70
|
-
? h(StreamingLine, { content: streamContent, width: contentW })
|
|
108
|
+
visible.map(m => h(MsgBlock, { key: m.id, msg: m, width: cw })),
|
|
109
|
+
(status === 'streaming' || status === 'thinking') && streamContent
|
|
110
|
+
? h(StreamingBlock, { content: streamContent, width: cw })
|
|
71
111
|
: null,
|
|
72
|
-
thinking && !streamContent
|
|
112
|
+
status === 'thinking' && !streamContent
|
|
73
113
|
? h(Box, { height: 1 },
|
|
74
|
-
h(Text, { color: hex.
|
|
114
|
+
h(Text, { color: hex.textMuted }, ' \u25CF processing...')
|
|
75
115
|
)
|
|
76
116
|
: null,
|
|
77
117
|
);
|
package/src/config/theme.js
CHANGED
|
@@ -2,33 +2,37 @@ import chalk from 'chalk';
|
|
|
2
2
|
import gradient from 'gradient-string';
|
|
3
3
|
|
|
4
4
|
export const hex = {
|
|
5
|
-
bg: '#
|
|
6
|
-
surface: '#
|
|
7
|
-
surfaceAlt: '#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
bg: '#0D0D14',
|
|
6
|
+
surface: '#111118',
|
|
7
|
+
surfaceAlt: '#16161E',
|
|
8
|
+
cardBg: '#1C1C1C',
|
|
9
|
+
inputBg: '#1A1A22',
|
|
10
|
+
codeBg: '#0D0D14',
|
|
11
|
+
selectionBg: '#FF9F43',
|
|
12
|
+
selectionText: '#000000',
|
|
13
|
+
accent: '#FF9F43',
|
|
14
|
+
orange: '#FF9F43',
|
|
13
15
|
purple: '#A855F7',
|
|
14
16
|
green: '#22C55E',
|
|
15
17
|
red: '#EF4444',
|
|
16
18
|
gold: '#F59E0B',
|
|
17
19
|
blue: '#3B82F6',
|
|
18
20
|
cyan: '#22D3EE',
|
|
19
|
-
text: '#
|
|
20
|
-
textDim: '#
|
|
21
|
-
textMuted: '#
|
|
21
|
+
text: '#EEEEF0',
|
|
22
|
+
textDim: '#9999AA',
|
|
23
|
+
textMuted: '#555566',
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
export const color = {
|
|
25
27
|
bg: chalk.hex(hex.bg),
|
|
26
28
|
surface: chalk.hex(hex.surface),
|
|
27
29
|
surfaceAlt: chalk.hex(hex.surfaceAlt),
|
|
28
|
-
|
|
30
|
+
cardBg: chalk.hex(hex.cardBg),
|
|
31
|
+
inputBg: chalk.hex(hex.inputBg),
|
|
29
32
|
selectionBg: chalk.hex(hex.selectionBg),
|
|
30
33
|
selectionText: chalk.hex(hex.selectionText),
|
|
31
34
|
accent: chalk.hex(hex.accent),
|
|
35
|
+
orange: chalk.hex(hex.orange),
|
|
32
36
|
purple: chalk.hex(hex.purple),
|
|
33
37
|
green: chalk.hex(hex.green),
|
|
34
38
|
red: chalk.hex(hex.red),
|
|
@@ -40,4 +44,4 @@ export const color = {
|
|
|
40
44
|
textMuted: chalk.hex(hex.textMuted),
|
|
41
45
|
};
|
|
42
46
|
|
|
43
|
-
export const appGradient = gradient(['#
|
|
47
|
+
export const appGradient = gradient(['#FF9F43', '#A855F7', '#3B82F6']);
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
}
|
package/src/components/Header.js
DELETED
|
@@ -1,11 +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
|
-
export function Header({ cols }) {
|
|
7
|
-
const label = appGradient('CLARITY');
|
|
8
|
-
return h(Box, { height: 3, width: '100%', alignItems: 'center', justifyContent: 'center' },
|
|
9
|
-
h(Text, { bold: true }, label)
|
|
10
|
-
);
|
|
11
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import React, { useState } 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
|
-
const DIVIDER = '\u2500';
|
|
8
|
-
|
|
9
|
-
export function PromptCard({ width, compact, provider, model, agentMode, thinking, onSubmit }) {
|
|
10
|
-
const [input, setInput] = useState('');
|
|
11
|
-
const mShort = model.replace(/^[^/]+\//, '').slice(0, compact ? 10 : 20);
|
|
12
|
-
const pShort = provider.slice(0, compact ? 6 : 10);
|
|
13
|
-
const statusLine = '[ ' + pShort + ' \u00B7 ' + mShort + (agentMode ? ' \u00B7 AGENT' : '') + ' ]';
|
|
14
|
-
const innerW = width - 2;
|
|
15
|
-
|
|
16
|
-
function handleSubmit(value) {
|
|
17
|
-
onSubmit(value);
|
|
18
|
-
setInput('');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return h(Box, { flexDirection: 'column', width, backgroundColor: hex.surface },
|
|
22
|
-
h(Box, { height: 1 },
|
|
23
|
-
h(Text, { color: hex.textMuted },
|
|
24
|
-
'\u250C' + '\u2500'.repeat(Math.max(0, innerW)) + '\u2510')
|
|
25
|
-
),
|
|
26
|
-
h(Box, { height: 1, paddingLeft: 1, paddingRight: 1 },
|
|
27
|
-
h(TextInput, {
|
|
28
|
-
value: input,
|
|
29
|
-
onChange: setInput,
|
|
30
|
-
onSubmit: handleSubmit,
|
|
31
|
-
placeholder: 'Ask anything...',
|
|
32
|
-
focus: !thinking,
|
|
33
|
-
})
|
|
34
|
-
),
|
|
35
|
-
h(Box, { height: 1 },
|
|
36
|
-
h(Text, { color: hex.textMuted },
|
|
37
|
-
'\u2502' + '\u2500'.repeat(Math.max(0, innerW)) + '\u2502')
|
|
38
|
-
),
|
|
39
|
-
h(Box, { height: 1, paddingLeft: 1, paddingRight: 1, alignItems: 'center' },
|
|
40
|
-
h(Text, { color: hex.textDim },
|
|
41
|
-
statusLine + ' '.repeat(Math.max(0, innerW - statusLine.length - 1))
|
|
42
|
-
),
|
|
43
|
-
h(Text, { color: hex.textMuted }, '\u2502')
|
|
44
|
-
),
|
|
45
|
-
h(Box, { height: 1 },
|
|
46
|
-
h(Text, { color: hex.textMuted },
|
|
47
|
-
'\u2514' + '\u2500'.repeat(Math.max(0, innerW)) + '\u2518')
|
|
48
|
-
),
|
|
49
|
-
);
|
|
50
|
-
}
|