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 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.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 [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
@@ -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
- role: m.role === 'error' ? 'assistant' : m.role,
31
- content: m.content,
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
- const stream = callAI(provider, model, history, { tools: state.agentMode ? TOOLS : undefined });
35
- let buffer = '';
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
- if (event.type === 'error') {
91
+ } else if (event.type === 'tool_calls') {
92
+ toolCalls = event.calls;
93
+ } else if (event.type === 'error') {
47
94
  handleError(setState, event);
48
- break;
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
- handleError(setState, err);
58
- } else {
59
- setState(s => ({
60
- ...s, thinking: false,
61
- messages: [...s.messages, { id: nextId(), role: 'error', content: err.message || 'Unknown error' }],
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: 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
  }