clarity-ai 4.2.0 → 4.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/package.json +2 -2
- package/src/app.js +3 -2
- package/src/chat.js +65 -21
- package/src/components/CommandPicker.js +6 -5
- package/src/components/ModelPicker.js +49 -17
- package/src/components/PromptBox.js +21 -19
- package/src/providers/streaming.js +87 -35
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clarity-ai",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "Premium terminal AI
|
|
3
|
+
"version": "4.3.0",
|
|
4
|
+
"description": "Premium terminal AI agent for Termux — OpenCode-style UI, streaming, markdown, tools, agent mode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"clarity": "bin/clarity.js"
|
package/src/app.js
CHANGED
|
@@ -11,7 +11,8 @@ const { createElement: h } = React;
|
|
|
11
11
|
|
|
12
12
|
export function App({ config }) {
|
|
13
13
|
const [state, setState] = useState(createChatState);
|
|
14
|
-
const
|
|
14
|
+
const defaultModel = (config.model || 'groq/llama-3.3-70b-versatile').replace(/^[^/]+\//, '');
|
|
15
|
+
const [model, setModel] = useState(defaultModel);
|
|
15
16
|
const [provider, setProvider] = useState(config.provider || 'groq');
|
|
16
17
|
const [showCommands, setShowCommands] = useState(false);
|
|
17
18
|
const [showModels, setShowModels] = useState(false);
|
|
@@ -37,7 +38,7 @@ export function App({ config }) {
|
|
|
37
38
|
function handleModelSelect(modelId) {
|
|
38
39
|
const p = modelId.split('/')[0];
|
|
39
40
|
setProvider(p);
|
|
40
|
-
setModel(modelId);
|
|
41
|
+
setModel(modelId.replace(/^[^/]+\//, ''));
|
|
41
42
|
setShowModels(false);
|
|
42
43
|
setState(s => ({
|
|
43
44
|
...s,
|
package/src/chat.js
CHANGED
|
@@ -31,27 +31,7 @@ export async function handleSend(state, setState, input, model, provider) {
|
|
|
31
31
|
content: m.content,
|
|
32
32
|
}));
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
let buffer = '';
|
|
36
|
-
|
|
37
|
-
for await (const event of stream) {
|
|
38
|
-
if (event.type === 'token') {
|
|
39
|
-
buffer += event.content;
|
|
40
|
-
setState(s => ({
|
|
41
|
-
...s,
|
|
42
|
-
streamBuffer: buffer,
|
|
43
|
-
messages: updateLastAssistant(s.messages, buffer),
|
|
44
|
-
}));
|
|
45
|
-
}
|
|
46
|
-
if (event.type === 'error') {
|
|
47
|
-
handleError(setState, event);
|
|
48
|
-
break;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (buffer) {
|
|
53
|
-
setState(s => ({ ...s, thinking: false, streamBuffer: '' }));
|
|
54
|
-
}
|
|
34
|
+
await processStream(provider, model, history, state.agentMode, setState);
|
|
55
35
|
} catch (err) {
|
|
56
36
|
if (err.type) {
|
|
57
37
|
handleError(setState, err);
|
|
@@ -64,6 +44,70 @@ export async function handleSend(state, setState, input, model, provider) {
|
|
|
64
44
|
}
|
|
65
45
|
}
|
|
66
46
|
|
|
47
|
+
async function processStream(provider, model, history, agentMode, setState, depth = 0) {
|
|
48
|
+
if (depth > 10) {
|
|
49
|
+
setState(s => ({ ...s, thinking: false, streamBuffer: '' }));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const stream = callAI(provider, model, history, { tools: agentMode ? TOOLS : undefined });
|
|
54
|
+
let buffer = '';
|
|
55
|
+
let toolCalls = null;
|
|
56
|
+
|
|
57
|
+
for await (const event of stream) {
|
|
58
|
+
if (event.type === 'token') {
|
|
59
|
+
buffer += event.content;
|
|
60
|
+
setState(s => ({
|
|
61
|
+
...s,
|
|
62
|
+
streamBuffer: buffer,
|
|
63
|
+
messages: updateLastAssistant(s.messages, buffer),
|
|
64
|
+
}));
|
|
65
|
+
} else if (event.type === 'tool_calls') {
|
|
66
|
+
toolCalls = event.calls;
|
|
67
|
+
} else if (event.type === 'error') {
|
|
68
|
+
handleError(setState, event);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (toolCalls && toolCalls.length > 0 && agentMode) {
|
|
74
|
+
const assistantMsg = { id: nextId(), role: 'assistant', content: buffer || '', toolCalls };
|
|
75
|
+
const toolResults = [];
|
|
76
|
+
|
|
77
|
+
for (const tc of toolCalls) {
|
|
78
|
+
const { name, arguments: argsStr } = tc.function;
|
|
79
|
+
let args;
|
|
80
|
+
try {
|
|
81
|
+
args = JSON.parse(argsStr);
|
|
82
|
+
} catch {
|
|
83
|
+
args = {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setState(s => ({
|
|
87
|
+
...s,
|
|
88
|
+
messages: [...s.messages, { id: nextId(), role: 'tool', content: '⚙ ' + name + '(' + JSON.stringify(args).slice(0, 100) + ')...', toolName: name }],
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
const result = await executeTool(name, args);
|
|
92
|
+
toolResults.push({ tool_call_id: tc.id, role: 'tool', content: result });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const newHistory = [
|
|
96
|
+
...history,
|
|
97
|
+
{ role: 'assistant', content: buffer || null, tool_calls: toolCalls.map(tc => ({
|
|
98
|
+
id: tc.id,
|
|
99
|
+
type: 'function',
|
|
100
|
+
function: { name: tc.function.name, arguments: tc.function.arguments },
|
|
101
|
+
}))},
|
|
102
|
+
...toolResults,
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
await processStream(provider, model, newHistory, agentMode, setState, depth + 1);
|
|
106
|
+
} else {
|
|
107
|
+
setState(s => ({ ...s, thinking: false, streamBuffer: '' }));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
67
111
|
function handleError(setState, err) {
|
|
68
112
|
if (err.type === 'auth_error') {
|
|
69
113
|
setState(s => ({
|
|
@@ -27,14 +27,15 @@ export function CommandPicker({ query, onSelect, onClose }) {
|
|
|
27
27
|
if (key.escape) onClose();
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
return h(Box, { flexDirection: 'column', paddingX:
|
|
30
|
+
return h(Box, { flexDirection: 'column', paddingX: 1 },
|
|
31
|
+
h(Text, { color: '#00D4FF', bold: true }, 'Commands'),
|
|
32
|
+
h(Text, { color: '#333333' }, '\u2500'.repeat(30)),
|
|
31
33
|
filtered.map((cmd, i) =>
|
|
32
|
-
h(Box, { key: cmd.name },
|
|
34
|
+
h(Box, { key: cmd.name, flexDirection: 'row', gap: 2 },
|
|
33
35
|
h(Text, {
|
|
34
|
-
color: i === idx ? '#
|
|
36
|
+
color: i === idx ? '#FF6B6B' : '#F0F0F0',
|
|
35
37
|
bold: i === idx,
|
|
36
|
-
},
|
|
37
|
-
' ' + cmd.name.padEnd(18)),
|
|
38
|
+
}, '\u25B6 ' + cmd.name),
|
|
38
39
|
h(Text, { color: '#555555' }, cmd.desc)
|
|
39
40
|
)
|
|
40
41
|
)
|
|
@@ -5,31 +5,63 @@ const { createElement: h } = React;
|
|
|
5
5
|
|
|
6
6
|
export function ModelPicker({ onSelect, onClose }) {
|
|
7
7
|
const [search, setSearch] = useState('');
|
|
8
|
-
const filtered = useMemo(() =>
|
|
9
|
-
ALL_MODELS.filter(m => m.id.toLowerCase().includes(search.toLowerCase())),
|
|
10
|
-
[search]
|
|
11
|
-
);
|
|
12
8
|
const [idx, setIdx] = useState(0);
|
|
13
9
|
|
|
10
|
+
const grouped = useMemo(() => {
|
|
11
|
+
const filtered = ALL_MODELS.filter(m =>
|
|
12
|
+
m.id.toLowerCase().includes(search.toLowerCase()) ||
|
|
13
|
+
m.label.toLowerCase().includes(search.toLowerCase())
|
|
14
|
+
);
|
|
15
|
+
const groups = {};
|
|
16
|
+
for (const m of filtered) {
|
|
17
|
+
if (!groups[m.provider]) groups[m.provider] = [];
|
|
18
|
+
groups[m.provider].push(m);
|
|
19
|
+
}
|
|
20
|
+
return groups;
|
|
21
|
+
}, [search]);
|
|
22
|
+
|
|
23
|
+
const flatList = useMemo(() => {
|
|
24
|
+
const list = [];
|
|
25
|
+
for (const [provider, models] of Object.entries(grouped)) {
|
|
26
|
+
for (const m of models) {
|
|
27
|
+
list.push({ ...m, providerHeader: list.length === 0 || list[list.length - 1]?.provider !== provider });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return list;
|
|
31
|
+
}, [grouped]);
|
|
32
|
+
|
|
14
33
|
useInput((input, key) => {
|
|
15
34
|
if (key.upArrow) setIdx(i => Math.max(0, i - 1));
|
|
16
|
-
if (key.downArrow) setIdx(i => Math.min(
|
|
17
|
-
if (key.return) { onSelect(
|
|
35
|
+
if (key.downArrow) setIdx(i => Math.min(flatList.length - 1, i + 1));
|
|
36
|
+
if (key.return) { onSelect(flatList[idx]?.id); return; }
|
|
18
37
|
if (key.escape) onClose();
|
|
19
38
|
if (key.backspace) setSearch(s => s.slice(0, -1));
|
|
20
39
|
else if (input && !key.ctrl && !key.meta) setSearch(s => s + input);
|
|
21
40
|
});
|
|
22
41
|
|
|
23
|
-
return h(Box, { flexDirection: 'column', paddingX:
|
|
24
|
-
h(Text, { color:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
return h(Box, { flexDirection: 'column', paddingX: 1 },
|
|
43
|
+
h(Text, { color: '#00D4FF', bold: true }, 'Select model'),
|
|
44
|
+
h(Text, { color: '#333333' }, '\u2500'.repeat(30)),
|
|
45
|
+
h(Box, { flexDirection: 'row', gap: 1 },
|
|
46
|
+
h(Text, { color: '#555555' }, 'Search:'),
|
|
47
|
+
h(Text, { color: search ? '#F0F0F0' : '#333333' }, search || '_')
|
|
48
|
+
),
|
|
49
|
+
h(Text, { color: '#333333' }, ''),
|
|
50
|
+
flatList.map((m, i) => {
|
|
51
|
+
const items = [];
|
|
52
|
+
if (m.providerHeader) {
|
|
53
|
+
items.push(h(Text, { key: 'h-' + m.provider, color: '#00D4FF', bold: true }, '\u25BC ' + m.provider.toUpperCase()));
|
|
54
|
+
}
|
|
55
|
+
items.push(
|
|
56
|
+
h(Box, { key: m.id, flexDirection: 'row', gap: 2 },
|
|
57
|
+
h(Text, {
|
|
58
|
+
color: i === idx ? '#FF6B6B' : '#F0F0F0',
|
|
59
|
+
bold: i === idx,
|
|
60
|
+
}, i === idx ? '\u25B6 ' : ' ' + m.label),
|
|
61
|
+
m.badge ? h(Text, { color: '#555555' }, '[' + m.badge + ']') : null
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
return items;
|
|
65
|
+
})
|
|
34
66
|
);
|
|
35
67
|
}
|
|
@@ -5,7 +5,7 @@ const { createElement: h } = React;
|
|
|
5
5
|
|
|
6
6
|
export function PromptBox({ provider, model, agentMode, thinking, onSlash, onSubmit }) {
|
|
7
7
|
const [value, setValue] = useState('');
|
|
8
|
-
const
|
|
8
|
+
const displayName = provider + '/' + model;
|
|
9
9
|
|
|
10
10
|
function handleChange(val) {
|
|
11
11
|
setValue(val);
|
|
@@ -18,24 +18,26 @@ export function PromptBox({ provider, model, agentMode, thinking, onSlash, onSub
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
return h(Box, { flexDirection: 'column', flexShrink: 0 },
|
|
21
|
-
h(
|
|
22
|
-
h(Text, { color: '#
|
|
23
|
-
h(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
h(Box, { flexDirection: 'row', gap: 1 },
|
|
22
|
+
h(Text, { color: '#00D4FF' }, '\u2502'),
|
|
23
|
+
h(Box, { flexDirection: 'column', flexGrow: 1 },
|
|
24
|
+
h(Box, { flexDirection: 'row', gap: 1 },
|
|
25
|
+
h(Text, { color: '#555555' }, displayName),
|
|
26
|
+
h(Text, { color: '#333333' }, ' \u00B7 '),
|
|
27
|
+
h(Text, { color: agentMode ? '#00FF88' : '#555555' }, agentMode ? 'agent:ON' : 'agent:OFF'),
|
|
28
|
+
h(Text, { color: '#333333' }, ' \u00B7 '),
|
|
29
|
+
h(Text, { color: '#555555' }, 'ctrl+p commands')
|
|
30
|
+
),
|
|
31
|
+
h(Box, { flexDirection: 'row', gap: 1 },
|
|
32
|
+
h(Text, { color: '#00D4FF' }, thinking ? ' \u25CF ' : ' \u276F '),
|
|
33
|
+
h(TextInput, {
|
|
34
|
+
value,
|
|
35
|
+
onChange: handleChange,
|
|
36
|
+
onSubmit: handleSubmit,
|
|
37
|
+
placeholder: thinking ? 'Thinking...' : 'Ask anything or type / for commands',
|
|
38
|
+
})
|
|
39
|
+
)
|
|
40
|
+
)
|
|
39
41
|
)
|
|
40
42
|
);
|
|
41
43
|
}
|
|
@@ -1,41 +1,93 @@
|
|
|
1
1
|
export async function* streamResponse(endpoint, body, apiKey, extraHeaders = {}) {
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
2
|
+
const controller = new AbortController();
|
|
3
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
const res = await fetch(endpoint, {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: {
|
|
9
|
+
'Content-Type': 'application/json',
|
|
10
|
+
'Authorization': 'Bearer ' + apiKey,
|
|
11
|
+
...extraHeaders,
|
|
12
|
+
},
|
|
13
|
+
body: JSON.stringify({ ...body, stream: true }),
|
|
14
|
+
signal: controller.signal,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
clearTimeout(timeoutId);
|
|
18
|
+
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
const text = await res.text();
|
|
21
|
+
const { parseErrorResponse } = await import('./errors.js');
|
|
22
|
+
throw parseErrorResponse(res, text);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const reader = res.body.getReader();
|
|
26
|
+
const decoder = new TextDecoder();
|
|
27
|
+
let buffer = '';
|
|
28
|
+
const toolCallsMap = new Map();
|
|
17
29
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
while (true) {
|
|
31
|
+
const { done, value } = await reader.read();
|
|
32
|
+
if (done) break;
|
|
33
|
+
buffer += decoder.decode(value, { stream: true });
|
|
34
|
+
const lines = buffer.split('\n');
|
|
35
|
+
buffer = lines.pop() || '';
|
|
36
|
+
for (const line of lines) {
|
|
37
|
+
if (line.startsWith('data: ')) {
|
|
38
|
+
const data = line.slice(6).trim();
|
|
39
|
+
if (data === '[DONE]') {
|
|
40
|
+
const toolCalls = Array.from(toolCallsMap.values());
|
|
41
|
+
if (toolCalls.length > 0) {
|
|
42
|
+
yield { type: 'tool_calls', calls: toolCalls };
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const json = JSON.parse(data);
|
|
48
|
+
const choice = json.choices?.[0];
|
|
49
|
+
if (!choice) continue;
|
|
50
|
+
|
|
51
|
+
const delta = choice.delta;
|
|
52
|
+
|
|
53
|
+
if (delta?.content) {
|
|
54
|
+
yield { type: 'token', content: delta.content };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (delta?.tool_calls) {
|
|
58
|
+
for (const tc of delta.tool_calls) {
|
|
59
|
+
const idx = tc.index ?? 0;
|
|
60
|
+
if (!toolCallsMap.has(idx)) {
|
|
61
|
+
toolCallsMap.set(idx, {
|
|
62
|
+
id: tc.id || 'call_' + Date.now() + '_' + idx,
|
|
63
|
+
type: 'function',
|
|
64
|
+
function: { name: '', arguments: '' },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const existing = toolCallsMap.get(idx);
|
|
68
|
+
if (tc.id) existing.id = tc.id;
|
|
69
|
+
if (tc.function?.name) existing.function.name += tc.function.name;
|
|
70
|
+
if (tc.function?.arguments) existing.function.arguments += tc.function.arguments;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (choice.finish_reason) {
|
|
75
|
+
const toolCalls = Array.from(toolCallsMap.values());
|
|
76
|
+
if (toolCalls.length > 0 && choice.finish_reason === 'tool_calls') {
|
|
77
|
+
yield { type: 'tool_calls', calls: toolCalls };
|
|
78
|
+
}
|
|
79
|
+
yield { type: 'done', reason: choice.finish_reason };
|
|
80
|
+
}
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
38
83
|
}
|
|
39
84
|
}
|
|
85
|
+
|
|
86
|
+
const toolCalls = Array.from(toolCallsMap.values());
|
|
87
|
+
if (toolCalls.length > 0) {
|
|
88
|
+
yield { type: 'tool_calls', calls: toolCalls };
|
|
89
|
+
}
|
|
90
|
+
} finally {
|
|
91
|
+
clearTimeout(timeoutId);
|
|
40
92
|
}
|
|
41
93
|
}
|