clarity-ai 5.0.0 → 5.1.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 +1 -1
- package/src/app.js +5 -4
- package/src/chat.js +126 -46
- package/src/components/MessageBubble.js +77 -41
- package/src/components/MessageList.js +87 -83
- package/src/providers/streaming.js +37 -4
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useCallback
|
|
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'
|
|
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'
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 >
|
|
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,148 @@ async function processStream(provider, model, history, agentMode, setState, onSt
|
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
let buffer = '';
|
|
82
|
-
let
|
|
88
|
+
let toolCallsData = null;
|
|
89
|
+
let thoughtTime = Date.now();
|
|
83
90
|
|
|
84
91
|
try {
|
|
85
92
|
for await (const event of stream) {
|
|
86
93
|
if (event.type === 'token') {
|
|
87
94
|
buffer += event.content;
|
|
88
95
|
onStreamContent(buffer);
|
|
96
|
+
setState(s => ({ ...s, agentStatus: 'Writing response...' }));
|
|
89
97
|
} else if (event.type === 'tool_calls') {
|
|
90
|
-
|
|
98
|
+
toolCallsData = event.calls;
|
|
99
|
+
} else if (event.type === 'done') {
|
|
100
|
+
} else if (event.type === 'timeout') {
|
|
101
|
+
setState(s => ({
|
|
102
|
+
...s, agentStatus: 'Stream stalled, completing...',
|
|
103
|
+
messages: [...s.messages, { id: nextId(), role: 'system', content: 'Stream timeout — response may be incomplete' }],
|
|
104
|
+
}));
|
|
91
105
|
} else if (event.type === 'error') {
|
|
92
|
-
|
|
106
|
+
setState(s => ({ ...s, thinking: false, streamContent: '', agentStatus: '', toolExecutions: [], thoughtTimer: null }));
|
|
107
|
+
setState(s => ({
|
|
108
|
+
...s,
|
|
109
|
+
messages: [...s.messages, { id: nextId(), role: 'error', content: event.hint || event.message }],
|
|
110
|
+
}));
|
|
93
111
|
onStreamContent('');
|
|
94
112
|
return;
|
|
95
113
|
}
|
|
96
114
|
}
|
|
97
115
|
} catch (err) {
|
|
98
|
-
if (err.type === 'rate_limit') {
|
|
116
|
+
if (err.type === 'rate_limit') {
|
|
117
|
+
await sleep(2000);
|
|
118
|
+
setState(s => ({ ...s, agentStatus: 'Retrying after rate limit...' }));
|
|
119
|
+
return processStream(provider, model, history, agentMode, setState, onStreamContent, depth);
|
|
120
|
+
}
|
|
99
121
|
throw err;
|
|
100
122
|
}
|
|
101
123
|
|
|
124
|
+
const elapsed = Date.now() - thoughtTime;
|
|
125
|
+
|
|
102
126
|
if (buffer) {
|
|
103
127
|
setState(s => ({
|
|
104
128
|
...s,
|
|
105
|
-
messages: [...s.messages, { id: nextId(), role: 'assistant', content: buffer,
|
|
129
|
+
messages: [...s.messages, { id: nextId(), role: 'assistant', content: buffer, duration: elapsed }],
|
|
106
130
|
streamContent: '',
|
|
131
|
+
agentStatus: '',
|
|
107
132
|
}));
|
|
108
133
|
onStreamContent('');
|
|
109
134
|
} else {
|
|
110
|
-
setState(s => ({ ...s, streamContent: '' }));
|
|
135
|
+
setState(s => ({ ...s, streamContent: '', agentStatus: '' }));
|
|
111
136
|
onStreamContent('');
|
|
112
137
|
}
|
|
113
138
|
|
|
114
|
-
if (
|
|
139
|
+
if (toolCallsData && toolCallsData.length > 0 && agentMode) {
|
|
140
|
+
const execs = toolCallsData.map(tc => ({
|
|
141
|
+
execId: nextExecId(),
|
|
142
|
+
tcId: tc.id,
|
|
143
|
+
name: tc.function.name,
|
|
144
|
+
args: tc.function.arguments,
|
|
145
|
+
status: 'running',
|
|
146
|
+
startTime: Date.now(),
|
|
147
|
+
duration: 0,
|
|
148
|
+
result: '',
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
setState(s => ({
|
|
152
|
+
...s,
|
|
153
|
+
toolExecutions: execs,
|
|
154
|
+
agentStatus: 'Running tools...',
|
|
155
|
+
}));
|
|
156
|
+
|
|
115
157
|
const toolResults = [];
|
|
116
|
-
for (
|
|
158
|
+
for (let i = 0; i < toolCallsData.length; i++) {
|
|
159
|
+
const tc = toolCallsData[i];
|
|
117
160
|
const { name, arguments: argsStr } = tc.function;
|
|
118
161
|
let args;
|
|
119
162
|
try { args = JSON.parse(argsStr); } catch { args = {}; }
|
|
120
|
-
|
|
163
|
+
|
|
164
|
+
setState(s => ({
|
|
165
|
+
...s,
|
|
166
|
+
agentStatus: '' + name + '(' + JSON.stringify(args).slice(0, 60) + ')',
|
|
167
|
+
toolExecutions: s.toolExecutions.map(x =>
|
|
168
|
+
x.execId === execs[i].execId ? { ...x, status: 'running' } : x
|
|
169
|
+
),
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
let result, error;
|
|
173
|
+
const toolStart = Date.now();
|
|
174
|
+
try {
|
|
175
|
+
result = await executeTool(name, args);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
error = e.message;
|
|
178
|
+
result = 'Error: ' + e.message;
|
|
179
|
+
}
|
|
180
|
+
const toolDuration = Date.now() - toolStart;
|
|
181
|
+
|
|
121
182
|
toolResults.push({ tool_call_id: tc.id, role: 'tool', content: result, name });
|
|
183
|
+
|
|
184
|
+
setState(s => ({
|
|
185
|
+
...s,
|
|
186
|
+
toolExecutions: s.toolExecutions.map(x =>
|
|
187
|
+
x.execId === execs[i].execId
|
|
188
|
+
? { ...x, status: error ? 'failed' : 'completed', duration: toolDuration, result, error }
|
|
189
|
+
: x
|
|
190
|
+
),
|
|
191
|
+
}));
|
|
122
192
|
}
|
|
123
193
|
|
|
194
|
+
setState(s => ({
|
|
195
|
+
...s,
|
|
196
|
+
agentStatus: 'Processing results...',
|
|
197
|
+
}));
|
|
198
|
+
|
|
199
|
+
const totalThoughtTime = Date.now() - thoughtTime;
|
|
200
|
+
|
|
124
201
|
setState(s => ({
|
|
125
202
|
...s,
|
|
126
203
|
messages: [
|
|
127
204
|
...s.messages,
|
|
128
|
-
{
|
|
205
|
+
{
|
|
206
|
+
id: nextId(),
|
|
207
|
+
role: 'assistant',
|
|
208
|
+
content: buffer || '',
|
|
209
|
+
toolCalls: toolCallsData,
|
|
210
|
+
toolResults: toolResults.map((tr, i) => ({
|
|
211
|
+
...tr,
|
|
212
|
+
execId: execs[i]?.execId,
|
|
213
|
+
duration: execs[i]?.duration,
|
|
214
|
+
status: execs[i]?.status,
|
|
215
|
+
})),
|
|
216
|
+
duration: totalThoughtTime,
|
|
217
|
+
},
|
|
129
218
|
...toolResults.map(tr => ({
|
|
130
219
|
id: nextId(), role: 'tool', content: tr.content,
|
|
131
220
|
tool_call_id: tr.tool_call_id, toolName: tr.name,
|
|
132
221
|
})),
|
|
133
222
|
],
|
|
223
|
+
toolExecutions: [],
|
|
224
|
+
agentStatus: '',
|
|
134
225
|
}));
|
|
135
226
|
|
|
136
227
|
const newHistory = [
|
|
137
228
|
...history,
|
|
138
|
-
{ role: 'assistant', content: buffer || null, tool_calls:
|
|
229
|
+
{ role: 'assistant', content: buffer || null, tool_calls: toolCallsData.map(tc => ({
|
|
139
230
|
id: tc.id, type: 'function',
|
|
140
231
|
function: { name: tc.function.name, arguments: tc.function.arguments },
|
|
141
232
|
}))},
|
|
@@ -143,21 +234,17 @@ async function processStream(provider, model, history, agentMode, setState, onSt
|
|
|
143
234
|
];
|
|
144
235
|
|
|
145
236
|
await processStream(provider, model, newHistory, agentMode, setState, onStreamContent, depth + 1);
|
|
237
|
+
} else {
|
|
238
|
+
setState(s => ({
|
|
239
|
+
...s,
|
|
240
|
+
thinking: false,
|
|
241
|
+
agentStatus: '',
|
|
242
|
+
toolExecutions: [],
|
|
243
|
+
thoughtTimer: null,
|
|
244
|
+
}));
|
|
146
245
|
}
|
|
147
246
|
}
|
|
148
247
|
|
|
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
248
|
export async function handleCommand(input, state, setState, modelSetter, providerSetter, model, provider) {
|
|
162
249
|
const parts = input.trim().split(/\s+/);
|
|
163
250
|
const cmd = parts[0];
|
|
@@ -185,13 +272,7 @@ export async function handleCommand(input, state, setState, modelSetter, provide
|
|
|
185
272
|
}));
|
|
186
273
|
break;
|
|
187
274
|
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
|
-
}));
|
|
275
|
+
setState(s => ({ ...s, messages: [], streamContent: '', toolExecutions: [], agentStatus: '' }));
|
|
195
276
|
break;
|
|
196
277
|
case '/export': {
|
|
197
278
|
const text = state.messages.map(m => '[' + m.role + '] ' + m.content).join('\n\n');
|
|
@@ -211,7 +292,6 @@ export async function handleCommand(input, state, setState, modelSetter, provide
|
|
|
211
292
|
'/provider Switch provider',
|
|
212
293
|
'/agent Toggle agent mode',
|
|
213
294
|
'/clear Clear conversation',
|
|
214
|
-
'/theme Change color theme',
|
|
215
295
|
'/export Export conversation',
|
|
216
296
|
'/help Show this help',
|
|
217
297
|
'/exit Exit CLARITY',
|
|
@@ -1,75 +1,102 @@
|
|
|
1
|
-
import React, { useMemo } from 'react';
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { CodeBlock } from './CodeBlock.js';
|
|
4
4
|
import { ThinkingBlock } from './ThinkingBlock.js';
|
|
5
5
|
import { ToolCard } from './ToolCard.js';
|
|
6
6
|
const { createElement: h } = React;
|
|
7
|
-
const
|
|
7
|
+
const tw = () => process.stdout.columns || 80;
|
|
8
8
|
|
|
9
9
|
function parseContent(text) {
|
|
10
10
|
if (!text) return [{ type: 'text', content: '' }];
|
|
11
11
|
const parts = [];
|
|
12
|
-
const
|
|
13
|
-
let
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
parts.push({ type: 'text', content: text.slice(lastIdx, match.index) });
|
|
19
|
-
}
|
|
20
|
-
parts.push({ type: 'code', lang: match[1] || 'text', code: match[2] });
|
|
21
|
-
lastIdx = match.index + match[0].length;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (lastIdx < text.length) {
|
|
25
|
-
parts.push({ type: 'text', content: text.slice(lastIdx) });
|
|
12
|
+
const cb = /```(\w*)\n?([\s\S]*?)```/g;
|
|
13
|
+
let last = 0, m;
|
|
14
|
+
while ((m = cb.exec(text)) !== null) {
|
|
15
|
+
if (m.index > last) parts.push({ type: 'text', content: text.slice(last, m.index) });
|
|
16
|
+
parts.push({ type: 'code', lang: m[1] || 'text', code: m[2] });
|
|
17
|
+
last = m.index + m[0].length;
|
|
26
18
|
}
|
|
27
|
-
|
|
28
|
-
return parts.length
|
|
19
|
+
if (last < text.length) parts.push({ type: 'text', content: text.slice(last) });
|
|
20
|
+
return parts.length ? parts : [{ type: 'text', content: text }];
|
|
29
21
|
}
|
|
30
22
|
|
|
31
|
-
function TextContent({ text }) {
|
|
23
|
+
function TextContent({ text, color }) {
|
|
32
24
|
const lines = text.split('\n');
|
|
33
|
-
const w = termWidth();
|
|
34
25
|
return h(Box, { flexDirection: 'column' },
|
|
35
26
|
lines.map((line, i) =>
|
|
36
27
|
h(Box, { key: i, flexDirection: 'row' },
|
|
37
|
-
h(Text, { color: '#7B2FFF'
|
|
28
|
+
h(Text, { color: color || '#7B2FFF' }, '\u2502'),
|
|
38
29
|
h(Text, { color: '#E0E0E0', wrap: 'wrap' }, ' ' + (line || ' '))
|
|
39
30
|
)
|
|
40
31
|
)
|
|
41
32
|
);
|
|
42
33
|
}
|
|
43
34
|
|
|
44
|
-
function
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
35
|
+
function ThoughtBlock({ toolResults, duration }) {
|
|
36
|
+
const [collapsed, setCollapsed] = useState(true);
|
|
37
|
+
const items = toolResults || [];
|
|
38
|
+
const timeStr = duration ? (duration < 1000 ? duration + 'ms' : (duration / 1000).toFixed(1) + 's') : '';
|
|
39
|
+
|
|
40
|
+
return h(Box, { flexDirection: 'column', marginY: 0 },
|
|
41
|
+
h(Box, { flexDirection: 'row' },
|
|
42
|
+
h(Text, { color: '#7B2FFF' }, '\u2502'),
|
|
43
|
+
h(Text, {
|
|
44
|
+
color: '#555',
|
|
45
|
+
bold: false,
|
|
46
|
+
wrap: 'truncate-end',
|
|
47
|
+
}, ' ' + (collapsed ? '\u25B6' : '\u25BC') + ' Thought' + (timeStr ? ' (' + timeStr + ')' : '')),
|
|
48
|
+
),
|
|
49
|
+
collapsed
|
|
50
|
+
? null
|
|
51
|
+
: h(Box, { flexDirection: 'column', paddingLeft: 2 },
|
|
52
|
+
items.map((tr, i) => {
|
|
53
|
+
const isLast = i === items.length - 1;
|
|
54
|
+
const prefix = isLast ? '\u2514\u2500' : '\u251C\u2500';
|
|
55
|
+
const icon = tr.status === 'failed' ? '\u2716' : '\u2714';
|
|
56
|
+
const col = tr.status === 'failed' ? '#FF4455' : '#00FF88';
|
|
57
|
+
const td = tr.duration ? ' ' + tr.duration + 'ms' : '';
|
|
58
|
+
return h(Box, { key: tr.execId || i, flexDirection: 'column' },
|
|
59
|
+
h(Box, { flexDirection: 'row' },
|
|
60
|
+
h(Text, { color: '#555' }, ' ' + prefix + ' '),
|
|
61
|
+
h(Text, { color: col }, icon + ' ' + tr.name + td),
|
|
62
|
+
),
|
|
63
|
+
tr.content && tr.content.length < 200
|
|
64
|
+
? h(Box, { paddingLeft: 4 },
|
|
65
|
+
h(Text, { color: '#AAA', wrap: 'wrap' }, String(tr.content).slice(0, tw() - 10))
|
|
66
|
+
)
|
|
67
|
+
: null
|
|
68
|
+
);
|
|
69
|
+
})
|
|
70
|
+
)
|
|
54
71
|
);
|
|
55
72
|
}
|
|
56
73
|
|
|
57
74
|
export function MessageBubble({ msg, isStreaming }) {
|
|
58
|
-
const w =
|
|
75
|
+
const w = tw();
|
|
59
76
|
|
|
60
77
|
if (msg.role === 'user') {
|
|
78
|
+
const lines = String(msg.content).split('\n');
|
|
61
79
|
return h(Box, { flexDirection: 'column', marginBottom: 1, marginTop: 1 },
|
|
62
80
|
h(Box, { flexDirection: 'row' },
|
|
63
81
|
h(Text, { color: '#FF6B6B', bold: true }, '\u276F'),
|
|
64
82
|
h(Text, { color: '#FF6B6B', bold: true }, ' YOU '),
|
|
65
83
|
h(Text, { color: '#9B59FF' }, '\u2500'.repeat(Math.max(0, w - 14))),
|
|
66
84
|
),
|
|
67
|
-
h(
|
|
85
|
+
h(Box, { flexDirection: 'column', backgroundColor: '#1A0A2E' },
|
|
86
|
+
lines.map((line, i) =>
|
|
87
|
+
h(Box, { key: i, flexDirection: 'row', backgroundColor: '#1A0A2E' },
|
|
88
|
+
h(Text, { color: '#9B59FF', backgroundColor: '#1A0A2E' }, '\u2502'),
|
|
89
|
+
h(Text, { color: '#C39BD3', backgroundColor: '#1A0A2E', wrap: 'wrap' }, ' ' + (line || ' '))
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
)
|
|
68
93
|
);
|
|
69
94
|
}
|
|
70
95
|
|
|
71
96
|
if (msg.role === 'assistant') {
|
|
72
97
|
const parts = useMemo(() => parseContent(msg.content), [msg.content]);
|
|
98
|
+
const hasToolResults = msg.toolResults && msg.toolResults.length > 0;
|
|
99
|
+
|
|
73
100
|
return h(Box, { flexDirection: 'column', marginBottom: 1 },
|
|
74
101
|
h(Box, { flexDirection: 'row' },
|
|
75
102
|
msg.streaming
|
|
@@ -81,20 +108,29 @@ export function MessageBubble({ msg, isStreaming }) {
|
|
|
81
108
|
? h(Text, { color: '#00D4FF' }, ' \u25CF streaming')
|
|
82
109
|
: null
|
|
83
110
|
),
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
111
|
+
hasToolResults
|
|
112
|
+
? h(ThoughtBlock, { toolResults: msg.toolResults, duration: msg.duration })
|
|
113
|
+
: null,
|
|
114
|
+
parts.length > 0 && parts[0].content
|
|
115
|
+
? parts.map((part, i) =>
|
|
116
|
+
part.type === 'code'
|
|
117
|
+
? h(CodeBlock, { key: i, code: part.code, language: part.lang, termWidth: w })
|
|
118
|
+
: h(TextContent, { key: i, text: part.content })
|
|
119
|
+
)
|
|
120
|
+
: null,
|
|
121
|
+
msg.duration && !hasToolResults
|
|
122
|
+
? h(Box, { flexDirection: 'row' },
|
|
123
|
+
h(Text, { color: '#7B2FFF' }, '\u2502'),
|
|
124
|
+
h(Text, { color: '#555' }, ' + Response: ' + (msg.duration < 1000 ? msg.duration + 'ms' : (msg.duration / 1000).toFixed(1) + 's'))
|
|
125
|
+
)
|
|
126
|
+
: null
|
|
90
127
|
);
|
|
91
128
|
}
|
|
92
129
|
|
|
93
130
|
if (msg.role === 'tool') {
|
|
94
131
|
return h(ToolCard, {
|
|
95
132
|
name: msg.toolName || 'tool',
|
|
96
|
-
|
|
97
|
-
status: msg.status || (msg.error ? 'failed' : 'completed'),
|
|
133
|
+
status: msg.error ? 'failed' : 'completed',
|
|
98
134
|
duration: msg.duration,
|
|
99
135
|
result: msg.content,
|
|
100
136
|
error: msg.error,
|
|
@@ -1,107 +1,111 @@
|
|
|
1
|
-
import React, { useState, useRef,
|
|
1
|
+
import React, { useState, useRef, useMemo } from 'react';
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
3
|
import { MessageBubble } from './MessageBubble.js';
|
|
4
|
-
import {
|
|
4
|
+
import { ToolCard } from './ToolCard.js';
|
|
5
5
|
const { createElement: h } = React;
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
}
|
|
7
|
+
function ToolExecutionTree({ executions }) {
|
|
8
|
+
if (!executions || executions.length === 0) return null;
|
|
9
|
+
return h(Box, { flexDirection: 'column', marginLeft: 2, marginY: 1 },
|
|
10
|
+
executions.map((exec, i) => {
|
|
11
|
+
const isLast = i === executions.length - 1;
|
|
12
|
+
const prefix = isLast ? '\u2514\u2500' : '\u251C\u2500';
|
|
13
|
+
const icon = exec.status === 'completed' ? '\u2714' :
|
|
14
|
+
exec.status === 'failed' ? '\u2716' : '\u25CF';
|
|
15
|
+
const color = exec.status === 'completed' ? '#00FF88' :
|
|
16
|
+
exec.status === 'failed' ? '#FF4455' : '#00D4FF';
|
|
17
|
+
const timeStr = exec.duration ? ' ' + exec.duration + 'ms' : '';
|
|
18
|
+
return h(Box, { key: exec.execId, flexDirection: 'column' },
|
|
19
|
+
h(Box, { flexDirection: 'row' },
|
|
20
|
+
h(Text, { color: '#555' }, ' ' + prefix + ' '),
|
|
21
|
+
h(Text, { color }, ' ' + icon + ' ' + exec.name + timeStr),
|
|
22
|
+
),
|
|
23
|
+
exec.status === 'completed' && exec.result
|
|
24
|
+
? h(Box, { paddingLeft: 6 },
|
|
25
|
+
h(Text, { color: '#AAA', wrap: 'wrap' },
|
|
26
|
+
String(exec.result).slice(0, termTrunc())
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
: null,
|
|
30
|
+
exec.status === 'running'
|
|
31
|
+
? h(Box, { paddingLeft: 6 },
|
|
32
|
+
h(Text, { color: '#555' }, 'running...')
|
|
33
|
+
)
|
|
34
|
+
: null,
|
|
35
|
+
);
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
}
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
const max = Math.max(0, totalItems - maxVisible);
|
|
44
|
-
setScrollOffset(s => {
|
|
45
|
-
const next = Math.min(max, s + maxVisible);
|
|
46
|
-
if (next >= max) setUserScrolled(false);
|
|
47
|
-
else setUserScrolled(true);
|
|
48
|
-
return next;
|
|
49
|
-
});
|
|
50
|
-
}
|
|
40
|
+
const termTrunc = () => Math.min(process.stdout.columns || 80, 120);
|
|
51
41
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
42
|
+
function AgentStatusLine({ agentStatus, executions }) {
|
|
43
|
+
if (!agentStatus && (!executions || executions.length === 0)) return null;
|
|
44
|
+
return h(Box, { flexDirection: 'column', marginLeft: 2, marginBottom: 1 },
|
|
45
|
+
agentStatus
|
|
46
|
+
? h(Box, { flexDirection: 'row' },
|
|
47
|
+
h(Text, { color: '#7B2FFF' }, '\u2502'),
|
|
48
|
+
h(Text, { color: '#00D4FF' }, ' \u25CF ' + agentStatus)
|
|
49
|
+
)
|
|
50
|
+
: null,
|
|
51
|
+
executions && executions.length > 0
|
|
52
|
+
? h(ToolExecutionTree, { executions })
|
|
53
|
+
: null,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
59
56
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
export function MessageList({ messages, thinking, streamContent, agentStatus, toolExecutions }) {
|
|
58
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
59
|
+
const [userScrolled, setUserScrolled] = useState(false);
|
|
60
|
+
const termHeight = process.stdout.rows ? process.stdout.rows - 6 : 20;
|
|
61
|
+
const maxVisible = Math.max(termHeight - 3, 5);
|
|
64
62
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
setUserScrolled(false);
|
|
69
|
-
}
|
|
63
|
+
const extraEntries = (thinking || streamContent) ? 1 : 0;
|
|
64
|
+
const totalItems = messages.length + extraEntries;
|
|
65
|
+
const maxOffset = Math.max(0, totalItems - maxVisible);
|
|
70
66
|
|
|
71
67
|
useInput((input, key) => {
|
|
72
|
-
if (key.upArrow && !key.ctrl)
|
|
73
|
-
if (key.downArrow && !key.ctrl)
|
|
74
|
-
if (key.pageUp
|
|
75
|
-
if (key.pageDown
|
|
76
|
-
if (key.home)
|
|
77
|
-
if (key.end)
|
|
68
|
+
if (key.upArrow && !key.ctrl) { setScrollOffset(s => { const n = Math.max(0, s - 1); if (n < maxOffset) setUserScrolled(true); return n; }); }
|
|
69
|
+
if (key.downArrow && !key.ctrl) { setScrollOffset(s => { const n = Math.min(maxOffset, s + 1); if (n >= maxOffset) setUserScrolled(false); return n; }); }
|
|
70
|
+
if (key.pageUp) { setScrollOffset(s => { const n = Math.max(0, s - maxVisible); setUserScrolled(true); return n; }); }
|
|
71
|
+
if (key.pageDown) { setScrollOffset(s => { const n = Math.min(maxOffset, s + maxVisible); if (n >= maxOffset) setUserScrolled(false); else setUserScrolled(true); return n; }); }
|
|
72
|
+
if (key.home) { setScrollOffset(0); setUserScrolled(true); }
|
|
73
|
+
if (key.end) { setScrollOffset(maxOffset); setUserScrolled(false); }
|
|
78
74
|
});
|
|
79
75
|
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
if (scrollOffset !== max) setScrollOffset(max);
|
|
76
|
+
if (!userScrolled && scrollOffset < maxOffset) {
|
|
77
|
+
setScrollOffset(maxOffset);
|
|
83
78
|
}
|
|
84
79
|
|
|
85
|
-
const
|
|
80
|
+
const visibleEnd = Math.min(totalItems, scrollOffset + maxVisible);
|
|
81
|
+
const visibleStart = Math.max(0, visibleEnd - maxVisible);
|
|
82
|
+
const visibleMsgs = messages.slice(visibleStart, visibleEnd);
|
|
83
|
+
const showStreaming = (thinking || streamContent) && visibleEnd >= totalItems - 1;
|
|
86
84
|
|
|
87
85
|
return h(Box, { flexDirection: 'column', flexGrow: 1 },
|
|
88
|
-
h(Box, { flexDirection: 'column', flexGrow: 1
|
|
89
|
-
|
|
90
|
-
h(MessageBubble, {
|
|
91
|
-
key: msg.id,
|
|
92
|
-
msg,
|
|
93
|
-
isStreaming: false,
|
|
94
|
-
})
|
|
86
|
+
h(Box, { flexDirection: 'column', flexGrow: 1 },
|
|
87
|
+
visibleMsgs.map(msg =>
|
|
88
|
+
h(MessageBubble, { key: msg.id, msg, isStreaming: false })
|
|
95
89
|
),
|
|
96
|
-
|
|
97
|
-
?
|
|
98
|
-
|
|
99
|
-
|
|
90
|
+
showStreaming
|
|
91
|
+
? h(Box, { flexDirection: 'column' },
|
|
92
|
+
h(AgentStatusLine, { agentStatus, executions: toolExecutions }),
|
|
93
|
+
streamContent
|
|
94
|
+
? h(MessageBubble, {
|
|
95
|
+
key: 'streaming',
|
|
96
|
+
msg: { id: 'stream', role: 'assistant', content: streamContent, streaming: true },
|
|
97
|
+
isStreaming: true
|
|
98
|
+
})
|
|
99
|
+
: h(Box, { marginLeft: 2, flexDirection: 'row' },
|
|
100
|
+
h(Text, { color: '#7B2FFF' }, '\u2502'),
|
|
101
|
+
h(Text, { color: '#00D4FF' }, ' \u25CF thinking...')
|
|
102
|
+
)
|
|
103
|
+
)
|
|
100
104
|
: null
|
|
101
105
|
),
|
|
102
106
|
userScrolled
|
|
103
107
|
? h(Box, { flexDirection: 'row', justifyContent: 'center' },
|
|
104
|
-
h(Text, { color: '#00D4FF', bold: true, backgroundColor: '#1A1A2E' },
|
|
108
|
+
h(Text, { color: '#00D4FF', bold: true, backgroundColor: '#1A1A2E', wrap: 'truncate-end' },
|
|
105
109
|
' \u25B2 Jump to latest (End) '
|
|
106
110
|
)
|
|
107
111
|
)
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
function readWithTimeout(reader, timeoutMs) {
|
|
2
|
+
return Promise.race([
|
|
3
|
+
reader.read(),
|
|
4
|
+
new Promise((_, reject) =>
|
|
5
|
+
setTimeout(() => reject(new Error('timeout')), timeoutMs)
|
|
6
|
+
),
|
|
7
|
+
]);
|
|
8
|
+
}
|
|
9
|
+
|
|
1
10
|
export async function* streamResponse(endpoint, body, apiKey, extraHeaders = {}) {
|
|
2
11
|
const controller = new AbortController();
|
|
3
|
-
const
|
|
12
|
+
const fetchTimeoutId = setTimeout(() => controller.abort(), 15000);
|
|
4
13
|
|
|
5
14
|
try {
|
|
6
15
|
const res = await fetch(endpoint, {
|
|
@@ -14,7 +23,7 @@ export async function* streamResponse(endpoint, body, apiKey, extraHeaders = {})
|
|
|
14
23
|
signal: controller.signal,
|
|
15
24
|
});
|
|
16
25
|
|
|
17
|
-
clearTimeout(
|
|
26
|
+
clearTimeout(fetchTimeoutId);
|
|
18
27
|
|
|
19
28
|
if (!res.ok) {
|
|
20
29
|
const text = await res.text();
|
|
@@ -26,13 +35,34 @@ export async function* streamResponse(endpoint, body, apiKey, extraHeaders = {})
|
|
|
26
35
|
const decoder = new TextDecoder();
|
|
27
36
|
let buffer = '';
|
|
28
37
|
const toolCallsMap = new Map();
|
|
38
|
+
let hasContent = false;
|
|
39
|
+
let idleTimeout = 15000;
|
|
29
40
|
|
|
30
41
|
while (true) {
|
|
31
|
-
|
|
42
|
+
let result;
|
|
43
|
+
try {
|
|
44
|
+
result = await readWithTimeout(reader, idleTimeout);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err.message === 'timeout') {
|
|
47
|
+
if (hasContent) {
|
|
48
|
+
yield { type: 'timeout', message: 'Stream idle for ' + idleTimeout + 'ms' };
|
|
49
|
+
}
|
|
50
|
+
const toolCalls = Array.from(toolCallsMap.values());
|
|
51
|
+
if (toolCalls.length > 0) {
|
|
52
|
+
yield { type: 'tool_calls', calls: toolCalls };
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { done, value } = result;
|
|
32
60
|
if (done) break;
|
|
61
|
+
|
|
33
62
|
buffer += decoder.decode(value, { stream: true });
|
|
34
63
|
const lines = buffer.split('\n');
|
|
35
64
|
buffer = lines.pop() || '';
|
|
65
|
+
|
|
36
66
|
for (const line of lines) {
|
|
37
67
|
if (line.startsWith('data: ')) {
|
|
38
68
|
const data = line.slice(6).trim();
|
|
@@ -51,6 +81,8 @@ export async function* streamResponse(endpoint, body, apiKey, extraHeaders = {})
|
|
|
51
81
|
const delta = choice.delta;
|
|
52
82
|
|
|
53
83
|
if (delta?.content) {
|
|
84
|
+
hasContent = true;
|
|
85
|
+
idleTimeout = 15000;
|
|
54
86
|
yield { type: 'token', content: delta.content };
|
|
55
87
|
}
|
|
56
88
|
|
|
@@ -88,6 +120,7 @@ export async function* streamResponse(endpoint, body, apiKey, extraHeaders = {})
|
|
|
88
120
|
yield { type: 'tool_calls', calls: toolCalls };
|
|
89
121
|
}
|
|
90
122
|
} finally {
|
|
91
|
-
clearTimeout(
|
|
123
|
+
clearTimeout(fetchTimeoutId);
|
|
124
|
+
try { controller.abort(); } catch {}
|
|
92
125
|
}
|
|
93
126
|
}
|