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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clarity-ai",
3
- "version": "5.0.0",
4
- "description": "Premium OpenCode-style terminal AI agent — streaming, tools, multiline composer, virtual scroll, code blocks",
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, useRef, useEffect } from 'react';
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', alignItems: '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', alignItems: '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
- tokenCount: 0,
15
- idCounter: 0,
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
- if (err.type) {
55
- handleError(setState, err);
56
- } else {
57
- setState(s => ({
58
- ...s, thinking: false, streamContent: '',
59
- messages: [...s.messages, { id: nextId(), role: 'error', content: err.message || 'Unknown error' }],
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 > 10) {
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 toolCalls = null;
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
- toolCalls = event.calls;
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
- handleError(setState, event);
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') { await sleep(2000); return processStream(provider, model, history, agentMode, setState, onStreamContent, depth); }
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, streaming: false }],
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 (toolCalls && toolCalls.length > 0 && agentMode) {
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 (const tc of toolCalls) {
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
- const result = await executeTool(name, args);
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
- { id: nextId(), role: 'assistant', content: buffer || '', toolCalls, streaming: false },
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: toolCalls.map(tc => ({
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',
@@ -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, { justifyContent: 'center', marginBottom: 0 },
9
- h(Gradient, { name: 'cristal' },
10
- h(BigText, { text: 'CLARITY', font: 'block' })
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, termWidth }) {
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 lineNumWidth = String(lines.length).length;
19
+ const lnW = String(lines.length).length;
19
20
 
20
- return h(Box, { flexDirection: 'column', marginY: 1, marginLeft: 2 },
21
- h(Box, { flexDirection: 'row' },
22
- h(Box, { backgroundColor: '#1C1C1C', paddingX: 1 },
23
- h(Text, { color: langColor, bold: true }, ' ' + lang + ' '),
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: '#0D1117', paddingY: 0 },
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: '#555', backgroundColor: '#0D1117' },
31
- ' ' + String(i + 1).padStart(lineNumWidth) + ' '
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: '#0D1117', wrap: 'truncate-end' },
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(query) || c.desc.toLowerCase().includes(query.toLowerCase())
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].name);
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
- return h(Box, { flexDirection: 'column', paddingX: 1, borderStyle: 'round', borderColor: '#333' },
31
- h(Text, { color: '#00D4FF', bold: true }, ' Commands'),
32
- h(Text, { color: '#333' }, ''),
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, { key: cmd.name, flexDirection: 'row', gap: 2 },
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 ? '#FF6B6B' : '#F0F0F0',
51
+ color: i === idx ? theme.selectionText : theme.text,
37
52
  bold: i === idx,
38
- backgroundColor: i === idx ? '#2A2A2A' : undefined,
39
- }, ' ' + cmd.name),
40
- h(Text, { color: '#555' }, cmd.desc)
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: '#333' }, ''),
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
  }