clarity-ai 4.3.1 → 5.1.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/package.json +2 -2
- package/src/app.js +32 -20
- package/src/chat.js +159 -88
- package/src/components/CodeBlock.js +34 -7
- package/src/components/CommandPicker.js +10 -7
- package/src/components/Composer.js +152 -0
- package/src/components/MessageBubble.js +134 -36
- package/src/components/MessageList.js +107 -9
- package/src/components/ModelPicker.js +25 -34
- package/src/components/ThinkingBlock.js +30 -0
- package/src/components/ToolCard.js +42 -0
- package/src/providers/streaming.js +37 -4
- package/src/components/InputArea.js +0 -33
- package/src/components/PromptBox.js +0 -43
- package/src/components/StatusBar.js +0 -22
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clarity-ai",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Premium terminal AI agent
|
|
3
|
+
"version": "5.1.0",
|
|
4
|
+
"description": "Premium OpenCode-style terminal AI agent — streaming, tools, multiline composer, virtual scroll, code blocks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"clarity": "bin/clarity.js"
|
package/src/app.js
CHANGED
|
@@ -2,22 +2,21 @@ import React, { useState, useCallback } from 'react';
|
|
|
2
2
|
import { Box } from 'ink';
|
|
3
3
|
import { Banner } from './components/Banner.js';
|
|
4
4
|
import { MessageList } from './components/MessageList.js';
|
|
5
|
-
import {
|
|
5
|
+
import { Composer } from './components/Composer.js';
|
|
6
6
|
import { CommandPicker } from './components/CommandPicker.js';
|
|
7
7
|
import { ModelPicker } from './components/ModelPicker.js';
|
|
8
|
-
import { useScroll } from './hooks/useScroll.js';
|
|
9
8
|
import { createChatState, handleSend, handleCommand } from './chat.js';
|
|
10
9
|
const { createElement: h } = React;
|
|
11
10
|
|
|
12
11
|
export function App({ config }) {
|
|
13
|
-
const [state, setState] = useState(createChatState);
|
|
12
|
+
const [state, setState] = useState(() => createChatState());
|
|
13
|
+
const [streamContent, setStreamContent] = useState('');
|
|
14
14
|
const defaultModel = (config.model || 'groq/llama-3.3-70b-versatile').replace(/^[^/]+\//, '');
|
|
15
15
|
const [model, setModel] = useState(defaultModel);
|
|
16
16
|
const [provider, setProvider] = useState(config.provider || 'groq');
|
|
17
17
|
const [showCommands, setShowCommands] = useState(false);
|
|
18
18
|
const [showModels, setShowModels] = useState(false);
|
|
19
19
|
const [showBanner, setShowBanner] = useState(true);
|
|
20
|
-
const { scrollOffset, termHeight } = useScroll(state.messages.length);
|
|
21
20
|
|
|
22
21
|
const onSubmit = useCallback(async (input) => {
|
|
23
22
|
if (input.startsWith('/')) {
|
|
@@ -27,12 +26,12 @@ export function App({ config }) {
|
|
|
27
26
|
return;
|
|
28
27
|
}
|
|
29
28
|
if (showBanner) setShowBanner(false);
|
|
30
|
-
await handleSend(state, setState, input, model, provider);
|
|
31
|
-
}, [state,
|
|
29
|
+
await handleSend(state, setState, input, model, provider, setStreamContent);
|
|
30
|
+
}, [state, model, provider, showBanner]);
|
|
32
31
|
|
|
33
|
-
function handleCommandSelect(
|
|
32
|
+
function handleCommandSelect(cmdName) {
|
|
34
33
|
setShowCommands(false);
|
|
35
|
-
onSubmit(
|
|
34
|
+
onSubmit(cmdName);
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
function handleModelSelect(modelId) {
|
|
@@ -52,20 +51,33 @@ export function App({ config }) {
|
|
|
52
51
|
h(MessageList, {
|
|
53
52
|
messages: state.messages,
|
|
54
53
|
thinking: state.thinking,
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
streamContent,
|
|
55
|
+
agentStatus: state.agentStatus,
|
|
56
|
+
toolExecutions: state.toolExecutions,
|
|
57
57
|
})
|
|
58
58
|
),
|
|
59
|
-
showCommands
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
showCommands
|
|
60
|
+
? h(Box, { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center' },
|
|
61
|
+
h(Box, { backgroundColor: '#0A0A0A', borderStyle: 'round', borderColor: '#333', paddingX: 2, paddingY: 1 },
|
|
62
|
+
h(CommandPicker, {
|
|
63
|
+
query: '',
|
|
64
|
+
onSelect: handleCommandSelect,
|
|
65
|
+
onClose: () => setShowCommands(false),
|
|
66
|
+
})
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
: null,
|
|
70
|
+
showModels
|
|
71
|
+
? h(Box, { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center' },
|
|
72
|
+
h(Box, { backgroundColor: '#0A0A0A', borderStyle: 'round', borderColor: '#333', paddingX: 2, paddingY: 1 },
|
|
73
|
+
h(ModelPicker, {
|
|
74
|
+
onSelect: handleModelSelect,
|
|
75
|
+
onClose: () => setShowModels(false),
|
|
76
|
+
})
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
: null,
|
|
80
|
+
h(Composer, {
|
|
69
81
|
provider,
|
|
70
82
|
model,
|
|
71
83
|
agentMode: state.agentMode,
|
package/src/chat.js
CHANGED
|
@@ -1,31 +1,41 @@
|
|
|
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 { extractCommandFromText } from './intentDetect.js';
|
|
5
|
-
|
|
6
4
|
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
7
5
|
|
|
8
6
|
export function createChatState() {
|
|
9
7
|
return {
|
|
10
8
|
messages: [],
|
|
11
9
|
thinking: false,
|
|
12
|
-
|
|
10
|
+
streamContent: '',
|
|
13
11
|
awaitingKey: false,
|
|
14
12
|
blockedProvider: null,
|
|
15
13
|
agentMode: true,
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
agentStatus: '',
|
|
15
|
+
toolExecutions: [],
|
|
16
|
+
thoughtTimer: null,
|
|
18
17
|
};
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
let msgId = 0;
|
|
21
|
+
let execId = 0;
|
|
22
22
|
function nextId() { return 'm' + (++msgId); }
|
|
23
|
+
function nextExecId() { return 'x' + (++execId); }
|
|
23
24
|
|
|
24
|
-
export async function handleSend(state, setState, input, model, provider) {
|
|
25
|
+
export async function handleSend(state, setState, input, model, provider, onStreamContent) {
|
|
25
26
|
if (!input.trim() || state.awaitingKey) return;
|
|
26
27
|
|
|
27
28
|
const userMsg = { id: nextId(), role: 'user', content: input };
|
|
28
|
-
setState(s => ({
|
|
29
|
+
setState(s => ({
|
|
30
|
+
...s,
|
|
31
|
+
messages: [...s.messages, userMsg],
|
|
32
|
+
thinking: true,
|
|
33
|
+
streamContent: '',
|
|
34
|
+
agentStatus: 'Processing...',
|
|
35
|
+
toolExecutions: [],
|
|
36
|
+
thoughtTimer: Date.now(),
|
|
37
|
+
}));
|
|
38
|
+
onStreamContent('');
|
|
29
39
|
|
|
30
40
|
try {
|
|
31
41
|
const history = [...state.messages, userMsg].map(m => {
|
|
@@ -33,35 +43,36 @@ export async function handleSend(state, setState, input, model, provider) {
|
|
|
33
43
|
role: m.role === 'error' ? 'assistant' : m.role,
|
|
34
44
|
content: m.content,
|
|
35
45
|
};
|
|
36
|
-
if (m.role === 'tool' && m.tool_call_id)
|
|
37
|
-
base.tool_call_id = m.tool_call_id;
|
|
38
|
-
}
|
|
46
|
+
if (m.role === 'tool' && m.tool_call_id) base.tool_call_id = m.tool_call_id;
|
|
39
47
|
if (m.role === 'assistant' && m.toolCalls) {
|
|
40
48
|
base.tool_calls = m.toolCalls.map(tc => ({
|
|
41
|
-
id: tc.id,
|
|
42
|
-
type: 'function',
|
|
49
|
+
id: tc.id, type: 'function',
|
|
43
50
|
function: { name: tc.function.name, arguments: tc.function.arguments },
|
|
44
51
|
}));
|
|
45
52
|
}
|
|
46
53
|
return base;
|
|
47
54
|
});
|
|
48
55
|
|
|
49
|
-
await processStream(provider, model, history, state.agentMode, setState);
|
|
56
|
+
await processStream(provider, model, history, state.agentMode, setState, onStreamContent);
|
|
50
57
|
} catch (err) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
const thoughtTime = state.thoughtTimer ? Date.now() - state.thoughtTimer : 0;
|
|
59
|
+
setState(s => ({
|
|
60
|
+
...s, thinking: false, streamContent: '', agentStatus: '', toolExecutions: [], thoughtTimer: null,
|
|
61
|
+
messages: [...s.messages, {
|
|
62
|
+
id: nextId(), role: 'error',
|
|
63
|
+
content: err.hint || err.message || 'Request failed',
|
|
64
|
+
type: err.type,
|
|
65
|
+
duration: thoughtTime,
|
|
66
|
+
}],
|
|
67
|
+
}));
|
|
68
|
+
onStreamContent('');
|
|
59
69
|
}
|
|
60
70
|
}
|
|
61
71
|
|
|
62
|
-
async function processStream(provider, model, history, agentMode, setState, depth = 0) {
|
|
63
|
-
if (depth >
|
|
64
|
-
setState(s => ({ ...s, thinking: false,
|
|
72
|
+
async function processStream(provider, model, history, agentMode, setState, onStreamContent, depth = 0) {
|
|
73
|
+
if (depth > 8) {
|
|
74
|
+
setState(s => ({ ...s, thinking: false, streamContent: '', agentStatus: '', toolExecutions: [], thoughtTimer: null }));
|
|
75
|
+
onStreamContent('');
|
|
65
76
|
return;
|
|
66
77
|
}
|
|
67
78
|
|
|
@@ -69,102 +80,169 @@ async function processStream(provider, model, history, agentMode, setState, dept
|
|
|
69
80
|
try {
|
|
70
81
|
stream = callAI(provider, model, history, { tools: agentMode ? TOOLS : undefined });
|
|
71
82
|
} catch (err) {
|
|
72
|
-
if (err.type === 'rate_limit') {
|
|
73
|
-
await sleep(2000);
|
|
74
|
-
return processStream(provider, model, history, agentMode, setState, depth);
|
|
75
|
-
}
|
|
83
|
+
if (err.type === 'rate_limit') { await sleep(2000); return processStream(provider, model, history, agentMode, setState, onStreamContent, depth); }
|
|
76
84
|
throw err;
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
let buffer = '';
|
|
80
|
-
let
|
|
88
|
+
let toolCallsData = null;
|
|
89
|
+
let thoughtTime = Date.now();
|
|
81
90
|
|
|
82
91
|
try {
|
|
83
92
|
for await (const event of stream) {
|
|
84
93
|
if (event.type === 'token') {
|
|
85
94
|
buffer += event.content;
|
|
95
|
+
onStreamContent(buffer);
|
|
96
|
+
setState(s => ({ ...s, agentStatus: 'Writing response...' }));
|
|
97
|
+
} else if (event.type === 'tool_calls') {
|
|
98
|
+
toolCallsData = event.calls;
|
|
99
|
+
} else if (event.type === 'done') {
|
|
100
|
+
} else if (event.type === 'timeout') {
|
|
86
101
|
setState(s => ({
|
|
87
|
-
...s,
|
|
88
|
-
|
|
89
|
-
messages: updateLastAssistant(s.messages, buffer),
|
|
102
|
+
...s, agentStatus: 'Stream stalled, completing...',
|
|
103
|
+
messages: [...s.messages, { id: nextId(), role: 'system', content: 'Stream timeout — response may be incomplete' }],
|
|
90
104
|
}));
|
|
91
|
-
} else if (event.type === 'tool_calls') {
|
|
92
|
-
toolCalls = event.calls;
|
|
93
105
|
} else if (event.type === 'error') {
|
|
94
|
-
|
|
106
|
+
setState(s => ({ ...s, thinking: false, streamContent: '', agentStatus: '', toolExecutions: [], thoughtTimer: null }));
|
|
107
|
+
setState(s => ({
|
|
108
|
+
...s,
|
|
109
|
+
messages: [...s.messages, { id: nextId(), role: 'error', content: event.hint || event.message }],
|
|
110
|
+
}));
|
|
111
|
+
onStreamContent('');
|
|
95
112
|
return;
|
|
96
113
|
}
|
|
97
114
|
}
|
|
98
115
|
} catch (err) {
|
|
99
116
|
if (err.type === 'rate_limit') {
|
|
100
117
|
await sleep(2000);
|
|
101
|
-
|
|
118
|
+
setState(s => ({ ...s, agentStatus: 'Retrying after rate limit...' }));
|
|
119
|
+
return processStream(provider, model, history, agentMode, setState, onStreamContent, depth);
|
|
102
120
|
}
|
|
103
121
|
throw err;
|
|
104
122
|
}
|
|
105
123
|
|
|
106
|
-
|
|
124
|
+
const elapsed = Date.now() - thoughtTime;
|
|
125
|
+
|
|
126
|
+
if (buffer) {
|
|
127
|
+
setState(s => ({
|
|
128
|
+
...s,
|
|
129
|
+
messages: [...s.messages, { id: nextId(), role: 'assistant', content: buffer, duration: elapsed }],
|
|
130
|
+
streamContent: '',
|
|
131
|
+
agentStatus: '',
|
|
132
|
+
}));
|
|
133
|
+
onStreamContent('');
|
|
134
|
+
} else {
|
|
135
|
+
setState(s => ({ ...s, streamContent: '', agentStatus: '' }));
|
|
136
|
+
onStreamContent('');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (toolCallsData && toolCallsData.length > 0 && agentMode) {
|
|
140
|
+
const execs = toolCallsData.map(tc => ({
|
|
141
|
+
execId: nextExecId(),
|
|
142
|
+
tcId: tc.id,
|
|
143
|
+
name: tc.function.name,
|
|
144
|
+
args: tc.function.arguments,
|
|
145
|
+
status: 'running',
|
|
146
|
+
startTime: Date.now(),
|
|
147
|
+
duration: 0,
|
|
148
|
+
result: '',
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
setState(s => ({
|
|
152
|
+
...s,
|
|
153
|
+
toolExecutions: execs,
|
|
154
|
+
agentStatus: 'Running tools...',
|
|
155
|
+
}));
|
|
156
|
+
|
|
107
157
|
const toolResults = [];
|
|
108
|
-
for (
|
|
158
|
+
for (let i = 0; i < toolCallsData.length; i++) {
|
|
159
|
+
const tc = toolCallsData[i];
|
|
109
160
|
const { name, arguments: argsStr } = tc.function;
|
|
110
161
|
let args;
|
|
111
162
|
try { args = JSON.parse(argsStr); } catch { args = {}; }
|
|
112
|
-
|
|
163
|
+
|
|
164
|
+
setState(s => ({
|
|
165
|
+
...s,
|
|
166
|
+
agentStatus: '' + name + '(' + JSON.stringify(args).slice(0, 60) + ')',
|
|
167
|
+
toolExecutions: s.toolExecutions.map(x =>
|
|
168
|
+
x.execId === execs[i].execId ? { ...x, status: 'running' } : x
|
|
169
|
+
),
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
let result, error;
|
|
173
|
+
const toolStart = Date.now();
|
|
174
|
+
try {
|
|
175
|
+
result = await executeTool(name, args);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
error = e.message;
|
|
178
|
+
result = 'Error: ' + e.message;
|
|
179
|
+
}
|
|
180
|
+
const toolDuration = Date.now() - toolStart;
|
|
181
|
+
|
|
113
182
|
toolResults.push({ tool_call_id: tc.id, role: 'tool', content: result, name });
|
|
183
|
+
|
|
184
|
+
setState(s => ({
|
|
185
|
+
...s,
|
|
186
|
+
toolExecutions: s.toolExecutions.map(x =>
|
|
187
|
+
x.execId === execs[i].execId
|
|
188
|
+
? { ...x, status: error ? 'failed' : 'completed', duration: toolDuration, result, error }
|
|
189
|
+
: x
|
|
190
|
+
),
|
|
191
|
+
}));
|
|
114
192
|
}
|
|
115
193
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
tool_call_id: tr.tool_call_id, toolName: tr.name,
|
|
194
|
+
setState(s => ({
|
|
195
|
+
...s,
|
|
196
|
+
agentStatus: 'Processing results...',
|
|
120
197
|
}));
|
|
121
198
|
|
|
122
|
-
|
|
199
|
+
const totalThoughtTime = Date.now() - thoughtTime;
|
|
200
|
+
|
|
201
|
+
setState(s => ({
|
|
202
|
+
...s,
|
|
203
|
+
messages: [
|
|
204
|
+
...s.messages,
|
|
205
|
+
{
|
|
206
|
+
id: nextId(),
|
|
207
|
+
role: 'assistant',
|
|
208
|
+
content: buffer || '',
|
|
209
|
+
toolCalls: toolCallsData,
|
|
210
|
+
toolResults: toolResults.map((tr, i) => ({
|
|
211
|
+
...tr,
|
|
212
|
+
execId: execs[i]?.execId,
|
|
213
|
+
duration: execs[i]?.duration,
|
|
214
|
+
status: execs[i]?.status,
|
|
215
|
+
})),
|
|
216
|
+
duration: totalThoughtTime,
|
|
217
|
+
},
|
|
218
|
+
...toolResults.map(tr => ({
|
|
219
|
+
id: nextId(), role: 'tool', content: tr.content,
|
|
220
|
+
tool_call_id: tr.tool_call_id, toolName: tr.name,
|
|
221
|
+
})),
|
|
222
|
+
],
|
|
223
|
+
toolExecutions: [],
|
|
224
|
+
agentStatus: '',
|
|
225
|
+
}));
|
|
123
226
|
|
|
124
227
|
const newHistory = [
|
|
125
228
|
...history,
|
|
126
|
-
{ role: 'assistant', content: buffer || null, tool_calls:
|
|
127
|
-
id: tc.id,
|
|
128
|
-
type: 'function',
|
|
229
|
+
{ role: 'assistant', content: buffer || null, tool_calls: toolCallsData.map(tc => ({
|
|
230
|
+
id: tc.id, type: 'function',
|
|
129
231
|
function: { name: tc.function.name, arguments: tc.function.arguments },
|
|
130
232
|
}))},
|
|
131
233
|
...toolResults,
|
|
132
234
|
];
|
|
133
235
|
|
|
134
|
-
await processStream(provider, model, newHistory, agentMode, setState, depth + 1);
|
|
236
|
+
await processStream(provider, model, newHistory, agentMode, setState, onStreamContent, depth + 1);
|
|
135
237
|
} else {
|
|
136
|
-
setState(s => ({ ...s, thinking: false, streamBuffer: '' }));
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function handleError(setState, err) {
|
|
141
|
-
if (err.type === 'auth_error') {
|
|
142
|
-
setState(s => ({
|
|
143
|
-
...s, thinking: false, awaitingKey: true, blockedProvider: err.provider,
|
|
144
|
-
messages: [...s.messages, { id: nextId(), role: 'error', content: err.hint || err.message }],
|
|
145
|
-
}));
|
|
146
|
-
} else if (err.type === 'rate_limit') {
|
|
147
238
|
setState(s => ({
|
|
148
|
-
...s,
|
|
149
|
-
|
|
239
|
+
...s,
|
|
240
|
+
thinking: false,
|
|
241
|
+
agentStatus: '',
|
|
242
|
+
toolExecutions: [],
|
|
243
|
+
thoughtTimer: null,
|
|
150
244
|
}));
|
|
151
|
-
} else {
|
|
152
|
-
setState(s => ({
|
|
153
|
-
...s, thinking: false,
|
|
154
|
-
messages: [...s.messages, { id: nextId(), role: 'error', content: err.message }],
|
|
155
|
-
}));
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function updateLastAssistant(messages, buffer) {
|
|
160
|
-
const copy = [...messages];
|
|
161
|
-
const last = copy[copy.length - 1];
|
|
162
|
-
if (last && last.role === 'assistant') {
|
|
163
|
-
copy[copy.length - 1] = { ...last, content: buffer, streaming: true };
|
|
164
|
-
} else {
|
|
165
|
-
copy.push({ id: nextId(), role: 'assistant', content: buffer, streaming: true });
|
|
166
245
|
}
|
|
167
|
-
return copy;
|
|
168
246
|
}
|
|
169
247
|
|
|
170
248
|
export async function handleCommand(input, state, setState, modelSetter, providerSetter, model, provider) {
|
|
@@ -194,22 +272,16 @@ export async function handleCommand(input, state, setState, modelSetter, provide
|
|
|
194
272
|
}));
|
|
195
273
|
break;
|
|
196
274
|
case '/clear':
|
|
197
|
-
setState(s => ({ ...s, messages: [],
|
|
198
|
-
break;
|
|
199
|
-
case '/theme':
|
|
200
|
-
setState(s => ({
|
|
201
|
-
...s,
|
|
202
|
-
messages: [...s.messages, { id: nextId(), role: 'system', content: 'Themes: dark (only theme available)' }],
|
|
203
|
-
}));
|
|
275
|
+
setState(s => ({ ...s, messages: [], streamContent: '', toolExecutions: [], agentStatus: '' }));
|
|
204
276
|
break;
|
|
205
277
|
case '/export': {
|
|
206
278
|
const text = state.messages.map(m => '[' + m.role + '] ' + m.content).join('\n\n');
|
|
207
279
|
const { writeFileSync } = await import('fs');
|
|
208
|
-
const
|
|
209
|
-
writeFileSync(
|
|
280
|
+
const fpath = 'clarity-export-' + Date.now() + '.md';
|
|
281
|
+
writeFileSync(fpath, text);
|
|
210
282
|
setState(s => ({
|
|
211
283
|
...s,
|
|
212
|
-
messages: [...s.messages, { id: nextId(), role: 'system', content: 'Exported to ' +
|
|
284
|
+
messages: [...s.messages, { id: nextId(), role: 'system', content: 'Exported to ' + fpath }],
|
|
213
285
|
}));
|
|
214
286
|
break;
|
|
215
287
|
}
|
|
@@ -220,7 +292,6 @@ export async function handleCommand(input, state, setState, modelSetter, provide
|
|
|
220
292
|
'/provider Switch provider',
|
|
221
293
|
'/agent Toggle agent mode',
|
|
222
294
|
'/clear Clear conversation',
|
|
223
|
-
'/theme Change color theme',
|
|
224
295
|
'/export Export conversation',
|
|
225
296
|
'/help Show this help',
|
|
226
297
|
'/exit Exit CLARITY',
|
|
@@ -1,13 +1,40 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import highlight from 'cli-highlight';
|
|
4
3
|
const { createElement: h } = React;
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const LANG_COLORS = {
|
|
6
|
+
js: '#F0DB4F', jsx: '#F0DB4F', ts: '#3178C6', tsx: '#3178C6',
|
|
7
|
+
py: '#3572A5', rb: '#CC342D', go: '#00ADD8', rs: '#DEA584',
|
|
8
|
+
java: '#B07219', kt: '#7F52FF', swift: '#FFAC45',
|
|
9
|
+
html: '#E34F26', css: '#1572B6', scss: '#CC6699',
|
|
10
|
+
sh: '#89E051', bash: '#89E051', dockerfile: '#384D54',
|
|
11
|
+
json: '#292929', yaml: '#CB171E', md: '#083FA1', sql: '#E38C00',
|
|
12
|
+
};
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
export function CodeBlock({ code, language, termWidth }) {
|
|
15
|
+
const lang = language || 'code';
|
|
16
|
+
const lines = useMemo(() => String(code).split('\n'), [code]);
|
|
17
|
+
const langColor = LANG_COLORS[lang] || '#555';
|
|
18
|
+
const lineNumWidth = String(lines.length).length;
|
|
19
|
+
|
|
20
|
+
return h(Box, { flexDirection: 'column', marginY: 1, marginLeft: 2 },
|
|
21
|
+
h(Box, { flexDirection: 'row' },
|
|
22
|
+
h(Box, { backgroundColor: '#1C1C1C', paddingX: 1 },
|
|
23
|
+
h(Text, { color: langColor, bold: true }, ' ' + lang + ' '),
|
|
24
|
+
h(Text, { color: '#555' }, String(lines.length).padStart(3) + ' lines '),
|
|
25
|
+
)
|
|
26
|
+
),
|
|
27
|
+
h(Box, { flexDirection: 'column', backgroundColor: '#0D1117', paddingY: 0 },
|
|
28
|
+
lines.map((line, i) =>
|
|
29
|
+
h(Box, { key: i, flexDirection: 'row' },
|
|
30
|
+
h(Text, { color: '#555', backgroundColor: '#0D1117' },
|
|
31
|
+
' ' + String(i + 1).padStart(lineNumWidth) + ' '
|
|
32
|
+
),
|
|
33
|
+
h(Text, { color: '#C9D1D9', backgroundColor: '#0D1117', wrap: 'truncate-end' },
|
|
34
|
+
line || ' '
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
)
|
|
12
39
|
);
|
|
13
40
|
}
|
|
@@ -23,21 +23,24 @@ export function CommandPicker({ query, onSelect, onClose }) {
|
|
|
23
23
|
useInput((input, key) => {
|
|
24
24
|
if (key.upArrow) setIdx(i => Math.max(0, i - 1));
|
|
25
25
|
if (key.downArrow) setIdx(i => Math.min(filtered.length - 1, i + 1));
|
|
26
|
-
if (key.return) onSelect(filtered[idx]);
|
|
26
|
+
if (key.return) onSelect(filtered[idx].name);
|
|
27
27
|
if (key.escape) onClose();
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
return h(Box, { flexDirection: 'column', paddingX: 1 },
|
|
31
|
-
h(Text, { color: '#00D4FF', bold: true }, 'Commands'),
|
|
32
|
-
h(Text, { color: '#
|
|
30
|
+
return h(Box, { flexDirection: 'column', paddingX: 1, borderStyle: 'round', borderColor: '#333' },
|
|
31
|
+
h(Text, { color: '#00D4FF', bold: true }, ' Commands'),
|
|
32
|
+
h(Text, { color: '#333' }, ''),
|
|
33
33
|
filtered.map((cmd, i) =>
|
|
34
34
|
h(Box, { key: cmd.name, flexDirection: 'row', gap: 2 },
|
|
35
35
|
h(Text, {
|
|
36
36
|
color: i === idx ? '#FF6B6B' : '#F0F0F0',
|
|
37
37
|
bold: i === idx,
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
backgroundColor: i === idx ? '#2A2A2A' : undefined,
|
|
39
|
+
}, ' ' + cmd.name),
|
|
40
|
+
h(Text, { color: '#555' }, cmd.desc)
|
|
40
41
|
)
|
|
41
|
-
)
|
|
42
|
+
),
|
|
43
|
+
h(Text, { color: '#333' }, ''),
|
|
44
|
+
h(Text, { color: '#555' }, ' \u2191\u2193 navigate \u23CE select Esc close')
|
|
42
45
|
);
|
|
43
46
|
}
|