clarity-ai 6.2.3 → 6.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/bin/clarity.js +3 -20
- package/package.json +3 -10
- package/src/chat.js +25 -13
- package/src/{app.js → components/AppRoot.js} +44 -40
- package/src/components/CodeBlock.js +20 -12
- package/src/components/CommandPicker.js +10 -10
- package/src/components/Composer.js +82 -71
- package/src/components/Layout.js +48 -0
- package/src/components/LoadingIndicator.js +6 -6
- package/src/components/MessageBubble.js +50 -127
- package/src/components/MessageList.js +110 -105
- package/src/components/ModelPicker.js +11 -11
- package/src/components/ResponseCard.js +53 -0
- package/src/components/StatusBar.js +16 -0
- package/src/components/ThinkingBlock.js +42 -21
- package/src/components/ToolCard.js +41 -30
- package/src/config/layout.js +129 -0
- package/src/config/theme.js +117 -46
- package/src/providers/index.js +1 -1
- package/src/providers/streaming.js +16 -8
- package/src/components/ErrorMessage.js +0 -26
- package/src/components/ThoughtBlock.js +0 -17
- package/src/config/themes.js +0 -17
- package/src/hooks/useHistory.js +0 -28
- package/src/hooks/useScroll.js +0 -19
- package/src/intentDetect.js +0 -13
- package/src/renderer/diff.js +0 -22
- package/src/renderer/markdown.js +0 -53
- package/src/renderer/table.js +0 -25
- package/src/utils/formatTokens.js +0 -11
- package/src/utils/wrapText.js +0 -8
package/bin/clarity.js
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { render } from 'ink';
|
|
4
|
-
import { App } from '../src/
|
|
4
|
+
import { App } from '../src/components/AppRoot.js';
|
|
5
5
|
import { hasKey } from '../src/config/keys.js';
|
|
6
6
|
import { createInterface } from 'readline';
|
|
7
7
|
|
|
8
|
-
// Keep stdin flowing so Ink's useInput hooks can register.
|
|
9
|
-
// This must happen before any async I/O that might pause stdin.
|
|
10
8
|
process.stdin.resume();
|
|
11
9
|
process.stdin.setEncoding('utf8');
|
|
12
10
|
|
|
13
|
-
// Do NOT clear the screen here. Ink's { fullscreen: true } switches
|
|
14
|
-
// to the alternate screen buffer on mount, which implicitly clears
|
|
15
|
-
// the visible area. Any manual \x1b[2J before render() would clear
|
|
16
|
-
// the main buffer — destructive, shows as a flash on Termux.
|
|
17
|
-
|
|
18
11
|
async function main() {
|
|
19
12
|
const provider = process.env.CLARITY_PROVIDER || 'groq';
|
|
20
13
|
|
|
@@ -28,37 +21,27 @@ async function main() {
|
|
|
28
21
|
});
|
|
29
22
|
const { setKey } = await import('../src/config/keys.js');
|
|
30
23
|
setKey(provider, key);
|
|
31
|
-
// readline.close() can leave stdin paused — restore it for Ink
|
|
32
24
|
process.stdin.resume();
|
|
33
25
|
}
|
|
34
26
|
|
|
35
27
|
const config = { provider, model: process.env.CLARITY_MODEL || 'groq/llama-3.3-70b-versatile' };
|
|
36
28
|
|
|
37
|
-
// Mount Ink. fullscreen:true enters the alternate screen buffer.
|
|
38
|
-
// We DO NOT await waitUntilExit; on some Termux/Node combos it
|
|
39
|
-
// resolves before the first frame renders. Instead we keep the
|
|
40
|
-
// process alive with a never-resolving promise and exit only via
|
|
41
|
-
// signal handlers or process.exit from inside the app.
|
|
42
29
|
const { clear } = render(React.createElement(App, { config }), {
|
|
43
30
|
fullscreen: true,
|
|
44
31
|
patchConsole: false,
|
|
45
32
|
});
|
|
46
33
|
|
|
47
|
-
// Keep the event loop alive — Ink's reconciler settles
|
|
48
|
-
// synchronously when no async state updates are queued.
|
|
49
34
|
setInterval(() => {}, 2 ** 31 - 1);
|
|
50
35
|
|
|
51
|
-
// Signal handlers are the ONLY way to exit.
|
|
52
36
|
function cleanup() {
|
|
53
|
-
try { clear(); } catch {}
|
|
54
|
-
process.stdout.write('\x1b[?25h\x1b[0m');
|
|
37
|
+
try { clear(); } catch {}
|
|
38
|
+
process.stdout.write('\x1b[?25h\x1b[0m');
|
|
55
39
|
process.exit(0);
|
|
56
40
|
}
|
|
57
41
|
|
|
58
42
|
process.on('SIGINT', () => cleanup());
|
|
59
43
|
process.on('SIGTERM', () => cleanup());
|
|
60
44
|
|
|
61
|
-
// Never-resolving promise — we stay alive until a signal fires cleanup.
|
|
62
45
|
await new Promise(() => {});
|
|
63
46
|
}
|
|
64
47
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clarity-ai",
|
|
3
|
-
"version": "6.
|
|
4
|
-
"description": "Premium
|
|
3
|
+
"version": "6.3.0",
|
|
4
|
+
"description": "Premium terminal AI agent — fixed-height viewport, box-drawing UI, TrueColor theme, streaming with abort",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"clarity": "bin/clarity.js"
|
|
@@ -16,14 +16,7 @@
|
|
|
16
16
|
"ink": "^5",
|
|
17
17
|
"react": "^18",
|
|
18
18
|
"ink-spinner": "^5",
|
|
19
|
-
"marked": "^12",
|
|
20
|
-
"cli-highlight": "^2",
|
|
21
19
|
"chalk": "^5",
|
|
22
|
-
"ansi
|
|
23
|
-
"cli-cursor": "^5",
|
|
24
|
-
"wrap-ansi": "^9",
|
|
25
|
-
"strip-ansi": "^7",
|
|
26
|
-
"string-width": "^7",
|
|
27
|
-
"picocolors": "^1"
|
|
20
|
+
"wrap-ansi": "^9"
|
|
28
21
|
}
|
|
29
22
|
}
|
package/src/chat.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { callAI } from './providers/index.js';
|
|
2
2
|
import { setKey } from './config/keys.js';
|
|
3
3
|
import { TOOLS, executeTool } from './tools.js';
|
|
4
|
+
import { cancelStream } from './components/AppRoot.js';
|
|
5
|
+
|
|
4
6
|
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
5
7
|
|
|
6
8
|
export function createChatState() {
|
|
@@ -12,8 +14,6 @@ export function createChatState() {
|
|
|
12
14
|
}],
|
|
13
15
|
thinking: false,
|
|
14
16
|
streamContent: '',
|
|
15
|
-
awaitingKey: false,
|
|
16
|
-
blockedProvider: null,
|
|
17
17
|
agentMode: true,
|
|
18
18
|
agentStatus: '',
|
|
19
19
|
toolExecutions: [],
|
|
@@ -26,8 +26,8 @@ let execId = 0;
|
|
|
26
26
|
function nextId() { return 'm' + (++msgId); }
|
|
27
27
|
function nextExecId() { return 'x' + (++execId); }
|
|
28
28
|
|
|
29
|
-
export async function handleSend(state, setState, input, model, provider, onStreamContent) {
|
|
30
|
-
if (!input.trim()
|
|
29
|
+
export async function handleSend(state, setState, input, model, provider, onStreamContent, signal) {
|
|
30
|
+
if (!input.trim()) return;
|
|
31
31
|
|
|
32
32
|
const userMsg = { id: nextId(), role: 'user', content: input };
|
|
33
33
|
setState(s => ({
|
|
@@ -57,8 +57,9 @@ export async function handleSend(state, setState, input, model, provider, onStre
|
|
|
57
57
|
return base;
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
await processStream(provider, model, history, state.agentMode, setState, onStreamContent);
|
|
60
|
+
await processStream(provider, model, history, state.agentMode, setState, onStreamContent, 0, signal);
|
|
61
61
|
} catch (err) {
|
|
62
|
+
if (err.name === 'AbortError') return;
|
|
62
63
|
const thoughtTime = state.thoughtTimer ? Date.now() - state.thoughtTimer : 0;
|
|
63
64
|
setState(s => ({
|
|
64
65
|
...s, thinking: false, streamContent: '', agentStatus: '', toolExecutions: [], thoughtTimer: null,
|
|
@@ -73,18 +74,20 @@ export async function handleSend(state, setState, input, model, provider, onStre
|
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
async function processStream(provider, model, history, agentMode, setState, onStreamContent, depth = 0) {
|
|
77
|
+
async function processStream(provider, model, history, agentMode, setState, onStreamContent, depth = 0, signal) {
|
|
77
78
|
if (depth > 8) {
|
|
78
79
|
setState(s => ({ ...s, thinking: false, streamContent: '', agentStatus: '', toolExecutions: [], thoughtTimer: null }));
|
|
79
80
|
onStreamContent('');
|
|
80
81
|
return;
|
|
81
82
|
}
|
|
83
|
+
if (signal?.aborted) return;
|
|
82
84
|
|
|
83
85
|
let stream;
|
|
84
86
|
try {
|
|
85
|
-
stream = callAI(provider, model, history, { tools: agentMode ? TOOLS : undefined });
|
|
87
|
+
stream = callAI(provider, model, history, { tools: agentMode ? TOOLS : undefined, signal });
|
|
86
88
|
} catch (err) {
|
|
87
|
-
if (err.type === 'rate_limit') { await sleep(2000); return processStream(provider, model, history, agentMode, setState, onStreamContent, depth); }
|
|
89
|
+
if (err.type === 'rate_limit') { await sleep(2000); return processStream(provider, model, history, agentMode, setState, onStreamContent, depth, signal); }
|
|
90
|
+
if (err.name === 'AbortError') return;
|
|
88
91
|
throw err;
|
|
89
92
|
}
|
|
90
93
|
|
|
@@ -95,13 +98,13 @@ async function processStream(provider, model, history, agentMode, setState, onSt
|
|
|
95
98
|
|
|
96
99
|
try {
|
|
97
100
|
for await (const event of stream) {
|
|
101
|
+
if (signal?.aborted) return;
|
|
98
102
|
if (event.type === 'token') {
|
|
99
103
|
buffer += event.content;
|
|
100
104
|
onStreamContent(buffer);
|
|
101
105
|
setState(s => ({ ...s, agentStatus: 'Writing response...' }));
|
|
102
106
|
} else if (event.type === 'tool_calls') {
|
|
103
107
|
if (!timedOut) toolCallsData = event.calls;
|
|
104
|
-
} else if (event.type === 'done') {
|
|
105
108
|
} else if (event.type === 'timeout') {
|
|
106
109
|
timedOut = true;
|
|
107
110
|
setState(s => ({
|
|
@@ -119,11 +122,13 @@ async function processStream(provider, model, history, agentMode, setState, onSt
|
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
124
|
} catch (err) {
|
|
125
|
+
if (err.name === 'AbortError') return;
|
|
122
126
|
if (err.type === 'rate_limit') {
|
|
123
127
|
await sleep(2000);
|
|
124
128
|
setState(s => ({ ...s, agentStatus: 'Retrying after rate limit...' }));
|
|
125
|
-
return processStream(provider, model, history, agentMode, setState, onStreamContent, depth);
|
|
129
|
+
return processStream(provider, model, history, agentMode, setState, onStreamContent, depth, signal);
|
|
126
130
|
}
|
|
131
|
+
if (signal?.aborted) return;
|
|
127
132
|
throw err;
|
|
128
133
|
}
|
|
129
134
|
|
|
@@ -142,7 +147,7 @@ async function processStream(provider, model, history, agentMode, setState, onSt
|
|
|
142
147
|
onStreamContent('');
|
|
143
148
|
}
|
|
144
149
|
|
|
145
|
-
if (timedOut) {
|
|
150
|
+
if (timedOut || signal?.aborted) {
|
|
146
151
|
setState(s => ({ ...s, thinking: false, toolExecutions: [], thoughtTimer: null }));
|
|
147
152
|
onStreamContent('');
|
|
148
153
|
return;
|
|
@@ -168,6 +173,7 @@ async function processStream(provider, model, history, agentMode, setState, onSt
|
|
|
168
173
|
|
|
169
174
|
const toolResults = [];
|
|
170
175
|
for (let i = 0; i < toolCallsData.length; i++) {
|
|
176
|
+
if (signal?.aborted) return;
|
|
171
177
|
const tc = toolCallsData[i];
|
|
172
178
|
const { name, arguments: argsStr } = tc.function;
|
|
173
179
|
let args;
|
|
@@ -203,6 +209,8 @@ async function processStream(provider, model, history, agentMode, setState, onSt
|
|
|
203
209
|
}));
|
|
204
210
|
}
|
|
205
211
|
|
|
212
|
+
if (signal?.aborted) return;
|
|
213
|
+
|
|
206
214
|
setState(s => ({
|
|
207
215
|
...s,
|
|
208
216
|
agentStatus: 'Processing results...',
|
|
@@ -230,12 +238,15 @@ async function processStream(provider, model, history, agentMode, setState, onSt
|
|
|
230
238
|
...toolResults.map(tr => ({
|
|
231
239
|
id: nextId(), role: 'tool', content: tr.content,
|
|
232
240
|
tool_call_id: tr.tool_call_id, toolName: tr.name,
|
|
241
|
+
completed: true,
|
|
233
242
|
})),
|
|
234
243
|
],
|
|
235
244
|
toolExecutions: [],
|
|
236
245
|
agentStatus: '',
|
|
237
246
|
}));
|
|
238
247
|
|
|
248
|
+
if (signal?.aborted) return;
|
|
249
|
+
|
|
239
250
|
const newHistory = [
|
|
240
251
|
...history,
|
|
241
252
|
{ role: 'assistant', content: buffer || null, tool_calls: toolCallsData.map(tc => ({
|
|
@@ -245,7 +256,7 @@ async function processStream(provider, model, history, agentMode, setState, onSt
|
|
|
245
256
|
...toolResults,
|
|
246
257
|
];
|
|
247
258
|
|
|
248
|
-
await processStream(provider, model, newHistory, agentMode, setState, onStreamContent, depth + 1);
|
|
259
|
+
await processStream(provider, model, newHistory, agentMode, setState, onStreamContent, depth + 1, signal);
|
|
249
260
|
} else {
|
|
250
261
|
setState(s => ({
|
|
251
262
|
...s,
|
|
@@ -267,7 +278,7 @@ export async function handleCommand(input, state, setState, modelSetter, provide
|
|
|
267
278
|
if (args.length >= 2) {
|
|
268
279
|
setKey(args[0], args[1]);
|
|
269
280
|
setState(s => ({
|
|
270
|
-
...s,
|
|
281
|
+
...s,
|
|
271
282
|
messages: [...s.messages, { id: nextId(), role: 'system', content: 'Key saved for ' + args[0] }],
|
|
272
283
|
}));
|
|
273
284
|
} else {
|
|
@@ -303,6 +314,7 @@ export async function handleCommand(input, state, setState, modelSetter, provide
|
|
|
303
314
|
'/model Switch model',
|
|
304
315
|
'/provider Switch provider',
|
|
305
316
|
'/agent Toggle agent mode',
|
|
317
|
+
'/stop Cancel streaming',
|
|
306
318
|
'/clear Clear conversation',
|
|
307
319
|
'/export Export conversation',
|
|
308
320
|
'/help Show this help',
|
|
@@ -1,12 +1,25 @@
|
|
|
1
|
-
import React, { useState, useCallback, useRef } from 'react';
|
|
1
|
+
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
2
|
import { Box } from 'ink';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
import { createChatState, handleSend, handleCommand } from '../chat.js';
|
|
4
|
+
import { hex } from '../config/theme.js';
|
|
5
|
+
import { Layout } from './Layout.js';
|
|
6
|
+
|
|
7
|
+
let abortController = null;
|
|
8
|
+
|
|
9
|
+
export function getAbortController() {
|
|
10
|
+
return abortController;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function setAbortController(ac) {
|
|
14
|
+
abortController = ac;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function cancelStream() {
|
|
18
|
+
if (abortController) {
|
|
19
|
+
try { abortController.abort(); } catch {}
|
|
20
|
+
abortController = null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
10
23
|
|
|
11
24
|
export function App({ config }) {
|
|
12
25
|
const [state, setState] = useState(() => createChatState());
|
|
@@ -20,18 +33,29 @@ export function App({ config }) {
|
|
|
20
33
|
const stateRef = useRef(state);
|
|
21
34
|
const modelRef = useRef(model);
|
|
22
35
|
const providerRef = useRef(provider);
|
|
36
|
+
const streamRef = useRef(streamContent);
|
|
23
37
|
stateRef.current = state;
|
|
24
38
|
modelRef.current = model;
|
|
25
39
|
providerRef.current = provider;
|
|
40
|
+
streamRef.current = streamContent;
|
|
26
41
|
|
|
27
42
|
const onSubmit = useCallback(async (input) => {
|
|
43
|
+
if (input === '/exit') { process.exit(0); return; }
|
|
28
44
|
if (input.startsWith('/')) {
|
|
29
45
|
if (input === '/model' || input === '/models') { setShowModels(true); return; }
|
|
30
46
|
if (input === '/help') { setShowCommands(true); return; }
|
|
47
|
+
if (input === '/stop') { cancelStream(); return; }
|
|
31
48
|
await handleCommand(input, stateRef.current, setState, setModel, setProvider, modelRef.current, providerRef.current);
|
|
32
49
|
return;
|
|
33
50
|
}
|
|
34
|
-
|
|
51
|
+
cancelStream();
|
|
52
|
+
const ac = new AbortController();
|
|
53
|
+
setAbortController(ac);
|
|
54
|
+
ac.signal.addEventListener('abort', () => {
|
|
55
|
+
setState(s => ({ ...s, thinking: false, streamContent: '', agentStatus: '', toolExecutions: [], thoughtTimer: null }));
|
|
56
|
+
setStreamContent('');
|
|
57
|
+
});
|
|
58
|
+
await handleSend(stateRef.current, setState, input, modelRef.current, providerRef.current, setStreamContent, ac.signal);
|
|
35
59
|
}, []);
|
|
36
60
|
|
|
37
61
|
function handleCommandSelect(cmdName) {
|
|
@@ -49,38 +73,18 @@ export function App({ config }) {
|
|
|
49
73
|
}));
|
|
50
74
|
}
|
|
51
75
|
|
|
52
|
-
return
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
thinking: state.thinking,
|
|
57
|
-
streamContent,
|
|
58
|
-
agentStatus: state.agentStatus,
|
|
59
|
-
toolExecutions: state.toolExecutions,
|
|
60
|
-
})
|
|
61
|
-
),
|
|
62
|
-
showCommands || showModels
|
|
63
|
-
? h(Box, { flexDirection: 'column', marginBottom: 1, backgroundColor: theme.surfaceAlt, borderStyle: 'round', borderColor: theme.borderLight },
|
|
64
|
-
showCommands
|
|
65
|
-
? h(CommandPicker, {
|
|
66
|
-
query: '',
|
|
67
|
-
onSelect: handleCommandSelect,
|
|
68
|
-
onClose: () => setShowCommands(false),
|
|
69
|
-
})
|
|
70
|
-
: null,
|
|
71
|
-
showModels
|
|
72
|
-
? h(ModelPicker, {
|
|
73
|
-
onSelect: handleModelSelect,
|
|
74
|
-
onClose: () => setShowModels(false),
|
|
75
|
-
})
|
|
76
|
-
: null
|
|
77
|
-
)
|
|
78
|
-
: null,
|
|
79
|
-
h(Composer, {
|
|
80
|
-
provider,
|
|
76
|
+
return Box({ flexDirection: 'column', backgroundColor: hex.bg },
|
|
77
|
+
Layout({
|
|
78
|
+
state,
|
|
79
|
+
streamContent,
|
|
81
80
|
model,
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
provider,
|
|
82
|
+
showCommands,
|
|
83
|
+
showModels,
|
|
84
|
+
onCommandSelect: handleCommandSelect,
|
|
85
|
+
onModelSelect: handleModelSelect,
|
|
86
|
+
onCloseCommands: () => setShowCommands(false),
|
|
87
|
+
onCloseModels: () => setShowModels(false),
|
|
84
88
|
onSlash: () => setShowCommands(true),
|
|
85
89
|
onSubmit,
|
|
86
90
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import {
|
|
3
|
+
import { hex, usym } from '../config/theme.js';
|
|
4
|
+
import { getLayout } from '../config/layout.js';
|
|
4
5
|
const { createElement: h } = React;
|
|
5
6
|
|
|
6
7
|
const LANG_COLORS = {
|
|
@@ -17,23 +18,30 @@ export function CodeBlock({ code, language }) {
|
|
|
17
18
|
const lines = useMemo(() => String(code).split('\n'), [code]);
|
|
18
19
|
const langColor = LANG_COLORS[lang] || '#555';
|
|
19
20
|
const lnW = String(lines.length).length;
|
|
21
|
+
const { cols } = getLayout();
|
|
22
|
+
const maxLines = 20;
|
|
23
|
+
const visible = lines.slice(0, maxLines);
|
|
20
24
|
|
|
21
|
-
return h(Box, { flexDirection: 'column',
|
|
22
|
-
h(Box, { flexDirection: 'row', backgroundColor:
|
|
23
|
-
h(Text, { color: langColor, bold: true, backgroundColor:
|
|
24
|
-
h(Text, { color:
|
|
25
|
+
return h(Box, { flexDirection: 'column', backgroundColor: hex.codeBg },
|
|
26
|
+
h(Box, { flexDirection: 'row', backgroundColor: hex.codeBg },
|
|
27
|
+
h(Text, { color: langColor, bold: true, backgroundColor: hex.codeBg }, ' ' + lang + ' '),
|
|
28
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg }, String(lines.length) + ' lines '),
|
|
25
29
|
),
|
|
26
|
-
h(Box, { flexDirection: 'column', backgroundColor:
|
|
27
|
-
|
|
28
|
-
h(Box, { key: i, flexDirection: 'row', backgroundColor:
|
|
29
|
-
h(Text, { color:
|
|
30
|
+
h(Box, { flexDirection: 'column', backgroundColor: hex.codeBg },
|
|
31
|
+
visible.map((line, i) =>
|
|
32
|
+
h(Box, { key: i, flexDirection: 'row', backgroundColor: hex.codeBg },
|
|
33
|
+
h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg },
|
|
30
34
|
' ' + String(i + 1).padStart(lnW) + ' '
|
|
31
35
|
),
|
|
32
|
-
h(Text, { color: '#C9D1D9', backgroundColor:
|
|
33
|
-
line || ' '
|
|
36
|
+
h(Text, { color: '#C9D1D9', backgroundColor: hex.codeBg, wrap: 'truncate-end' },
|
|
37
|
+
(line || ' ').slice(0, cols - 8)
|
|
34
38
|
)
|
|
35
39
|
)
|
|
36
|
-
)
|
|
40
|
+
),
|
|
41
|
+
lines.length > maxLines
|
|
42
|
+
? h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg },
|
|
43
|
+
' ' + usym.ellipsis + ' ' + (lines.length - maxLines) + ' more lines')
|
|
44
|
+
: null
|
|
37
45
|
)
|
|
38
46
|
);
|
|
39
47
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
|
-
import {
|
|
3
|
+
import { hex, usym } from '../config/theme.js';
|
|
4
4
|
const { createElement: h } = React;
|
|
5
5
|
|
|
6
6
|
const COMMANDS = [
|
|
@@ -8,8 +8,8 @@ const COMMANDS = [
|
|
|
8
8
|
{ name: '/model', desc: 'Switch model' },
|
|
9
9
|
{ name: '/provider', desc: 'Switch provider' },
|
|
10
10
|
{ name: '/agent', desc: 'Toggle agent mode' },
|
|
11
|
+
{ name: '/stop', desc: 'Cancel streaming' },
|
|
11
12
|
{ name: '/clear', desc: 'Clear conversation' },
|
|
12
|
-
{ name: '/theme', desc: 'Change color theme' },
|
|
13
13
|
{ name: '/export', desc: 'Export conversation' },
|
|
14
14
|
{ name: '/help', desc: 'Show all commands' },
|
|
15
15
|
{ name: '/exit', desc: 'Exit CLARITY' },
|
|
@@ -37,29 +37,29 @@ export function CommandPicker({ query, onSelect, onClose }) {
|
|
|
37
37
|
|
|
38
38
|
return h(Box, { flexDirection: 'column', width: boxWidth },
|
|
39
39
|
h(Box, { flexDirection: 'row', marginBottom: 1, gap: 1 },
|
|
40
|
-
h(Text, { color:
|
|
41
|
-
h(Text, { color: search ?
|
|
40
|
+
h(Text, { color: hex.textMuted }, usym.bulb),
|
|
41
|
+
h(Text, { color: search ? hex.text : hex.textMuted }, search || 'type to filter...'),
|
|
42
42
|
),
|
|
43
43
|
filtered.map((cmd, i) =>
|
|
44
44
|
h(Box, {
|
|
45
45
|
key: cmd.name,
|
|
46
46
|
flexDirection: 'row',
|
|
47
|
-
backgroundColor: i === idx ?
|
|
47
|
+
backgroundColor: i === idx ? hex.selectionBg : undefined,
|
|
48
48
|
width: boxWidth,
|
|
49
49
|
},
|
|
50
50
|
h(Text, {
|
|
51
|
-
color: i === idx ?
|
|
51
|
+
color: i === idx ? hex.selectionText : hex.text,
|
|
52
52
|
bold: i === idx,
|
|
53
|
-
backgroundColor: i === idx ?
|
|
53
|
+
backgroundColor: i === idx ? hex.selectionBg : undefined,
|
|
54
54
|
wrap: 'truncate-end',
|
|
55
55
|
}, ' ' + cmd.name.padEnd(16)),
|
|
56
56
|
h(Text, {
|
|
57
|
-
color: i === idx ?
|
|
58
|
-
backgroundColor: i === idx ?
|
|
57
|
+
color: i === idx ? hex.selectionText : hex.textDim,
|
|
58
|
+
backgroundColor: i === idx ? hex.selectionBg : undefined,
|
|
59
59
|
wrap: 'truncate-end',
|
|
60
60
|
}, cmd.desc)
|
|
61
61
|
)
|
|
62
62
|
),
|
|
63
|
-
h(Text, { color:
|
|
63
|
+
h(Text, { color: hex.textMuted }, ' ' + usym.arrowU + usym.arrowD + ' nav Enter select Esc close')
|
|
64
64
|
);
|
|
65
65
|
}
|
|
@@ -1,93 +1,104 @@
|
|
|
1
|
-
import React, { useState, useRef } from 'react';
|
|
1
|
+
import React, { useState, useCallback, useRef } from 'react';
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { hex, usym, u } from '../config/theme.js';
|
|
4
|
+
import { getLayout } from '../config/layout.js';
|
|
5
|
+
|
|
6
|
+
const MAX_VISIBLE_ROWS = 3;
|
|
5
7
|
|
|
6
8
|
export function Composer({ provider, model, agentMode, thinking, onSlash, onSubmit }) {
|
|
7
|
-
const [
|
|
8
|
-
const [
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const [input, setInput] = useState('');
|
|
10
|
+
const [cursor, setCursor] = useState(0);
|
|
11
|
+
const inputRef = useRef('');
|
|
12
|
+
inputRef.current = input;
|
|
13
|
+
|
|
14
|
+
const { cols } = getLayout();
|
|
15
|
+
const w = Math.max(10, cols - 6);
|
|
16
|
+
const lineCount = Math.max(1, Math.ceil((input.slice(0, cursor).length + 1) / w));
|
|
17
|
+
const visibleLines = Math.min(lineCount, MAX_VISIBLE_ROWS);
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
const s = buf.current;
|
|
19
|
+
const modelShort = model.replace(/^[^/]+\//, '').slice(0, 18);
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
+
useInput((ch, key) => {
|
|
22
|
+
if (key.ctrl && key.p) { onSlash(); return; }
|
|
23
|
+
if (key.escape) { onSubmit('/exit'); return; }
|
|
24
|
+
if (key.return && !key.shift) {
|
|
25
|
+
if (input.trim()) {
|
|
26
|
+
const text = input;
|
|
27
|
+
setInput('');
|
|
28
|
+
setCursor(0);
|
|
29
|
+
onSubmit(text);
|
|
30
|
+
}
|
|
21
31
|
return;
|
|
22
32
|
}
|
|
23
|
-
|
|
24
33
|
if (key.return && key.shift) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
s.lines.splice(s.line + 1, 0, rest);
|
|
28
|
-
s.line++; s.col = 0;
|
|
29
|
-
setLines([...s.lines]); setCursorLine(s.line); setCursorCol(0);
|
|
34
|
+
setInput(prev => prev.slice(0, cursor) + '\n' + prev.slice(cursor));
|
|
35
|
+
setCursor(c => c + 1);
|
|
30
36
|
return;
|
|
31
37
|
}
|
|
32
|
-
|
|
33
38
|
if (key.backspace || key.delete) {
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
} else if (s.line > 0) {
|
|
38
|
-
s.col = s.lines[s.line - 1].length;
|
|
39
|
-
s.lines[s.line - 1] += s.lines[s.line];
|
|
40
|
-
s.lines.splice(s.line, 1);
|
|
41
|
-
s.line--;
|
|
39
|
+
if (cursor > 0) {
|
|
40
|
+
setInput(prev => prev.slice(0, cursor - 1) + prev.slice(cursor));
|
|
41
|
+
setCursor(c => c - 1);
|
|
42
42
|
}
|
|
43
|
-
setLines([...s.lines]); setCursorLine(s.line); setCursorCol(s.col);
|
|
44
43
|
return;
|
|
45
44
|
}
|
|
46
|
-
|
|
47
|
-
if (key.
|
|
48
|
-
if (key.
|
|
49
|
-
if (key.
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (input === '/' && s.lines.length === 1 && s.lines[0] === '') { onSlash?.(); return; }
|
|
54
|
-
s.lines[s.line] = s.lines[s.line].slice(0, s.col) + input + s.lines[s.line].slice(s.col);
|
|
55
|
-
s.col++;
|
|
56
|
-
setLines([...s.lines]); setCursorLine(s.line); setCursorCol(s.col);
|
|
45
|
+
if (key.leftArrow && cursor > 0) { setCursor(c => c - 1); return; }
|
|
46
|
+
if (key.rightArrow && cursor < input.length) { setCursor(c => c + 1); return; }
|
|
47
|
+
if (key.home) { setCursor(0); return; }
|
|
48
|
+
if (key.end) { setCursor(input.length); return; }
|
|
49
|
+
if (ch && ch.length === 1 && ch.charCodeAt(0) >= 32) {
|
|
50
|
+
setInput(prev => prev.slice(0, cursor) + ch + prev.slice(cursor));
|
|
51
|
+
setCursor(c => c + 1);
|
|
57
52
|
}
|
|
58
53
|
});
|
|
59
54
|
|
|
60
|
-
const
|
|
61
|
-
const
|
|
55
|
+
const displayText = input || (thinking ? '' : 'Type a message...');
|
|
56
|
+
const isPlaceholder = !input && !thinking;
|
|
57
|
+
|
|
58
|
+
const rows = [];
|
|
59
|
+
rows.push(
|
|
60
|
+
Box({ key: 'dock_header', height: 1, backgroundColor: hex.surfaceAlt },
|
|
61
|
+
Text({ color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' ' + u.h.repeat(Math.max(0, cols - 2)))
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const inputRows = [];
|
|
66
|
+
for (let i = 0; i < visibleLines; i++) {
|
|
67
|
+
const start = i * w;
|
|
68
|
+
const end = start + w;
|
|
69
|
+
const seg = displayText.slice(start, end);
|
|
70
|
+
inputRows.push(
|
|
71
|
+
Box({ key: 'in' + i, height: 1, backgroundColor: hex.bg },
|
|
72
|
+
Text({
|
|
73
|
+
color: isPlaceholder ? hex.textMuted : hex.text,
|
|
74
|
+
backgroundColor: hex.bg,
|
|
75
|
+
wrap: 'truncate-end',
|
|
76
|
+
}, ' ' + usym.triR2 + ' ' + (seg || ' '))
|
|
77
|
+
)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
62
80
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
i === cursorLine ? '\u276F' : ' '
|
|
69
|
-
),
|
|
70
|
-
h(Text, { color: theme.text, backgroundColor: theme.surface, wrap: 'wrap' },
|
|
71
|
-
' ' + (line || ' ')
|
|
72
|
-
),
|
|
73
|
-
i === cursorLine
|
|
74
|
-
? h(Text, { color: theme.info, backgroundColor: theme.surface }, '\u258C')
|
|
75
|
-
: h(Text, { backgroundColor: theme.surface }, ' ')
|
|
76
|
-
)
|
|
81
|
+
for (let i = visibleLines; i < MAX_VISIBLE_ROWS + 1; i++) {
|
|
82
|
+
if (i === MAX_VISIBLE_ROWS + 0) break;
|
|
83
|
+
inputRows.push(
|
|
84
|
+
Box({ key: 'in_fill' + i, height: 1, backgroundColor: hex.bg },
|
|
85
|
+
Text({ color: hex.textMuted, backgroundColor: hex.bg }, ' ' + usym.lightV)
|
|
77
86
|
)
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
h(Text, { color: theme.textMuted }, '\u00B7'),
|
|
87
|
-
h(Text, { color: theme.textDim }, 'Ctrl+P'),
|
|
88
|
-
thinking
|
|
89
|
-
? h(Text, { color: theme.warning }, ' \u25CF thinking')
|
|
90
|
-
: null,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const statusLine = Box({ key: 'dock_status', height: 1, backgroundColor: hex.surfaceAlt },
|
|
91
|
+
Text({ color: hex.textMuted, backgroundColor: hex.surfaceAlt },
|
|
92
|
+
' ' + provider + ' ' + usym.midDot + ' ' + modelShort +
|
|
93
|
+
(agentMode ? ' ' + usym.midDot + ' Agent' : '') +
|
|
94
|
+
' ' + usym.midDot + ' Ctrl+P commands'
|
|
91
95
|
)
|
|
92
96
|
);
|
|
97
|
+
|
|
98
|
+
rows.push(...inputRows);
|
|
99
|
+
rows.push(statusLine);
|
|
100
|
+
|
|
101
|
+
return Box({ flexDirection: 'column', backgroundColor: hex.surfaceAlt },
|
|
102
|
+
...rows
|
|
103
|
+
);
|
|
93
104
|
}
|