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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clarity-ai",
3
- "version": "4.2.0",
4
- "description": "Premium terminal AI chat for Termux — OpenCode-style UI with prompt box, bg colors, side lines",
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 [model, setModel] = useState(config.model || 'groq/llama-3.3-70b-versatile');
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
- const stream = callAI(provider, model, history, { tools: state.agentMode ? TOOLS : undefined });
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: 2, paddingY: 1 },
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 ? '#00FFD1' : '#F0F0F0',
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(filtered.length - 1, i + 1));
17
- if (key.return) { onSelect(filtered[idx]?.id); return; }
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: 2, paddingY: 1 },
24
- h(Text, { color: search ? '#F0F0F0' : '#555555' }, 'Search: ' + (search || '')),
25
- filtered.map((m, i) =>
26
- h(Box, { key: m.id, justifyContent: 'space-between' },
27
- h(Text, {
28
- color: i === idx ? '#00FFD1' : '#F0F0F0',
29
- bold: i === idx,
30
- }, ' ' + m.id),
31
- m.badge ? h(Text, { color: '#555555' }, '[' + m.badge + ']') : null
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 w = process.stdout.columns || 80;
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(Text, null,
22
- h(Text, { color: '#333333' }, '\u2502 '),
23
- h(Text, { color: '#00FFFF' }, '\u276f '),
24
- h(Text, { color: '#555555' }, provider + '/' + model + ' '),
25
- h(Text, { color: agentMode ? '#00FF9F' : '#555555' }, agentMode ? '\u2714 agent' : '\u2716 agent'),
26
- h(Text, { color: '#333333' }, ' '.repeat(Math.max(1, w - (provider + '/' + model).length - 18)) + '\u2502')
27
- ),
28
- h(Text, { color: '#333333' },
29
- '\u2514' + '\u2500'.repeat(Math.max(0, w - 2)) + '\u2518'
30
- ),
31
- h(Box, null,
32
- h(Text, { color: '#00FFFF' }, ' \u276f '),
33
- h(TextInput, {
34
- value,
35
- onChange: handleChange,
36
- onSubmit: handleSubmit,
37
- placeholder: thinking ? 'Thinking...' : 'Ask anything or type / for commands',
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 res = await fetch(endpoint, {
3
- method: 'POST',
4
- headers: {
5
- 'Content-Type': 'application/json',
6
- 'Authorization': 'Bearer ' + apiKey,
7
- ...extraHeaders,
8
- },
9
- body: JSON.stringify({ ...body, stream: true }),
10
- });
11
-
12
- if (!res.ok) {
13
- const text = await res.text();
14
- const { parseErrorResponse } = await import('./errors.js');
15
- throw parseErrorResponse(res, text);
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
- const reader = res.body.getReader();
19
- const decoder = new TextDecoder();
20
- let buffer = '';
21
-
22
- while (true) {
23
- const { done, value } = await reader.read();
24
- if (done) break;
25
- buffer += decoder.decode(value, { stream: true });
26
- const lines = buffer.split('\n');
27
- buffer = lines.pop() || '';
28
- for (const line of lines) {
29
- if (line.startsWith('data: ')) {
30
- const data = line.slice(6).trim();
31
- if (data === '[DONE]') return;
32
- try {
33
- const json = JSON.parse(data);
34
- const delta = json.choices?.[0]?.delta?.content;
35
- if (delta) yield { type: 'token', content: delta };
36
- if (json.choices?.[0]?.finish_reason) yield { type: 'done', reason: json.choices[0].finish_reason };
37
- } catch {}
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
  }