clarity-ai 5.0.0 → 6.0.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 +10 -4
- package/src/app.js +5 -4
- package/src/chat.js +134 -46
- package/src/components/Banner.js +16 -6
- package/src/components/CodeBlock.js +12 -13
- package/src/components/CommandPicker.js +32 -13
- package/src/components/Composer.js +54 -112
- package/src/components/LoadingIndicator.js +6 -3
- package/src/components/MessageBubble.js +124 -77
- package/src/components/MessageList.js +93 -84
- package/src/components/ModelPicker.js +35 -15
- package/src/components/ThinkingBlock.js +12 -13
- package/src/components/ToolCard.js +15 -26
- package/src/config/theme.js +57 -0
- package/src/providers/streaming.js +71 -48
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clarity-ai",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Premium OpenCode-style terminal AI agent —
|
|
3
|
+
"version": "6.0.0",
|
|
4
|
+
"description": "Premium OpenCode-style terminal AI agent — 24-bit TrueColor theme, 8s timeout recovery, virtual scroll, inline tool trees, collapsible thought cards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"clarity": "bin/clarity.js"
|
|
@@ -15,11 +15,17 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"ink": "^5",
|
|
17
17
|
"react": "^18",
|
|
18
|
-
"ink-text-input": "^6.0.0",
|
|
19
18
|
"ink-spinner": "^5",
|
|
20
19
|
"ink-big-text": "^2",
|
|
21
20
|
"ink-gradient": "^3",
|
|
22
21
|
"marked": "^12",
|
|
23
|
-
"cli-highlight": "^2"
|
|
22
|
+
"cli-highlight": "^2",
|
|
23
|
+
"chalk": "^5",
|
|
24
|
+
"ansi-escapes": "^7",
|
|
25
|
+
"cli-cursor": "^5",
|
|
26
|
+
"wrap-ansi": "^9",
|
|
27
|
+
"strip-ansi": "^7",
|
|
28
|
+
"string-width": "^7",
|
|
29
|
+
"picocolors": "^1"
|
|
24
30
|
}
|
|
25
31
|
}
|
package/src/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useCallback
|
|
1
|
+
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';
|
|
@@ -17,7 +17,6 @@ export function App({ config }) {
|
|
|
17
17
|
const [showCommands, setShowCommands] = useState(false);
|
|
18
18
|
const [showModels, setShowModels] = useState(false);
|
|
19
19
|
const [showBanner, setShowBanner] = useState(true);
|
|
20
|
-
const streamRef = useRef('');
|
|
21
20
|
|
|
22
21
|
const onSubmit = useCallback(async (input) => {
|
|
23
22
|
if (input.startsWith('/')) {
|
|
@@ -53,10 +52,12 @@ export function App({ config }) {
|
|
|
53
52
|
messages: state.messages,
|
|
54
53
|
thinking: state.thinking,
|
|
55
54
|
streamContent,
|
|
55
|
+
agentStatus: state.agentStatus,
|
|
56
|
+
toolExecutions: state.toolExecutions,
|
|
56
57
|
})
|
|
57
58
|
),
|
|
58
59
|
showCommands
|
|
59
|
-
? h(Box, { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center'
|
|
60
|
+
? h(Box, { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center' },
|
|
60
61
|
h(Box, { backgroundColor: '#0A0A0A', borderStyle: 'round', borderColor: '#333', paddingX: 2, paddingY: 1 },
|
|
61
62
|
h(CommandPicker, {
|
|
62
63
|
query: '',
|
|
@@ -67,7 +68,7 @@ export function App({ config }) {
|
|
|
67
68
|
)
|
|
68
69
|
: null,
|
|
69
70
|
showModels
|
|
70
|
-
? h(Box, { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center'
|
|
71
|
+
? h(Box, { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center' },
|
|
71
72
|
h(Box, { backgroundColor: '#0A0A0A', borderStyle: 'round', borderColor: '#333', paddingX: 2, paddingY: 1 },
|
|
72
73
|
h(ModelPicker, {
|
|
73
74
|
onSelect: handleModelSelect,
|
package/src/chat.js
CHANGED
|
@@ -11,13 +11,16 @@ export function createChatState() {
|
|
|
11
11
|
awaitingKey: false,
|
|
12
12
|
blockedProvider: null,
|
|
13
13
|
agentMode: true,
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
agentStatus: '',
|
|
15
|
+
toolExecutions: [],
|
|
16
|
+
thoughtTimer: null,
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
let msgId = 0;
|
|
21
|
+
let execId = 0;
|
|
20
22
|
function nextId() { return 'm' + (++msgId); }
|
|
23
|
+
function nextExecId() { return 'x' + (++execId); }
|
|
21
24
|
|
|
22
25
|
export async function handleSend(state, setState, input, model, provider, onStreamContent) {
|
|
23
26
|
if (!input.trim() || state.awaitingKey) return;
|
|
@@ -28,6 +31,9 @@ export async function handleSend(state, setState, input, model, provider, onStre
|
|
|
28
31
|
messages: [...s.messages, userMsg],
|
|
29
32
|
thinking: true,
|
|
30
33
|
streamContent: '',
|
|
34
|
+
agentStatus: 'Processing...',
|
|
35
|
+
toolExecutions: [],
|
|
36
|
+
thoughtTimer: Date.now(),
|
|
31
37
|
}));
|
|
32
38
|
onStreamContent('');
|
|
33
39
|
|
|
@@ -37,9 +43,7 @@ export async function handleSend(state, setState, input, model, provider, onStre
|
|
|
37
43
|
role: m.role === 'error' ? 'assistant' : m.role,
|
|
38
44
|
content: m.content,
|
|
39
45
|
};
|
|
40
|
-
if (m.role === 'tool' && m.tool_call_id)
|
|
41
|
-
base.tool_call_id = m.tool_call_id;
|
|
42
|
-
}
|
|
46
|
+
if (m.role === 'tool' && m.tool_call_id) base.tool_call_id = m.tool_call_id;
|
|
43
47
|
if (m.role === 'assistant' && m.toolCalls) {
|
|
44
48
|
base.tool_calls = m.toolCalls.map(tc => ({
|
|
45
49
|
id: tc.id, type: 'function',
|
|
@@ -51,21 +55,23 @@ export async function handleSend(state, setState, input, model, provider, onStre
|
|
|
51
55
|
|
|
52
56
|
await processStream(provider, model, history, state.agentMode, setState, onStreamContent);
|
|
53
57
|
} catch (err) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
}));
|
|
62
68
|
onStreamContent('');
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
async function processStream(provider, model, history, agentMode, setState, onStreamContent, depth = 0) {
|
|
67
|
-
if (depth >
|
|
68
|
-
setState(s => ({ ...s, thinking: false, streamContent: '' }));
|
|
73
|
+
if (depth > 8) {
|
|
74
|
+
setState(s => ({ ...s, thinking: false, streamContent: '', agentStatus: '', toolExecutions: [], thoughtTimer: null }));
|
|
69
75
|
onStreamContent('');
|
|
70
76
|
return;
|
|
71
77
|
}
|
|
@@ -79,63 +85,156 @@ async function processStream(provider, model, history, agentMode, setState, onSt
|
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
let buffer = '';
|
|
82
|
-
let
|
|
88
|
+
let toolCallsData = null;
|
|
89
|
+
let thoughtTime = Date.now();
|
|
90
|
+
let timedOut = false;
|
|
83
91
|
|
|
84
92
|
try {
|
|
85
93
|
for await (const event of stream) {
|
|
86
94
|
if (event.type === 'token') {
|
|
87
95
|
buffer += event.content;
|
|
88
96
|
onStreamContent(buffer);
|
|
97
|
+
setState(s => ({ ...s, agentStatus: 'Writing response...' }));
|
|
89
98
|
} else if (event.type === 'tool_calls') {
|
|
90
|
-
|
|
99
|
+
if (!timedOut) toolCallsData = event.calls;
|
|
100
|
+
} else if (event.type === 'done') {
|
|
101
|
+
} else if (event.type === 'timeout') {
|
|
102
|
+
timedOut = true;
|
|
103
|
+
setState(s => ({
|
|
104
|
+
...s, agentStatus: 'Stalled — recovering...',
|
|
105
|
+
messages: [...s.messages, { id: nextId(), role: 'system', content: 'Response timed out — showing partial result.' }],
|
|
106
|
+
}));
|
|
91
107
|
} else if (event.type === 'error') {
|
|
92
|
-
|
|
108
|
+
setState(s => ({ ...s, thinking: false, streamContent: '', agentStatus: '', toolExecutions: [], thoughtTimer: null }));
|
|
109
|
+
setState(s => ({
|
|
110
|
+
...s,
|
|
111
|
+
messages: [...s.messages, { id: nextId(), role: 'error', content: event.hint || event.message }],
|
|
112
|
+
}));
|
|
93
113
|
onStreamContent('');
|
|
94
114
|
return;
|
|
95
115
|
}
|
|
96
116
|
}
|
|
97
117
|
} catch (err) {
|
|
98
|
-
if (err.type === 'rate_limit') {
|
|
118
|
+
if (err.type === 'rate_limit') {
|
|
119
|
+
await sleep(2000);
|
|
120
|
+
setState(s => ({ ...s, agentStatus: 'Retrying after rate limit...' }));
|
|
121
|
+
return processStream(provider, model, history, agentMode, setState, onStreamContent, depth);
|
|
122
|
+
}
|
|
99
123
|
throw err;
|
|
100
124
|
}
|
|
101
125
|
|
|
126
|
+
const elapsed = Date.now() - thoughtTime;
|
|
127
|
+
|
|
102
128
|
if (buffer) {
|
|
103
129
|
setState(s => ({
|
|
104
130
|
...s,
|
|
105
|
-
messages: [...s.messages, { id: nextId(), role: 'assistant', content: buffer,
|
|
131
|
+
messages: [...s.messages, { id: nextId(), role: 'assistant', content: buffer, duration: elapsed }],
|
|
106
132
|
streamContent: '',
|
|
133
|
+
agentStatus: '',
|
|
107
134
|
}));
|
|
108
135
|
onStreamContent('');
|
|
109
136
|
} else {
|
|
110
|
-
setState(s => ({ ...s, streamContent: '' }));
|
|
137
|
+
setState(s => ({ ...s, streamContent: '', agentStatus: '' }));
|
|
111
138
|
onStreamContent('');
|
|
112
139
|
}
|
|
113
140
|
|
|
114
|
-
if (
|
|
141
|
+
if (timedOut) {
|
|
142
|
+
setState(s => ({ ...s, thinking: false, toolExecutions: [], thoughtTimer: null }));
|
|
143
|
+
onStreamContent('');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (toolCallsData && toolCallsData.length > 0 && agentMode) {
|
|
148
|
+
const execs = toolCallsData.map(tc => ({
|
|
149
|
+
execId: nextExecId(),
|
|
150
|
+
tcId: tc.id,
|
|
151
|
+
name: tc.function.name,
|
|
152
|
+
args: tc.function.arguments,
|
|
153
|
+
status: 'running',
|
|
154
|
+
startTime: Date.now(),
|
|
155
|
+
duration: 0,
|
|
156
|
+
result: '',
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
setState(s => ({
|
|
160
|
+
...s,
|
|
161
|
+
toolExecutions: execs,
|
|
162
|
+
agentStatus: 'Running tools...',
|
|
163
|
+
}));
|
|
164
|
+
|
|
115
165
|
const toolResults = [];
|
|
116
|
-
for (
|
|
166
|
+
for (let i = 0; i < toolCallsData.length; i++) {
|
|
167
|
+
const tc = toolCallsData[i];
|
|
117
168
|
const { name, arguments: argsStr } = tc.function;
|
|
118
169
|
let args;
|
|
119
170
|
try { args = JSON.parse(argsStr); } catch { args = {}; }
|
|
120
|
-
|
|
171
|
+
|
|
172
|
+
setState(s => ({
|
|
173
|
+
...s,
|
|
174
|
+
agentStatus: '' + name + '(' + JSON.stringify(args).slice(0, 60) + ')',
|
|
175
|
+
toolExecutions: s.toolExecutions.map(x =>
|
|
176
|
+
x.execId === execs[i].execId ? { ...x, status: 'running' } : x
|
|
177
|
+
),
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
let result, error;
|
|
181
|
+
const toolStart = Date.now();
|
|
182
|
+
try {
|
|
183
|
+
result = await executeTool(name, args);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
error = e.message;
|
|
186
|
+
result = 'Error: ' + e.message;
|
|
187
|
+
}
|
|
188
|
+
const toolDuration = Date.now() - toolStart;
|
|
189
|
+
|
|
121
190
|
toolResults.push({ tool_call_id: tc.id, role: 'tool', content: result, name });
|
|
191
|
+
|
|
192
|
+
setState(s => ({
|
|
193
|
+
...s,
|
|
194
|
+
toolExecutions: s.toolExecutions.map(x =>
|
|
195
|
+
x.execId === execs[i].execId
|
|
196
|
+
? { ...x, status: error ? 'failed' : 'completed', duration: toolDuration, result, error }
|
|
197
|
+
: x
|
|
198
|
+
),
|
|
199
|
+
}));
|
|
122
200
|
}
|
|
123
201
|
|
|
202
|
+
setState(s => ({
|
|
203
|
+
...s,
|
|
204
|
+
agentStatus: 'Processing results...',
|
|
205
|
+
}));
|
|
206
|
+
|
|
207
|
+
const totalThoughtTime = Date.now() - thoughtTime;
|
|
208
|
+
|
|
124
209
|
setState(s => ({
|
|
125
210
|
...s,
|
|
126
211
|
messages: [
|
|
127
212
|
...s.messages,
|
|
128
|
-
{
|
|
213
|
+
{
|
|
214
|
+
id: nextId(),
|
|
215
|
+
role: 'assistant',
|
|
216
|
+
content: buffer || '',
|
|
217
|
+
toolCalls: toolCallsData,
|
|
218
|
+
toolResults: toolResults.map((tr, i) => ({
|
|
219
|
+
...tr,
|
|
220
|
+
execId: execs[i]?.execId,
|
|
221
|
+
duration: execs[i]?.duration,
|
|
222
|
+
status: execs[i]?.status,
|
|
223
|
+
})),
|
|
224
|
+
duration: totalThoughtTime,
|
|
225
|
+
},
|
|
129
226
|
...toolResults.map(tr => ({
|
|
130
227
|
id: nextId(), role: 'tool', content: tr.content,
|
|
131
228
|
tool_call_id: tr.tool_call_id, toolName: tr.name,
|
|
132
229
|
})),
|
|
133
230
|
],
|
|
231
|
+
toolExecutions: [],
|
|
232
|
+
agentStatus: '',
|
|
134
233
|
}));
|
|
135
234
|
|
|
136
235
|
const newHistory = [
|
|
137
236
|
...history,
|
|
138
|
-
{ role: 'assistant', content: buffer || null, tool_calls:
|
|
237
|
+
{ role: 'assistant', content: buffer || null, tool_calls: toolCallsData.map(tc => ({
|
|
139
238
|
id: tc.id, type: 'function',
|
|
140
239
|
function: { name: tc.function.name, arguments: tc.function.arguments },
|
|
141
240
|
}))},
|
|
@@ -143,21 +242,17 @@ async function processStream(provider, model, history, agentMode, setState, onSt
|
|
|
143
242
|
];
|
|
144
243
|
|
|
145
244
|
await processStream(provider, model, newHistory, agentMode, setState, onStreamContent, depth + 1);
|
|
245
|
+
} else {
|
|
246
|
+
setState(s => ({
|
|
247
|
+
...s,
|
|
248
|
+
thinking: false,
|
|
249
|
+
agentStatus: '',
|
|
250
|
+
toolExecutions: [],
|
|
251
|
+
thoughtTimer: null,
|
|
252
|
+
}));
|
|
146
253
|
}
|
|
147
254
|
}
|
|
148
255
|
|
|
149
|
-
function handleError(setState, err) {
|
|
150
|
-
setState(s => ({
|
|
151
|
-
...s, thinking: false, streamContent: '',
|
|
152
|
-
messages: [...s.messages, {
|
|
153
|
-
id: nextId(),
|
|
154
|
-
role: 'error',
|
|
155
|
-
content: err.hint || err.message,
|
|
156
|
-
type: err.type,
|
|
157
|
-
}],
|
|
158
|
-
}));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
256
|
export async function handleCommand(input, state, setState, modelSetter, providerSetter, model, provider) {
|
|
162
257
|
const parts = input.trim().split(/\s+/);
|
|
163
258
|
const cmd = parts[0];
|
|
@@ -185,13 +280,7 @@ export async function handleCommand(input, state, setState, modelSetter, provide
|
|
|
185
280
|
}));
|
|
186
281
|
break;
|
|
187
282
|
case '/clear':
|
|
188
|
-
setState(s => ({ ...s, messages: [], streamContent: '' }));
|
|
189
|
-
break;
|
|
190
|
-
case '/theme':
|
|
191
|
-
setState(s => ({
|
|
192
|
-
...s,
|
|
193
|
-
messages: [...s.messages, { id: nextId(), role: 'system', content: 'Themes: dark (only theme available)' }],
|
|
194
|
-
}));
|
|
283
|
+
setState(s => ({ ...s, messages: [], streamContent: '', toolExecutions: [], agentStatus: '' }));
|
|
195
284
|
break;
|
|
196
285
|
case '/export': {
|
|
197
286
|
const text = state.messages.map(m => '[' + m.role + '] ' + m.content).join('\n\n');
|
|
@@ -211,7 +300,6 @@ export async function handleCommand(input, state, setState, modelSetter, provide
|
|
|
211
300
|
'/provider Switch provider',
|
|
212
301
|
'/agent Toggle agent mode',
|
|
213
302
|
'/clear Clear conversation',
|
|
214
|
-
'/theme Change color theme',
|
|
215
303
|
'/export Export conversation',
|
|
216
304
|
'/help Show this help',
|
|
217
305
|
'/exit Exit CLARITY',
|
package/src/components/Banner.js
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Box } from 'ink';
|
|
3
|
-
import BigText from 'ink-big-text';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
4
3
|
import Gradient from 'ink-gradient';
|
|
4
|
+
import BigText from 'ink-big-text';
|
|
5
5
|
const { createElement: h } = React;
|
|
6
6
|
|
|
7
7
|
export function Banner() {
|
|
8
|
-
return h(Box, {
|
|
9
|
-
h(Gradient, { name: '
|
|
10
|
-
h(BigText, { text: 'CLARITY', font: '
|
|
11
|
-
)
|
|
8
|
+
return h(Box, { flexDirection: 'column', alignItems: 'center', marginTop: 1, marginBottom: 1 },
|
|
9
|
+
h(Gradient, { name: 'summer' },
|
|
10
|
+
h(BigText, { text: 'CLARITY', font: 'chrome', letterSpacing: 1 })
|
|
11
|
+
),
|
|
12
|
+
h(Box, { flexDirection: 'row', gap: 1 },
|
|
13
|
+
h(Text, { color: '#FF6B6B' }, '\u25C9'),
|
|
14
|
+
h(Text, { color: '#555' }, 'premium terminal AI'),
|
|
15
|
+
h(Text, { color: '#555' }, '\u00B7'),
|
|
16
|
+
h(Text, { color: '#00FF88' }, 'agent mode'),
|
|
17
|
+
h(Text, { color: '#555' }, '\u00B7'),
|
|
18
|
+
h(Text, { color: '#00D4FF' }, 'Ctrl+P commands'),
|
|
19
|
+
h(Text, { color: '#FF6B6B' }, '\u25C9'),
|
|
20
|
+
),
|
|
21
|
+
h(Text, { color: '#2A2A2A' }, '\u2501'.repeat(Math.min(process.stdout.columns || 80, 60))),
|
|
12
22
|
);
|
|
13
23
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { theme } from '../config/theme.js';
|
|
3
4
|
const { createElement: h } = React;
|
|
4
5
|
|
|
5
6
|
const LANG_COLORS = {
|
|
@@ -11,26 +12,24 @@ const LANG_COLORS = {
|
|
|
11
12
|
json: '#292929', yaml: '#CB171E', md: '#083FA1', sql: '#E38C00',
|
|
12
13
|
};
|
|
13
14
|
|
|
14
|
-
export function CodeBlock({ code, language
|
|
15
|
+
export function CodeBlock({ code, language }) {
|
|
15
16
|
const lang = language || 'code';
|
|
16
17
|
const lines = useMemo(() => String(code).split('\n'), [code]);
|
|
17
18
|
const langColor = LANG_COLORS[lang] || '#555';
|
|
18
|
-
const
|
|
19
|
+
const lnW = String(lines.length).length;
|
|
19
20
|
|
|
20
|
-
return h(Box, { flexDirection: 'column', marginY: 1, marginLeft:
|
|
21
|
-
h(Box, { flexDirection: 'row' },
|
|
22
|
-
h(
|
|
23
|
-
|
|
24
|
-
h(Text, { color: '#555' }, String(lines.length).padStart(3) + ' lines '),
|
|
25
|
-
)
|
|
21
|
+
return h(Box, { flexDirection: 'column', marginY: 1, marginLeft: 0 },
|
|
22
|
+
h(Box, { flexDirection: 'row', backgroundColor: theme.codeBg },
|
|
23
|
+
h(Text, { color: langColor, bold: true, backgroundColor: '#1C1C1C' }, ' ' + lang + ' '),
|
|
24
|
+
h(Text, { color: theme.textMuted, backgroundColor: '#1C1C1C' }, String(lines.length) + ' lines '),
|
|
26
25
|
),
|
|
27
|
-
h(Box, { flexDirection: 'column', backgroundColor:
|
|
26
|
+
h(Box, { flexDirection: 'column', backgroundColor: theme.codeBg },
|
|
28
27
|
lines.map((line, i) =>
|
|
29
|
-
h(Box, { key: i, flexDirection: 'row' },
|
|
30
|
-
h(Text, { color:
|
|
31
|
-
' ' + String(i + 1).padStart(
|
|
28
|
+
h(Box, { key: i, flexDirection: 'row', backgroundColor: theme.codeBg },
|
|
29
|
+
h(Text, { color: theme.textMuted, backgroundColor: theme.codeBg },
|
|
30
|
+
' ' + String(i + 1).padStart(lnW) + ' '
|
|
32
31
|
),
|
|
33
|
-
h(Text, { color: '#C9D1D9', backgroundColor:
|
|
32
|
+
h(Text, { color: '#C9D1D9', backgroundColor: theme.codeBg, wrap: 'truncate-end' },
|
|
34
33
|
line || ' '
|
|
35
34
|
)
|
|
36
35
|
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { theme } from '../config/theme.js';
|
|
3
4
|
const { createElement: h } = React;
|
|
4
5
|
|
|
5
6
|
const COMMANDS = [
|
|
@@ -15,32 +16,50 @@ const COMMANDS = [
|
|
|
15
16
|
];
|
|
16
17
|
|
|
17
18
|
export function CommandPicker({ query, onSelect, onClose }) {
|
|
19
|
+
const [search, setSearch] = useState('');
|
|
20
|
+
const [idx, setIdx] = useState(0);
|
|
21
|
+
|
|
18
22
|
const filtered = COMMANDS.filter(c =>
|
|
19
|
-
c.name.includes(
|
|
23
|
+
c.name.includes(search) || c.desc.toLowerCase().includes(search.toLowerCase())
|
|
20
24
|
);
|
|
21
|
-
const [idx, setIdx] = useState(0);
|
|
22
25
|
|
|
23
26
|
useInput((input, key) => {
|
|
24
27
|
if (key.upArrow) setIdx(i => Math.max(0, i - 1));
|
|
25
28
|
if (key.downArrow) setIdx(i => Math.min(filtered.length - 1, i + 1));
|
|
26
|
-
if (key.return) onSelect(filtered[idx]
|
|
29
|
+
if (key.return) onSelect(filtered[idx]?.name || '');
|
|
27
30
|
if (key.escape) onClose();
|
|
31
|
+
if (key.backspace) setSearch(s => s.slice(0, -1));
|
|
32
|
+
else if (input && !key.ctrl && !key.meta) setSearch(s => s + input);
|
|
28
33
|
});
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
const tw = process.stdout.columns || 80;
|
|
36
|
+
const boxWidth = Math.min(tw - 4, 50);
|
|
37
|
+
|
|
38
|
+
return h(Box, { flexDirection: 'column', width: boxWidth },
|
|
39
|
+
h(Box, { flexDirection: 'row', marginBottom: 1, gap: 1 },
|
|
40
|
+
h(Text, { color: theme.textMuted }, '\u2315'),
|
|
41
|
+
h(Text, { color: search ? theme.text : theme.textMuted }, search || 'type to filter...'),
|
|
42
|
+
),
|
|
33
43
|
filtered.map((cmd, i) =>
|
|
34
|
-
h(Box, {
|
|
44
|
+
h(Box, {
|
|
45
|
+
key: cmd.name,
|
|
46
|
+
flexDirection: 'row',
|
|
47
|
+
backgroundColor: i === idx ? theme.selectionBg : undefined,
|
|
48
|
+
width: boxWidth,
|
|
49
|
+
},
|
|
35
50
|
h(Text, {
|
|
36
|
-
color: i === idx ?
|
|
51
|
+
color: i === idx ? theme.selectionText : theme.text,
|
|
37
52
|
bold: i === idx,
|
|
38
|
-
backgroundColor: i === idx ?
|
|
39
|
-
|
|
40
|
-
|
|
53
|
+
backgroundColor: i === idx ? theme.selectionBg : undefined,
|
|
54
|
+
wrap: 'truncate-end',
|
|
55
|
+
}, ' ' + cmd.name.padEnd(16)),
|
|
56
|
+
h(Text, {
|
|
57
|
+
color: i === idx ? theme.selectionText : theme.textDim,
|
|
58
|
+
backgroundColor: i === idx ? theme.selectionBg : undefined,
|
|
59
|
+
wrap: 'truncate-end',
|
|
60
|
+
}, cmd.desc)
|
|
41
61
|
)
|
|
42
62
|
),
|
|
43
|
-
h(Text, { color:
|
|
44
|
-
h(Text, { color: '#555' }, ' \u2191\u2193 navigate \u23CE select Esc close')
|
|
63
|
+
h(Text, { color: theme.textMuted }, ' \u2191\u2193 nav \u23CE select Esc close')
|
|
45
64
|
);
|
|
46
65
|
}
|