clarity-ai 4.2.0 → 4.3.1
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 +93 -20
- 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.1",
|
|
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
|
@@ -3,6 +3,8 @@ import { setKey } from './config/keys.js';
|
|
|
3
3
|
import { TOOLS, executeTool } from './tools.js';
|
|
4
4
|
import { extractCommandFromText } from './intentDetect.js';
|
|
5
5
|
|
|
6
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
7
|
+
|
|
6
8
|
export function createChatState() {
|
|
7
9
|
return {
|
|
8
10
|
messages: [],
|
|
@@ -26,14 +28,58 @@ export async function handleSend(state, setState, input, model, provider) {
|
|
|
26
28
|
setState(s => ({ ...s, messages: [...s.messages, userMsg], thinking: true, streamBuffer: '' }));
|
|
27
29
|
|
|
28
30
|
try {
|
|
29
|
-
const history = [...state.messages, userMsg].map(m =>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
const history = [...state.messages, userMsg].map(m => {
|
|
32
|
+
const base = {
|
|
33
|
+
role: m.role === 'error' ? 'assistant' : m.role,
|
|
34
|
+
content: m.content,
|
|
35
|
+
};
|
|
36
|
+
if (m.role === 'tool' && m.tool_call_id) {
|
|
37
|
+
base.tool_call_id = m.tool_call_id;
|
|
38
|
+
}
|
|
39
|
+
if (m.role === 'assistant' && m.toolCalls) {
|
|
40
|
+
base.tool_calls = m.toolCalls.map(tc => ({
|
|
41
|
+
id: tc.id,
|
|
42
|
+
type: 'function',
|
|
43
|
+
function: { name: tc.function.name, arguments: tc.function.arguments },
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
return base;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await processStream(provider, model, history, state.agentMode, setState);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (err.type) {
|
|
52
|
+
handleError(setState, err);
|
|
53
|
+
} else {
|
|
54
|
+
setState(s => ({
|
|
55
|
+
...s, thinking: false,
|
|
56
|
+
messages: [...s.messages, { id: nextId(), role: 'error', content: err.message || 'Unknown error' }],
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function processStream(provider, model, history, agentMode, setState, depth = 0) {
|
|
63
|
+
if (depth > 10) {
|
|
64
|
+
setState(s => ({ ...s, thinking: false, streamBuffer: '' }));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let stream;
|
|
69
|
+
try {
|
|
70
|
+
stream = callAI(provider, model, history, { tools: agentMode ? TOOLS : undefined });
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (err.type === 'rate_limit') {
|
|
73
|
+
await sleep(2000);
|
|
74
|
+
return processStream(provider, model, history, agentMode, setState, depth);
|
|
75
|
+
}
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
33
78
|
|
|
34
|
-
|
|
35
|
-
|
|
79
|
+
let buffer = '';
|
|
80
|
+
let toolCalls = null;
|
|
36
81
|
|
|
82
|
+
try {
|
|
37
83
|
for await (const event of stream) {
|
|
38
84
|
if (event.type === 'token') {
|
|
39
85
|
buffer += event.content;
|
|
@@ -42,25 +88,52 @@ export async function handleSend(state, setState, input, model, provider) {
|
|
|
42
88
|
streamBuffer: buffer,
|
|
43
89
|
messages: updateLastAssistant(s.messages, buffer),
|
|
44
90
|
}));
|
|
45
|
-
}
|
|
46
|
-
|
|
91
|
+
} else if (event.type === 'tool_calls') {
|
|
92
|
+
toolCalls = event.calls;
|
|
93
|
+
} else if (event.type === 'error') {
|
|
47
94
|
handleError(setState, event);
|
|
48
|
-
|
|
95
|
+
return;
|
|
49
96
|
}
|
|
50
97
|
}
|
|
51
|
-
|
|
52
|
-
if (buffer) {
|
|
53
|
-
setState(s => ({ ...s, thinking: false, streamBuffer: '' }));
|
|
54
|
-
}
|
|
55
98
|
} catch (err) {
|
|
56
|
-
if (err.type) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
99
|
+
if (err.type === 'rate_limit') {
|
|
100
|
+
await sleep(2000);
|
|
101
|
+
return processStream(provider, model, history, agentMode, setState, depth);
|
|
102
|
+
}
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (toolCalls && toolCalls.length > 0 && agentMode) {
|
|
107
|
+
const toolResults = [];
|
|
108
|
+
for (const tc of toolCalls) {
|
|
109
|
+
const { name, arguments: argsStr } = tc.function;
|
|
110
|
+
let args;
|
|
111
|
+
try { args = JSON.parse(argsStr); } catch { args = {}; }
|
|
112
|
+
const result = await executeTool(name, args);
|
|
113
|
+
toolResults.push({ tool_call_id: tc.id, role: 'tool', content: result, name });
|
|
63
114
|
}
|
|
115
|
+
|
|
116
|
+
const newAssistantMsg = { id: nextId(), role: 'assistant', content: buffer || '', toolCalls };
|
|
117
|
+
const toolMsgs = toolResults.map(tr => ({
|
|
118
|
+
id: nextId(), role: 'tool', content: tr.content,
|
|
119
|
+
tool_call_id: tr.tool_call_id, toolName: tr.name,
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
setState(s => ({ ...s, messages: [...s.messages, newAssistantMsg, ...toolMsgs] }));
|
|
123
|
+
|
|
124
|
+
const newHistory = [
|
|
125
|
+
...history,
|
|
126
|
+
{ role: 'assistant', content: buffer || null, tool_calls: toolCalls.map(tc => ({
|
|
127
|
+
id: tc.id,
|
|
128
|
+
type: 'function',
|
|
129
|
+
function: { name: tc.function.name, arguments: tc.function.arguments },
|
|
130
|
+
}))},
|
|
131
|
+
...toolResults,
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
await processStream(provider, model, newHistory, agentMode, setState, depth + 1);
|
|
135
|
+
} else {
|
|
136
|
+
setState(s => ({ ...s, thinking: false, streamBuffer: '' }));
|
|
64
137
|
}
|
|
65
138
|
}
|
|
66
139
|
|
|
@@ -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
|
}
|