protoagent 0.1.3 → 0.1.4
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/dist/App.js +132 -78
- package/dist/agentic-loop.js +30 -11
- package/dist/components/CollapsibleBox.js +2 -2
- package/dist/components/ConsolidatedToolMessage.js +3 -11
- package/dist/components/FormattedMessage.js +80 -4
- package/dist/sub-agent.js +4 -1
- package/dist/system-prompt.js +3 -1
- package/dist/tools/edit-file.js +248 -16
- package/dist/tools/index.js +1 -1
- package/dist/tools/read-file.js +89 -3
- package/dist/tools/search-files.js +92 -1
- package/dist/utils/compactor.js +2 -1
- package/dist/utils/file-time.js +54 -0
- package/package.json +1 -1
- package/dist/components/Table.js +0 -275
package/dist/App.js
CHANGED
|
@@ -6,8 +6,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
* and cost/usage info. All heavy logic lives in `agentic-loop.ts`;
|
|
7
7
|
* this file is purely presentation + state wiring.
|
|
8
8
|
*/
|
|
9
|
-
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
10
|
-
import { Box, Text, useApp, useInput } from 'ink';
|
|
9
|
+
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
10
|
+
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
11
11
|
import { TextInput, Select, PasswordInput } from '@inkjs/ui';
|
|
12
12
|
import BigText from 'ink-big-text';
|
|
13
13
|
import { OpenAI } from 'openai';
|
|
@@ -24,7 +24,7 @@ import { generateSystemPrompt } from './system-prompt.js';
|
|
|
24
24
|
import { CollapsibleBox } from './components/CollapsibleBox.js';
|
|
25
25
|
import { ConsolidatedToolMessage } from './components/ConsolidatedToolMessage.js';
|
|
26
26
|
import { FormattedMessage } from './components/FormattedMessage.js';
|
|
27
|
-
function renderMessageList(messagesToRender, allMessages, expandedMessages, startIndex = 0) {
|
|
27
|
+
function renderMessageList(messagesToRender, allMessages, expandedMessages, startIndex = 0, deferTables = false) {
|
|
28
28
|
const rendered = [];
|
|
29
29
|
const skippedIndices = new Set();
|
|
30
30
|
messagesToRender.forEach((msg, localIndex) => {
|
|
@@ -35,6 +35,7 @@ function renderMessageList(messagesToRender, allMessages, expandedMessages, star
|
|
|
35
35
|
const msgAny = msg;
|
|
36
36
|
const isToolCall = msg.role === 'assistant' && msgAny.tool_calls && msgAny.tool_calls.length > 0;
|
|
37
37
|
const displayContent = 'content' in msg && typeof msg.content === 'string' ? msg.content : null;
|
|
38
|
+
const normalizedContent = normalizeMessageSpacing(displayContent || '');
|
|
38
39
|
const isFirstSystemMessage = msg.role === 'system' && !allMessages.slice(0, index).some((message) => message.role === 'system');
|
|
39
40
|
const previousMessage = index > 0 ? allMessages[index - 1] : null;
|
|
40
41
|
const followsToolMessage = previousMessage?.role === 'tool';
|
|
@@ -43,12 +44,15 @@ function renderMessageList(messagesToRender, allMessages, expandedMessages, star
|
|
|
43
44
|
const isConversationTurn = currentSpeaker === 'user' || currentSpeaker === 'assistant';
|
|
44
45
|
const previousWasConversationTurn = previousSpeaker === 'user' || previousSpeaker === 'assistant';
|
|
45
46
|
const speakerChanged = previousSpeaker !== currentSpeaker;
|
|
46
|
-
if
|
|
47
|
+
// Determine if we need a blank-line spacer above this message.
|
|
48
|
+
// At most one spacer is added per message to avoid doubling.
|
|
49
|
+
const needsSpacer = isFirstSystemMessage ||
|
|
50
|
+
(isConversationTurn && previousWasConversationTurn && speakerChanged) ||
|
|
51
|
+
followsToolMessage ||
|
|
52
|
+
(isToolCall && previousMessage != null);
|
|
53
|
+
if (needsSpacer) {
|
|
47
54
|
rendered.push(_jsx(Text, { children: " " }, `spacer-${index}`));
|
|
48
55
|
}
|
|
49
|
-
if (isConversationTurn && previousWasConversationTurn && speakerChanged) {
|
|
50
|
-
rendered.push(_jsx(Text, { children: " " }, `turn-spacer-${index}`));
|
|
51
|
-
}
|
|
52
56
|
if (msg.role === 'user') {
|
|
53
57
|
rendered.push(_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: '> ' }), _jsx(Text, { children: displayContent })] }) }, index));
|
|
54
58
|
return;
|
|
@@ -58,8 +62,8 @@ function renderMessageList(messagesToRender, allMessages, expandedMessages, star
|
|
|
58
62
|
return;
|
|
59
63
|
}
|
|
60
64
|
if (isToolCall) {
|
|
61
|
-
if (
|
|
62
|
-
rendered.push(_jsx(Box, { flexDirection: "column", children: _jsx(FormattedMessage, { content:
|
|
65
|
+
if (normalizedContent.length > 0) {
|
|
66
|
+
rendered.push(_jsx(Box, { flexDirection: "column", children: _jsx(FormattedMessage, { content: normalizedContent, deferTables: deferTables }) }, `${index}-text`));
|
|
63
67
|
}
|
|
64
68
|
const toolCalls = msgAny.tool_calls.map((tc) => ({
|
|
65
69
|
id: tc.id,
|
|
@@ -72,7 +76,7 @@ function renderMessageList(messagesToRender, allMessages, expandedMessages, star
|
|
|
72
76
|
const nextMsg = messagesToRender[nextLocalIndex];
|
|
73
77
|
if (nextMsg.role === 'tool' && nextMsg.tool_call_id === toolCall.id) {
|
|
74
78
|
toolResults.set(toolCall.id, {
|
|
75
|
-
content: nextMsg.content || '',
|
|
79
|
+
content: normalizeMessageSpacing(nextMsg.content || ''),
|
|
76
80
|
name: nextMsg.name || toolCall.name,
|
|
77
81
|
});
|
|
78
82
|
skippedIndices.add(nextLocalIndex);
|
|
@@ -84,19 +88,23 @@ function renderMessageList(messagesToRender, allMessages, expandedMessages, star
|
|
|
84
88
|
return;
|
|
85
89
|
}
|
|
86
90
|
if (msg.role === 'tool') {
|
|
87
|
-
rendered.push(_jsx(CollapsibleBox, { title: `${msgAny.name || 'tool'} result`, content:
|
|
91
|
+
rendered.push(_jsx(CollapsibleBox, { title: `${msgAny.name || 'tool'} result`, content: normalizedContent, dimColor: true, maxPreviewLines: 3, expanded: expandedMessages.has(index) }, index));
|
|
88
92
|
return;
|
|
89
93
|
}
|
|
90
|
-
rendered.push(_jsx(Box, { flexDirection: "column", children: _jsx(FormattedMessage, { content:
|
|
94
|
+
rendered.push(_jsx(Box, { flexDirection: "column", children: _jsx(FormattedMessage, { content: normalizedContent, deferTables: deferTables }) }, index));
|
|
91
95
|
});
|
|
92
96
|
return rendered;
|
|
93
97
|
}
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
function normalizeMessageSpacing(message) {
|
|
99
|
+
const normalized = message.replace(/\r\n/g, '\n');
|
|
100
|
+
const lines = normalized.split('\n');
|
|
101
|
+
while (lines.length > 0 && lines[0].trim() === '') {
|
|
102
|
+
lines.shift();
|
|
103
|
+
}
|
|
104
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
|
|
105
|
+
lines.pop();
|
|
106
|
+
}
|
|
107
|
+
return lines.join('\n');
|
|
100
108
|
}
|
|
101
109
|
function getVisualSpeaker(message) {
|
|
102
110
|
if (!message)
|
|
@@ -186,13 +194,13 @@ const ApprovalPrompt = ({ request, onRespond }) => {
|
|
|
186
194
|
{ label: sessionApprovalLabel, value: 'approve_session' },
|
|
187
195
|
{ label: 'Reject', value: 'reject' },
|
|
188
196
|
];
|
|
189
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "
|
|
197
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, marginY: 1, children: [_jsx(Text, { color: "green", bold: true, children: "Approval Required" }), _jsx(Text, { children: request.description }), request.detail && (_jsx(Text, { dimColor: true, children: request.detail.length > 200 ? request.detail.slice(0, 200) + '...' : request.detail })), _jsx(Box, { marginTop: 1, children: _jsx(Select, { options: items.map((item) => ({ value: item.value, label: item.label })), onChange: (value) => onRespond(value) }) })] }));
|
|
190
198
|
};
|
|
191
199
|
/** Cost/usage display in the status bar. */
|
|
192
200
|
const UsageDisplay = ({ usage, totalCost }) => {
|
|
193
201
|
if (!usage && totalCost === 0)
|
|
194
202
|
return null;
|
|
195
|
-
return (_jsxs(Box, { children: [usage && (_jsxs(Text, { dimColor: true, children: ["tokens: ", usage.inputTokens, "\u2193 ", usage.outputTokens, "\u2191 | ctx: ", usage.contextPercent.toFixed(0), "%"] })), totalCost > 0 && (_jsxs(Text, { dimColor: true, children: [" | cost: $", totalCost.toFixed(4)] }))] }));
|
|
203
|
+
return (_jsxs(Box, { marginTop: 1, children: [usage && (_jsxs(Text, { dimColor: true, children: ["tokens: ", usage.inputTokens, "\u2193 ", usage.outputTokens, "\u2191 | ctx: ", usage.contextPercent.toFixed(0), "%"] })), totalCost > 0 && (_jsxs(Text, { dimColor: true, children: [" | cost: $", totalCost.toFixed(4)] }))] }));
|
|
196
204
|
};
|
|
197
205
|
/** Inline setup wizard — shown when no config exists. */
|
|
198
206
|
const InlineSetup = ({ onComplete }) => {
|
|
@@ -231,6 +239,7 @@ const InlineSetup = ({ onComplete }) => {
|
|
|
231
239
|
// ─── Main App ───
|
|
232
240
|
export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
233
241
|
const { exit } = useApp();
|
|
242
|
+
const { stdout } = useStdout();
|
|
234
243
|
// Core state
|
|
235
244
|
const [config, setConfig] = useState(null);
|
|
236
245
|
const [completionMessages, setCompletionMessages] = useState([]);
|
|
@@ -242,7 +251,10 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
|
242
251
|
const [initialized, setInitialized] = useState(false);
|
|
243
252
|
const [needsSetup, setNeedsSetup] = useState(false);
|
|
244
253
|
const [logFilePath, setLogFilePath] = useState(null);
|
|
245
|
-
//
|
|
254
|
+
// Input reset key — incremented on submit to force TextInput remount and clear
|
|
255
|
+
const [inputResetKey, setInputResetKey] = useState(0);
|
|
256
|
+
const [inputWidthKey, setInputWidthKey] = useState(stdout?.columns ?? 80);
|
|
257
|
+
// Collapsible state — only applies to live (current turn) messages
|
|
246
258
|
const [expandedMessages, setExpandedMessages] = useState(new Set());
|
|
247
259
|
const expandLatestMessage = useCallback((index) => {
|
|
248
260
|
setExpandedMessages((prev) => {
|
|
@@ -259,6 +271,8 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
|
259
271
|
const [lastUsage, setLastUsage] = useState(null);
|
|
260
272
|
const [totalCost, setTotalCost] = useState(0);
|
|
261
273
|
const [spinnerFrame, setSpinnerFrame] = useState(0);
|
|
274
|
+
// Active tool tracking — shows which tool is currently executing
|
|
275
|
+
const [activeTool, setActiveTool] = useState(null);
|
|
262
276
|
// Session state
|
|
263
277
|
const [session, setSession] = useState(null);
|
|
264
278
|
// Quitting state — shows the resume command before exiting
|
|
@@ -269,6 +283,8 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
|
269
283
|
const assistantMessageRef = useRef(null);
|
|
270
284
|
// Abort controller for cancelling the current completion
|
|
271
285
|
const abortControllerRef = useRef(null);
|
|
286
|
+
// Debounce timer for text_delta renders (~50ms batching)
|
|
287
|
+
const textFlushTimerRef = useRef(null);
|
|
272
288
|
// ─── Post-config initialization (reused after inline setup) ───
|
|
273
289
|
const initializeWithConfig = useCallback(async (loadedConfig) => {
|
|
274
290
|
setConfig(loadedConfig);
|
|
@@ -313,6 +329,18 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
|
313
329
|
}, 100);
|
|
314
330
|
return () => clearInterval(interval);
|
|
315
331
|
}, [loading]);
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
if (!stdout)
|
|
334
|
+
return;
|
|
335
|
+
const handleResize = () => {
|
|
336
|
+
setInputWidthKey(stdout.columns ?? 80);
|
|
337
|
+
};
|
|
338
|
+
handleResize();
|
|
339
|
+
stdout.on('resize', handleResize);
|
|
340
|
+
return () => {
|
|
341
|
+
stdout.off('resize', handleResize);
|
|
342
|
+
};
|
|
343
|
+
}, [stdout]);
|
|
316
344
|
useEffect(() => {
|
|
317
345
|
const init = async () => {
|
|
318
346
|
// Set log level and initialize log file
|
|
@@ -416,7 +444,7 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
|
416
444
|
default:
|
|
417
445
|
return false;
|
|
418
446
|
}
|
|
419
|
-
}, [exit, session, completionMessages]);
|
|
447
|
+
}, [config, exit, session, completionMessages]);
|
|
420
448
|
// ─── Submit handler ───
|
|
421
449
|
const handleSubmit = useCallback(async (value) => {
|
|
422
450
|
const trimmed = value.trim();
|
|
@@ -427,10 +455,12 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
|
427
455
|
const handled = await handleSlashCommand(trimmed);
|
|
428
456
|
if (handled) {
|
|
429
457
|
setInputText('');
|
|
458
|
+
setInputResetKey((prev) => prev + 1);
|
|
430
459
|
return;
|
|
431
460
|
}
|
|
432
461
|
}
|
|
433
462
|
setInputText('');
|
|
463
|
+
setInputResetKey((prev) => prev + 1); // Force TextInput to remount and clear
|
|
434
464
|
setLoading(true);
|
|
435
465
|
setError(null);
|
|
436
466
|
setHelpMessage(null);
|
|
@@ -448,9 +478,9 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
|
448
478
|
const updatedMessages = await runAgenticLoop(clientRef.current, config.model, [...completionMessages, userMessage], trimmed, (event) => {
|
|
449
479
|
switch (event.type) {
|
|
450
480
|
case 'text_delta':
|
|
451
|
-
// Update the current assistant message in completionMessages
|
|
481
|
+
// Update the current assistant message in completionMessages
|
|
452
482
|
if (!assistantMessageRef.current || assistantMessageRef.current.kind !== 'streaming_text') {
|
|
453
|
-
// First text delta
|
|
483
|
+
// First text delta — create the assistant message immediately
|
|
454
484
|
const assistantMsg = { role: 'assistant', content: event.content || '', tool_calls: [] };
|
|
455
485
|
setCompletionMessages((prev) => {
|
|
456
486
|
assistantMessageRef.current = { message: assistantMsg, index: prev.length, kind: 'streaming_text' };
|
|
@@ -458,80 +488,96 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
|
458
488
|
});
|
|
459
489
|
}
|
|
460
490
|
else {
|
|
461
|
-
// Subsequent
|
|
491
|
+
// Subsequent deltas — accumulate in ref, debounce the render (~50ms)
|
|
462
492
|
assistantMessageRef.current.message.content += event.content || '';
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
493
|
+
if (!textFlushTimerRef.current) {
|
|
494
|
+
textFlushTimerRef.current = setTimeout(() => {
|
|
495
|
+
textFlushTimerRef.current = null;
|
|
496
|
+
setCompletionMessages((prev) => {
|
|
497
|
+
if (!assistantMessageRef.current)
|
|
498
|
+
return prev;
|
|
499
|
+
const updated = [...prev];
|
|
500
|
+
updated[assistantMessageRef.current.index] = { ...assistantMessageRef.current.message };
|
|
501
|
+
return updated;
|
|
502
|
+
});
|
|
503
|
+
}, 50);
|
|
504
|
+
}
|
|
468
505
|
}
|
|
469
506
|
break;
|
|
470
507
|
case 'tool_call':
|
|
471
508
|
if (event.toolCall) {
|
|
472
509
|
const toolCall = event.toolCall;
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
:
|
|
481
|
-
const assistantMsg = existingMessage || {
|
|
482
|
-
role: 'assistant',
|
|
483
|
-
content: '',
|
|
484
|
-
tool_calls: [],
|
|
485
|
-
};
|
|
486
|
-
const existingToolCallIndex = assistantMsg.tool_calls.findIndex((existingToolCall) => existingToolCall.id === toolCall.id);
|
|
487
|
-
const nextToolCall = {
|
|
488
|
-
id: toolCall.id,
|
|
489
|
-
type: 'function',
|
|
490
|
-
function: {
|
|
491
|
-
name: toolCall.name,
|
|
492
|
-
arguments: toolCall.args,
|
|
493
|
-
},
|
|
494
|
-
};
|
|
495
|
-
if (existingToolCallIndex === -1) {
|
|
496
|
-
assistantMsg.tool_calls.push(nextToolCall);
|
|
497
|
-
}
|
|
498
|
-
else {
|
|
499
|
-
assistantMsg.tool_calls[existingToolCallIndex] = nextToolCall;
|
|
510
|
+
setActiveTool(toolCall.name);
|
|
511
|
+
// Track the tool call in the ref WITHOUT triggering a render.
|
|
512
|
+
// The render will happen when tool_result arrives.
|
|
513
|
+
const existingRef = assistantMessageRef.current;
|
|
514
|
+
const assistantMsg = existingRef?.message
|
|
515
|
+
? {
|
|
516
|
+
...existingRef.message,
|
|
517
|
+
tool_calls: [...(existingRef.message.tool_calls || [])],
|
|
500
518
|
}
|
|
501
|
-
|
|
519
|
+
: { role: 'assistant', content: '', tool_calls: [] };
|
|
520
|
+
const nextToolCall = {
|
|
521
|
+
id: toolCall.id,
|
|
522
|
+
type: 'function',
|
|
523
|
+
function: { name: toolCall.name, arguments: toolCall.args },
|
|
524
|
+
};
|
|
525
|
+
const idx = assistantMsg.tool_calls.findIndex((tc) => tc.id === toolCall.id);
|
|
526
|
+
if (idx === -1) {
|
|
527
|
+
assistantMsg.tool_calls.push(nextToolCall);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
assistantMsg.tool_calls[idx] = nextToolCall;
|
|
531
|
+
}
|
|
532
|
+
if (!existingRef) {
|
|
533
|
+
// First tool call — we need to add the assistant message to state
|
|
534
|
+
setCompletionMessages((prev) => {
|
|
535
|
+
assistantMessageRef.current = {
|
|
536
|
+
message: assistantMsg,
|
|
537
|
+
index: prev.length,
|
|
538
|
+
kind: 'tool_call_assistant',
|
|
539
|
+
};
|
|
540
|
+
return [...prev, assistantMsg];
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
// Subsequent tool calls — just update the ref, no render
|
|
502
545
|
assistantMessageRef.current = {
|
|
546
|
+
...existingRef,
|
|
503
547
|
message: assistantMsg,
|
|
504
|
-
index: nextIndex,
|
|
505
548
|
kind: 'tool_call_assistant',
|
|
506
549
|
};
|
|
507
|
-
|
|
508
|
-
const updated = [...prev];
|
|
509
|
-
updated[existingRef.index] = assistantMsg;
|
|
510
|
-
return updated;
|
|
511
|
-
}
|
|
512
|
-
return [...prev, assistantMsg];
|
|
513
|
-
});
|
|
550
|
+
}
|
|
514
551
|
}
|
|
515
552
|
break;
|
|
516
553
|
case 'tool_result':
|
|
517
554
|
if (event.toolCall) {
|
|
518
555
|
const toolCall = event.toolCall;
|
|
556
|
+
setActiveTool(null);
|
|
519
557
|
if (toolCall.name === 'todo_read' || toolCall.name === 'todo_write') {
|
|
520
558
|
const currentAssistantIndex = assistantMessageRef.current?.index;
|
|
521
559
|
if (typeof currentAssistantIndex === 'number') {
|
|
522
560
|
expandLatestMessage(currentAssistantIndex);
|
|
523
561
|
}
|
|
524
562
|
}
|
|
525
|
-
//
|
|
526
|
-
setCompletionMessages((prev) =>
|
|
527
|
-
...prev
|
|
528
|
-
|
|
563
|
+
// Flush the assistant message update + tool result in a SINGLE state update
|
|
564
|
+
setCompletionMessages((prev) => {
|
|
565
|
+
const updated = [...prev];
|
|
566
|
+
// Sync assistant message (may have new tool_calls since last render)
|
|
567
|
+
if (assistantMessageRef.current) {
|
|
568
|
+
updated[assistantMessageRef.current.index] = {
|
|
569
|
+
...assistantMessageRef.current.message,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
// Append tool result
|
|
573
|
+
updated.push({
|
|
529
574
|
role: 'tool',
|
|
530
575
|
tool_call_id: toolCall.id,
|
|
531
576
|
content: toolCall.result || '',
|
|
532
577
|
name: toolCall.name,
|
|
533
|
-
}
|
|
534
|
-
|
|
578
|
+
});
|
|
579
|
+
return updated;
|
|
580
|
+
});
|
|
535
581
|
}
|
|
536
582
|
break;
|
|
537
583
|
case 'usage':
|
|
@@ -571,7 +617,16 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
|
571
617
|
setError('Unknown error');
|
|
572
618
|
}
|
|
573
619
|
break;
|
|
620
|
+
case 'iteration_done':
|
|
621
|
+
assistantMessageRef.current = null;
|
|
622
|
+
break;
|
|
574
623
|
case 'done':
|
|
624
|
+
// Clear any pending text delta timer
|
|
625
|
+
if (textFlushTimerRef.current) {
|
|
626
|
+
clearTimeout(textFlushTimerRef.current);
|
|
627
|
+
textFlushTimerRef.current = null;
|
|
628
|
+
}
|
|
629
|
+
setActiveTool(null);
|
|
575
630
|
setThreadErrors((prev) => prev.filter((threadError) => !threadError.transient));
|
|
576
631
|
break;
|
|
577
632
|
}
|
|
@@ -617,18 +672,17 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
|
|
|
617
672
|
: completionMessages.length;
|
|
618
673
|
const archivedMessages = completionMessages.slice(0, liveStartIndex);
|
|
619
674
|
const liveMessages = completionMessages.slice(liveStartIndex);
|
|
620
|
-
const archivedMessageNodes = renderMessageList(archivedMessages, completionMessages, expandedMessages);
|
|
621
|
-
const liveMessageNodes = renderMessageList(liveMessages, completionMessages, expandedMessages, liveStartIndex);
|
|
622
|
-
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsx(BigText, { text: "ProtoAgent", font: "tiny", colors: ["#09A469"] }),
|
|
675
|
+
const archivedMessageNodes = useMemo(() => renderMessageList(archivedMessages, completionMessages, expandedMessages), [archivedMessages, completionMessages, expandedMessages]);
|
|
676
|
+
const liveMessageNodes = useMemo(() => renderMessageList(liveMessages, completionMessages, expandedMessages, liveStartIndex, loading), [liveMessages, completionMessages, expandedMessages, liveStartIndex, loading]);
|
|
677
|
+
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsx(BigText, { text: "ProtoAgent", font: "tiny", colors: ["#09A469"] }), config && (_jsxs(Text, { dimColor: true, children: ["Model: ", providerInfo?.name || config.provider, " / ", config.model, dangerouslyAcceptAll && _jsx(Text, { color: "red", children: " (auto-approve all)" }), session && _jsxs(Text, { dimColor: true, children: [" | Session: ", session.id.slice(0, 8)] })] })), logFilePath && _jsxs(Text, { dimColor: true, children: ["Debug logs: ", logFilePath] }), error && _jsx(Text, { color: "red", children: error }), helpMessage && (_jsx(CollapsibleBox, { title: "Help", content: helpMessage, titleColor: "green", dimColor: false, maxPreviewLines: 10, expanded: true })), !initialized && !error && !needsSetup && _jsx(Text, { children: "Initializing..." }), needsSetup && (_jsx(InlineSetup, { onComplete: (newConfig) => {
|
|
623
678
|
initializeWithConfig(newConfig).catch((err) => {
|
|
624
679
|
setError(`Initialization failed: ${err.message}`);
|
|
625
680
|
});
|
|
626
|
-
} })), _jsxs(Box, { flexDirection: "column", flexGrow: 1,
|
|
681
|
+
} })), _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [archivedMessageNodes, liveMessageNodes, threadErrors.map((threadError) => (_jsx(Box, { marginBottom: 1, borderStyle: "round", borderColor: "red", paddingX: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", threadError.message] }) }, `thread-error-${threadError.id}`))), loading && completionMessages.length > 0 && ((() => {
|
|
627
682
|
const lastMsg = completionMessages[completionMessages.length - 1];
|
|
628
|
-
// Show "Thinking..." only if the last message is a user message (no assistant response yet)
|
|
629
683
|
return lastMsg.role === 'user' ? _jsx(Text, { dimColor: true, children: "Thinking..." }) : null;
|
|
630
684
|
})()), pendingApproval && (_jsx(ApprovalPrompt, { request: pendingApproval.request, onRespond: (response) => {
|
|
631
685
|
pendingApproval.resolve(response);
|
|
632
686
|
setPendingApproval(null);
|
|
633
|
-
} }))] }), _jsx(UsageDisplay, { usage: lastUsage ?? null, totalCost: totalCost }), initialized && !pendingApproval && inputText.startsWith('/') && (_jsx(CommandFilter, { inputText: inputText })), initialized && !pendingApproval && loading && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "green", bold: true, children: [SPINNER_FRAMES[spinnerFrame],
|
|
687
|
+
} }))] }), _jsx(UsageDisplay, { usage: lastUsage ?? null, totalCost: totalCost }), initialized && !pendingApproval && inputText.startsWith('/') && (_jsx(CommandFilter, { inputText: inputText })), initialized && !pendingApproval && loading && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "green", bold: true, children: [SPINNER_FRAMES[spinnerFrame], ' ', activeTool ? `Running ${activeTool}...` : 'Working...'] }) })), initialized && !pendingApproval && (_jsx(Box, { borderStyle: "round", borderColor: "green", paddingX: 1, flexDirection: "column", children: _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 2, flexShrink: 0, children: _jsx(Text, { color: "green", bold: true, children: '>' }) }), _jsx(Box, { flexGrow: 1, minWidth: 10, children: _jsx(TextInput, { defaultValue: inputText, onChange: setInputText, placeholder: "Type your message... (/help for commands)", onSubmit: handleSubmit }, `${inputResetKey}-${inputWidthKey}`) })] }) }, `input-shell-${inputWidthKey}`)), quittingSession && (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingX: 1, marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Session saved. Resume with:" }), _jsxs(Text, { color: "green", children: ["protoagent --session ", quittingSession.id] })] }))] }));
|
|
634
688
|
};
|
package/dist/agentic-loop.js
CHANGED
|
@@ -351,20 +351,19 @@ export async function runAgenticLoop(client, model, messages, userInput, onEvent
|
|
|
351
351
|
}
|
|
352
352
|
}
|
|
353
353
|
}
|
|
354
|
-
// Emit usage info
|
|
355
|
-
|
|
354
|
+
// Emit usage info — always emit, even without pricing (use estimates)
|
|
355
|
+
{
|
|
356
356
|
const inputTokens = actualUsage?.prompt_tokens ?? estimateConversationTokens(updatedMessages);
|
|
357
357
|
const outputTokens = actualUsage?.completion_tokens ?? estimateTokens(assistantMessage.content || '');
|
|
358
|
-
const
|
|
359
|
-
|
|
358
|
+
const cost = pricing
|
|
359
|
+
? createUsageInfo(inputTokens, outputTokens, pricing).estimatedCost
|
|
360
|
+
: 0;
|
|
361
|
+
const contextPercent = pricing
|
|
362
|
+
? getContextInfo(updatedMessages, pricing).utilizationPercentage
|
|
363
|
+
: 0;
|
|
360
364
|
onEvent({
|
|
361
365
|
type: 'usage',
|
|
362
|
-
usage: {
|
|
363
|
-
inputTokens,
|
|
364
|
-
outputTokens,
|
|
365
|
-
cost: usageInfo.estimatedCost,
|
|
366
|
-
contextPercent: contextInfo.utilizationPercentage,
|
|
367
|
-
},
|
|
366
|
+
usage: { inputTokens, outputTokens, cost, contextPercent },
|
|
368
367
|
});
|
|
369
368
|
}
|
|
370
369
|
// Handle tool calls
|
|
@@ -391,6 +390,12 @@ export async function runAgenticLoop(client, model, messages, userInput, onEvent
|
|
|
391
390
|
});
|
|
392
391
|
updatedMessages.push(assistantMessage);
|
|
393
392
|
for (const toolCall of assistantMessage.tool_calls) {
|
|
393
|
+
// Check abort between tool calls
|
|
394
|
+
if (abortSignal?.aborted) {
|
|
395
|
+
logger.debug('Agentic loop aborted between tool calls');
|
|
396
|
+
emitAbortAndFinish(onEvent);
|
|
397
|
+
return updatedMessages;
|
|
398
|
+
}
|
|
394
399
|
const { name, arguments: argsStr } = toolCall.function;
|
|
395
400
|
onEvent({
|
|
396
401
|
type: 'tool_call',
|
|
@@ -401,7 +406,18 @@ export async function runAgenticLoop(client, model, messages, userInput, onEvent
|
|
|
401
406
|
let result;
|
|
402
407
|
// Handle sub-agent tool specially
|
|
403
408
|
if (name === 'sub_agent') {
|
|
404
|
-
|
|
409
|
+
const subProgress = (evt) => {
|
|
410
|
+
onEvent({
|
|
411
|
+
type: 'tool_call',
|
|
412
|
+
toolCall: {
|
|
413
|
+
id: toolCall.id,
|
|
414
|
+
name: `sub_agent → ${evt.tool}`,
|
|
415
|
+
args: '',
|
|
416
|
+
status: evt.status === 'running' ? 'running' : 'done',
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
};
|
|
420
|
+
result = await runSubAgent(client, model, args.task, args.max_iterations, requestDefaults, subProgress);
|
|
405
421
|
}
|
|
406
422
|
else {
|
|
407
423
|
result = await handleToolCall(name, args, { sessionId });
|
|
@@ -435,6 +451,9 @@ export async function runAgenticLoop(client, model, messages, userInput, onEvent
|
|
|
435
451
|
});
|
|
436
452
|
}
|
|
437
453
|
}
|
|
454
|
+
// Signal UI that this iteration's tool calls are all done,
|
|
455
|
+
// so it can flush completed messages to static output.
|
|
456
|
+
onEvent({ type: 'iteration_done' });
|
|
438
457
|
// Continue loop — let the LLM process tool results
|
|
439
458
|
continue;
|
|
440
459
|
}
|
|
@@ -7,7 +7,7 @@ export const CollapsibleBox = ({ title, content, titleColor, dimColor = false, m
|
|
|
7
7
|
const isLong = isTooManyLines || isTooManyChars;
|
|
8
8
|
// If content is short, always show it
|
|
9
9
|
if (!isLong) {
|
|
10
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: marginBottom,
|
|
10
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: marginBottom, children: [_jsx(Text, { color: titleColor, dimColor: dimColor, bold: true, children: title }), _jsx(Box, { marginLeft: 1, children: _jsx(Text, { dimColor: dimColor, children: content }) })] }));
|
|
11
11
|
}
|
|
12
12
|
// For long content, show preview or full content
|
|
13
13
|
let preview;
|
|
@@ -22,5 +22,5 @@ export const CollapsibleBox = ({ title, content, titleColor, dimColor = false, m
|
|
|
22
22
|
: linesTruncated;
|
|
23
23
|
}
|
|
24
24
|
const hasMore = !expanded;
|
|
25
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: marginBottom,
|
|
25
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: marginBottom, children: [_jsxs(Text, { color: titleColor, dimColor: dimColor, bold: true, children: [expanded ? '▼' : '▶', " ", title] }), _jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsx(Text, { dimColor: dimColor, children: preview }), hasMore && _jsx(Text, { dimColor: true, children: "... (use /expand to see all)" })] })] }));
|
|
26
26
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import { Table } from './Table.js';
|
|
4
3
|
import { FormattedMessage } from './FormattedMessage.js';
|
|
5
4
|
export const ConsolidatedToolMessage = ({ toolCalls, toolResults, expanded = false, }) => {
|
|
6
5
|
const toolNames = toolCalls.map((toolCall) => toolCall.name);
|
|
@@ -9,18 +8,11 @@ export const ConsolidatedToolMessage = ({ toolCalls, toolResults, expanded = fal
|
|
|
9
8
|
const titleColor = containsTodoTool ? 'green' : 'white';
|
|
10
9
|
const isExpanded = expanded || containsTodoTool;
|
|
11
10
|
if (isExpanded) {
|
|
12
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
11
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: titleColor, bold: true, children: ["\u25BC ", title] }), _jsx(Box, { flexDirection: "column", marginLeft: 1, children: toolCalls.map((toolCall, idx) => {
|
|
13
12
|
const result = toolResults.get(toolCall.id);
|
|
14
13
|
if (!result)
|
|
15
14
|
return null;
|
|
16
|
-
|
|
17
|
-
let isJsonTable = false;
|
|
18
|
-
try {
|
|
19
|
-
const parsed = JSON.parse(result.content);
|
|
20
|
-
isJsonTable = typeof parsed === 'object' && parsed !== null;
|
|
21
|
-
}
|
|
22
|
-
catch (e) { }
|
|
23
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "cyan", bold: true, children: ["[", result.name, "]:"] }), isJsonTable ? (_jsx(Table, { data: result.content })) : (_jsx(FormattedMessage, { content: result.content }))] }, idx));
|
|
15
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "cyan", bold: true, children: ["[", result.name, "]:"] }), _jsx(FormattedMessage, { content: result.content })] }, idx));
|
|
24
16
|
}) })] }));
|
|
25
17
|
}
|
|
26
18
|
const compactLines = toolCalls.flatMap((toolCall) => {
|
|
@@ -37,5 +29,5 @@ export const ConsolidatedToolMessage = ({ toolCalls, toolResults, expanded = fal
|
|
|
37
29
|
const preview = compactPreview.length > previewLimit
|
|
38
30
|
? `${compactPreview.slice(0, previewLimit).trimEnd()}... (use /expand)`
|
|
39
31
|
: compactPreview;
|
|
40
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
32
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: titleColor, dimColor: true, bold: true, children: ["\u25B6 ", title] }), _jsx(Box, { marginLeft: 1, children: _jsx(Text, { dimColor: true, children: preview }) })] }));
|
|
41
33
|
};
|
|
@@ -1,16 +1,89 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import { Table } from './Table.js';
|
|
4
3
|
import { formatMessage } from '../utils/format-message.js';
|
|
4
|
+
export const DEFERRED_TABLE_PLACEHOLDER = 'table loading';
|
|
5
|
+
const graphemeSegmenter = typeof Intl !== 'undefined' && 'Segmenter' in Intl
|
|
6
|
+
? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
|
7
|
+
: null;
|
|
8
|
+
const COMBINING_MARK_PATTERN = /\p{Mark}/u;
|
|
9
|
+
const ZERO_WIDTH_PATTERN = /[\u200B-\u200D\uFE0E\uFE0F]/u;
|
|
10
|
+
const DOUBLE_WIDTH_PATTERN = /[\u1100-\u115F\u2329\u232A\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE6F\uFF00-\uFF60\uFFE0-\uFFE6\u{1F300}-\u{1FAFF}\u{1F900}-\u{1F9FF}\u{1F1E6}-\u{1F1FF}]/u;
|
|
11
|
+
function splitGraphemes(text) {
|
|
12
|
+
if (!text)
|
|
13
|
+
return [];
|
|
14
|
+
if (graphemeSegmenter) {
|
|
15
|
+
return Array.from(graphemeSegmenter.segment(text), (segment) => segment.segment);
|
|
16
|
+
}
|
|
17
|
+
return Array.from(text);
|
|
18
|
+
}
|
|
19
|
+
function getGraphemeWidth(grapheme) {
|
|
20
|
+
if (!grapheme)
|
|
21
|
+
return 0;
|
|
22
|
+
if (ZERO_WIDTH_PATTERN.test(grapheme))
|
|
23
|
+
return 0;
|
|
24
|
+
if (COMBINING_MARK_PATTERN.test(grapheme))
|
|
25
|
+
return 0;
|
|
26
|
+
if (/^[\u0000-\u001F\u007F-\u009F]$/.test(grapheme))
|
|
27
|
+
return 0;
|
|
28
|
+
if (DOUBLE_WIDTH_PATTERN.test(grapheme))
|
|
29
|
+
return 2;
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
function getTextWidth(text) {
|
|
33
|
+
return splitGraphemes(text).reduce((width, grapheme) => width + getGraphemeWidth(grapheme), 0);
|
|
34
|
+
}
|
|
35
|
+
function padToWidth(text, width) {
|
|
36
|
+
const padding = Math.max(0, width - getTextWidth(text));
|
|
37
|
+
return text + ' '.repeat(padding);
|
|
38
|
+
}
|
|
39
|
+
function parseMarkdownTableToRows(markdown) {
|
|
40
|
+
const lines = markdown.trim().split('\n');
|
|
41
|
+
if (lines.length < 3)
|
|
42
|
+
return null;
|
|
43
|
+
const parseRow = (row) => row.split('|')
|
|
44
|
+
.map((cell) => cell.trim())
|
|
45
|
+
.filter((cell, index, array) => {
|
|
46
|
+
if (index === 0 && cell === '')
|
|
47
|
+
return false;
|
|
48
|
+
if (index === array.length - 1 && cell === '')
|
|
49
|
+
return false;
|
|
50
|
+
return true;
|
|
51
|
+
});
|
|
52
|
+
const header = parseRow(lines[0]);
|
|
53
|
+
const separator = parseRow(lines[1]);
|
|
54
|
+
if (header.length === 0 || separator.length === 0)
|
|
55
|
+
return null;
|
|
56
|
+
if (!separator.every((cell) => /^:?-{3,}:?$/.test(cell.replace(/\s+/g, ''))))
|
|
57
|
+
return null;
|
|
58
|
+
const rows = lines.slice(2).map(parseRow);
|
|
59
|
+
return [header, ...rows];
|
|
60
|
+
}
|
|
61
|
+
function renderPreformattedTable(markdown) {
|
|
62
|
+
const rows = parseMarkdownTableToRows(markdown);
|
|
63
|
+
if (!rows || rows.length === 0) {
|
|
64
|
+
return markdown.trim();
|
|
65
|
+
}
|
|
66
|
+
const columnCount = Math.max(...rows.map((row) => row.length));
|
|
67
|
+
const normalizedRows = rows.map((row) => Array.from({ length: columnCount }, (_, index) => row[index] ?? ''));
|
|
68
|
+
const widths = Array.from({ length: columnCount }, (_, index) => Math.max(...normalizedRows.map((row) => getTextWidth(row[index]))));
|
|
69
|
+
const formatRow = (row) => row
|
|
70
|
+
.map((cell, index) => padToWidth(cell, widths[index]))
|
|
71
|
+
.join(' ')
|
|
72
|
+
.trimEnd();
|
|
73
|
+
const header = formatRow(normalizedRows[0]);
|
|
74
|
+
const divider = widths.map((width) => '-'.repeat(width)).join(' ');
|
|
75
|
+
const body = normalizedRows.slice(1).map(formatRow);
|
|
76
|
+
return [header, divider, ...body].join('\n');
|
|
77
|
+
}
|
|
5
78
|
/**
|
|
6
79
|
* FormattedMessage component
|
|
7
80
|
*
|
|
8
81
|
* Parses a markdown string and renders:
|
|
9
82
|
* - Standard text with ANSI formatting
|
|
10
|
-
* - Markdown tables
|
|
83
|
+
* - Markdown tables as preformatted monospace text
|
|
11
84
|
* - Code blocks (rendered in a box)
|
|
12
85
|
*/
|
|
13
|
-
export const FormattedMessage = ({ content }) => {
|
|
86
|
+
export const FormattedMessage = ({ content, deferTables = false }) => {
|
|
14
87
|
if (!content)
|
|
15
88
|
return null;
|
|
16
89
|
const lines = content.split('\n');
|
|
@@ -80,7 +153,10 @@ export const FormattedMessage = ({ content }) => {
|
|
|
80
153
|
if (block.type === 'table') {
|
|
81
154
|
if (!block.content.trim())
|
|
82
155
|
return null;
|
|
83
|
-
|
|
156
|
+
if (deferTables) {
|
|
157
|
+
return (_jsx(Box, { marginY: 1, children: _jsx(Text, { dimColor: true, children: DEFERRED_TABLE_PLACEHOLDER }) }, index));
|
|
158
|
+
}
|
|
159
|
+
return (_jsx(Box, { marginY: 1, paddingX: 1, borderStyle: "round", borderColor: "gray", children: _jsx(Text, { children: renderPreformattedTable(block.content) }) }, index));
|
|
84
160
|
}
|
|
85
161
|
if (block.type === 'code') {
|
|
86
162
|
return (_jsx(Box, { marginY: 1, paddingX: 1, borderStyle: "round", borderColor: "gray", children: _jsx(Text, { dimColor: true, children: block.content }) }, index));
|