protoagent 0.1.11 → 0.1.13
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 +107 -48
- package/dist/agentic-loop.js +18 -1
- package/dist/components/FormattedMessage.js +2 -2
- package/dist/system-prompt.js +2 -1
- package/dist/utils/format-message.js +49 -23
- package/package.json +1 -1
package/dist/App.js
CHANGED
|
@@ -55,7 +55,7 @@ Here's how the terminal UI is laid out (showcasing all options at once for demon
|
|
|
55
55
|
│ protoagent --session abc12345 │
|
|
56
56
|
└─────────────────────────────────────────┘
|
|
57
57
|
*/
|
|
58
|
-
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
58
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
59
59
|
import { Box, Text, Static, useApp, useInput, useStdout } from 'ink';
|
|
60
60
|
import { LeftBar } from './components/LeftBar.js';
|
|
61
61
|
import { TextInput, Select } from '@inkjs/ui';
|
|
@@ -70,18 +70,13 @@ import { createSession, ensureSystemPromptAtTop, saveSession, loadSession, gener
|
|
|
70
70
|
import { clearTodos, getTodosForSession, setTodosForSession } from './tools/todo.js';
|
|
71
71
|
import { initializeMcp, closeMcp, getConnectedMcpServers } from './mcp.js';
|
|
72
72
|
import { generateSystemPrompt } from './system-prompt.js';
|
|
73
|
+
import { renderFormattedText } from './utils/format-message.js';
|
|
73
74
|
// ─── Scrollback helpers ───
|
|
74
75
|
// These functions append text to the permanent scrollback buffer via the
|
|
75
76
|
// <Static> component. Ink flushes new Static items within its own render
|
|
76
77
|
// cycle, so there are no timing issues with write()/log-update.
|
|
77
78
|
function printBanner(addStatic) {
|
|
78
|
-
|
|
79
|
-
const reset = '\x1b[0m';
|
|
80
|
-
addStatic([
|
|
81
|
-
`${green}█▀█ █▀█ █▀█ ▀█▀ █▀█ ▄▀█ █▀▀ █▀▀ █▄ █ ▀█▀${reset}`,
|
|
82
|
-
`${green}█▀▀ █▀▄ █▄█ █ █▄█ █▀█ █▄█ ██▄ █ ▀█ █${reset}`,
|
|
83
|
-
'',
|
|
84
|
-
].join('\n'));
|
|
79
|
+
addStatic(_jsxs(Text, { children: [_jsx(Text, { color: "#09A469", children: "\u2588\u2580\u2588 \u2588\u2580\u2588 \u2588\u2580\u2588 \u2580\u2588\u2580 \u2588\u2580\u2588 \u2584\u2580\u2588 \u2588\u2580\u2580 \u2588\u2580\u2580 \u2588\u2584 \u2588 \u2580\u2588\u2580" }), '\n', _jsx(Text, { color: "#09A469", children: "\u2588\u2580\u2580 \u2588\u2580\u2584 \u2588\u2584\u2588 \u2588 \u2588\u2584\u2588 \u2588\u2580\u2588 \u2588\u2584\u2588 \u2588\u2588\u2584 \u2588 \u2580\u2588 \u2588" }), '\n'] }));
|
|
85
80
|
}
|
|
86
81
|
function printRuntimeHeader(addStatic, config, session, dangerouslySkipPermissions) {
|
|
87
82
|
const provider = getProvider(config.provider);
|
|
@@ -90,21 +85,20 @@ function printRuntimeHeader(addStatic, config, session, dangerouslySkipPermissio
|
|
|
90
85
|
line += ' (auto-approve all)';
|
|
91
86
|
if (session)
|
|
92
87
|
line += ` | Session: ${session.id}`;
|
|
93
|
-
|
|
88
|
+
const lines = [_jsx(Text, { dimColor: true, children: line }, "model")];
|
|
94
89
|
const logFilePath = logger.getLogFilePath();
|
|
95
90
|
if (logFilePath) {
|
|
96
|
-
|
|
91
|
+
lines.push(_jsxs(Text, { dimColor: true, children: ["Debug logs: ", logFilePath] }, "log"));
|
|
97
92
|
}
|
|
98
93
|
const configPath = getActiveRuntimeConfigPath();
|
|
99
94
|
if (configPath) {
|
|
100
|
-
|
|
95
|
+
lines.push(_jsxs(Text, { dimColor: true, children: ["Config file: ", configPath] }, "config"));
|
|
101
96
|
}
|
|
102
97
|
const mcpServers = getConnectedMcpServers();
|
|
103
98
|
if (mcpServers.length > 0) {
|
|
104
|
-
|
|
99
|
+
lines.push(_jsxs(Text, { dimColor: true, children: ["MCPs: ", mcpServers.join(', ')] }, "mcp"));
|
|
105
100
|
}
|
|
106
|
-
|
|
107
|
-
addStatic(text);
|
|
101
|
+
addStatic(_jsxs(Text, { children: [lines.map((l, i) => _jsxs(React.Fragment, { children: [l, '\n'] }, i)), '\n'] }));
|
|
108
102
|
}
|
|
109
103
|
function normalizeTranscriptText(text) {
|
|
110
104
|
const normalized = text.replace(/\r\n/g, '\n');
|
|
@@ -118,14 +112,15 @@ function normalizeTranscriptText(text) {
|
|
|
118
112
|
function printMessageToScrollback(addStatic, role, text) {
|
|
119
113
|
const normalized = normalizeTranscriptText(text);
|
|
120
114
|
if (!normalized) {
|
|
121
|
-
addStatic('\n');
|
|
115
|
+
addStatic(_jsx(Text, { children: '\n' }));
|
|
122
116
|
return;
|
|
123
117
|
}
|
|
124
118
|
if (role === 'user') {
|
|
125
|
-
addStatic(
|
|
119
|
+
addStatic(_jsxs(Text, { children: [_jsx(Text, { color: "green", children: '>' }), " ", normalized, '\n'] }));
|
|
126
120
|
return;
|
|
127
121
|
}
|
|
128
|
-
|
|
122
|
+
// Apply Markdown formatting (bold, italic) to assistant messages
|
|
123
|
+
addStatic(_jsxs(Text, { children: [renderFormattedText(normalized), '\n'] }));
|
|
129
124
|
}
|
|
130
125
|
/**
|
|
131
126
|
* Format a sub-agent tool call into a human-readable activity string.
|
|
@@ -200,11 +195,11 @@ function replayMessagesToScrollback(addStatic, messages) {
|
|
|
200
195
|
if (message.role === 'tool') {
|
|
201
196
|
const toolName = msgAny.name || 'tool';
|
|
202
197
|
const compact = String(msgAny.content || '').replace(/\s+/g, ' ').trim().slice(0, 180);
|
|
203
|
-
addStatic(
|
|
198
|
+
addStatic(_jsxs(Text, { dimColor: true, children: ['▶ ', toolName, ': ', compact, '\n'] }));
|
|
204
199
|
}
|
|
205
200
|
}
|
|
206
201
|
if (messages.length > 0) {
|
|
207
|
-
addStatic('\n');
|
|
202
|
+
addStatic(_jsx(Text, { children: '\n' }));
|
|
208
203
|
}
|
|
209
204
|
}
|
|
210
205
|
// Returns only the last N displayable lines of text so the live streaming box
|
|
@@ -342,10 +337,10 @@ export const App = ({ dangerouslySkipPermissions = false, logLevel, sessionId, }
|
|
|
342
337
|
// collisions if multiple App instances ever coexist.
|
|
343
338
|
const staticCounterRef = useRef(0);
|
|
344
339
|
const [staticItems, setStaticItems] = useState([]);
|
|
345
|
-
const addStatic = useCallback((
|
|
340
|
+
const addStatic = useCallback((node) => {
|
|
346
341
|
staticCounterRef.current += 1;
|
|
347
342
|
const id = `s${staticCounterRef.current}`;
|
|
348
|
-
setStaticItems((prev) => [...prev, { id,
|
|
343
|
+
setStaticItems((prev) => [...prev, { id, node }]);
|
|
349
344
|
}, []);
|
|
350
345
|
// Core state
|
|
351
346
|
const [config, setConfig] = useState(null);
|
|
@@ -384,6 +379,13 @@ export const App = ({ dangerouslySkipPermissions = false, logLevel, sessionId, }
|
|
|
384
379
|
const assistantMessageRef = useRef(null);
|
|
385
380
|
// Abort controller for cancelling the current completion
|
|
386
381
|
const abortControllerRef = useRef(null);
|
|
382
|
+
// Buffer for streaming text that accumulates content and flushes complete lines to static
|
|
383
|
+
// This prevents the live streaming area from growing unbounded - complete lines are
|
|
384
|
+
// immediately flushed to <Static>, only the incomplete final line stays in the dynamic frame
|
|
385
|
+
const streamingBufferRef = useRef({
|
|
386
|
+
unflushedContent: '',
|
|
387
|
+
hasFlushedAnyLine: false,
|
|
388
|
+
});
|
|
387
389
|
const didPrintIntroRef = useRef(false);
|
|
388
390
|
const printedThreadErrorIdsRef = useRef(new Set());
|
|
389
391
|
// ─── Post-config initialization (reused after inline setup) ───
|
|
@@ -443,7 +445,7 @@ export const App = ({ dangerouslySkipPermissions = false, logLevel, sessionId, }
|
|
|
443
445
|
}, [loading]);
|
|
444
446
|
useEffect(() => {
|
|
445
447
|
if (error) {
|
|
446
|
-
addStatic(
|
|
448
|
+
addStatic(_jsxs(Text, { color: "red", children: ["Error: ", error] }));
|
|
447
449
|
}
|
|
448
450
|
}, [error, addStatic]);
|
|
449
451
|
useEffect(() => {
|
|
@@ -452,7 +454,7 @@ export const App = ({ dangerouslySkipPermissions = false, logLevel, sessionId, }
|
|
|
452
454
|
continue;
|
|
453
455
|
}
|
|
454
456
|
printedThreadErrorIdsRef.current.add(threadError.id);
|
|
455
|
-
addStatic(
|
|
457
|
+
addStatic(_jsxs(Text, { color: "red", children: ["Error: ", threadError.message] }));
|
|
456
458
|
}
|
|
457
459
|
}, [threadErrors, addStatic]);
|
|
458
460
|
useEffect(() => {
|
|
@@ -556,8 +558,9 @@ export const App = ({ dangerouslySkipPermissions = false, logLevel, sessionId, }
|
|
|
556
558
|
setError(null);
|
|
557
559
|
setHelpMessage(null);
|
|
558
560
|
setThreadErrors([]);
|
|
559
|
-
// Reset turn tracking
|
|
561
|
+
// Reset turn tracking and streaming buffer
|
|
560
562
|
assistantMessageRef.current = null;
|
|
563
|
+
streamingBufferRef.current = { unflushedContent: '', hasFlushedAnyLine: false };
|
|
561
564
|
// Print the user message directly to scrollback so it is selectable/copyable.
|
|
562
565
|
// We still push it into completionMessages for session saving.
|
|
563
566
|
const userMessage = { role: 'user', content: trimmed };
|
|
@@ -573,23 +576,56 @@ export const App = ({ dangerouslySkipPermissions = false, logLevel, sessionId, }
|
|
|
573
576
|
const updatedMessages = await runAgenticLoop(clientRef.current, config.model, [...completionMessages, userMessage], trimmed, (event) => {
|
|
574
577
|
switch (event.type) {
|
|
575
578
|
case 'text_delta': {
|
|
576
|
-
|
|
577
|
-
//
|
|
578
|
-
// streaming box + input) so setState here does NOT trigger
|
|
579
|
-
// clearTerminal. At 'done' the full text is flushed to <Static>.
|
|
579
|
+
const deltaText = event.content || '';
|
|
580
|
+
// First text delta of this turn: initialize ref, show streaming indicator.
|
|
580
581
|
if (!assistantMessageRef.current || assistantMessageRef.current.kind !== 'streaming_text') {
|
|
581
|
-
//
|
|
582
|
-
const
|
|
582
|
+
// Trim leading whitespace from first delta - LLMs often output leading \n or spaces
|
|
583
|
+
const trimmedDelta = deltaText.replace(/^\s+/, '');
|
|
584
|
+
const assistantMsg = { role: 'assistant', content: trimmedDelta, tool_calls: [] };
|
|
583
585
|
const idx = completionMessages.length + 1;
|
|
584
586
|
assistantMessageRef.current = { message: assistantMsg, index: idx, kind: 'streaming_text' };
|
|
585
587
|
setIsStreaming(true);
|
|
586
|
-
setStreamingText(event.content || '');
|
|
587
588
|
setCompletionMessages((prev) => [...prev, assistantMsg]);
|
|
589
|
+
// Initialize the streaming buffer and process the first chunk
|
|
590
|
+
// through the same split logic as subsequent deltas for consistency
|
|
591
|
+
const buffer = { unflushedContent: trimmedDelta, hasFlushedAnyLine: false };
|
|
592
|
+
streamingBufferRef.current = buffer;
|
|
593
|
+
// Process the first chunk: split on newlines and flush complete lines
|
|
594
|
+
const lines = buffer.unflushedContent.split('\n');
|
|
595
|
+
if (lines.length > 1) {
|
|
596
|
+
const completeLines = lines.slice(0, -1);
|
|
597
|
+
const textToFlush = completeLines.join('\n');
|
|
598
|
+
if (textToFlush) {
|
|
599
|
+
addStatic(renderFormattedText(textToFlush));
|
|
600
|
+
buffer.hasFlushedAnyLine = true;
|
|
601
|
+
}
|
|
602
|
+
buffer.unflushedContent = lines[lines.length - 1];
|
|
603
|
+
}
|
|
604
|
+
setStreamingText(buffer.unflushedContent);
|
|
588
605
|
}
|
|
589
606
|
else {
|
|
590
|
-
// Subsequent deltas — append to ref
|
|
591
|
-
assistantMessageRef.current.message.content +=
|
|
592
|
-
|
|
607
|
+
// Subsequent deltas — append to ref and buffer, then flush complete lines
|
|
608
|
+
assistantMessageRef.current.message.content += deltaText;
|
|
609
|
+
// Accumulate in buffer and flush complete lines to static
|
|
610
|
+
const buffer = streamingBufferRef.current;
|
|
611
|
+
buffer.unflushedContent += deltaText;
|
|
612
|
+
// Split on newlines to find complete lines
|
|
613
|
+
const lines = buffer.unflushedContent.split('\n');
|
|
614
|
+
// If we have more than 1 element, there were newlines
|
|
615
|
+
if (lines.length > 1) {
|
|
616
|
+
// All lines except the last one are complete (ended with \n)
|
|
617
|
+
const completeLines = lines.slice(0, -1);
|
|
618
|
+
// Build the text to flush - each complete line gets a newline added back
|
|
619
|
+
const textToFlush = completeLines.join('\n');
|
|
620
|
+
if (textToFlush) {
|
|
621
|
+
addStatic(renderFormattedText(textToFlush));
|
|
622
|
+
buffer.hasFlushedAnyLine = true;
|
|
623
|
+
}
|
|
624
|
+
// Keep only the last (incomplete) line in the buffer
|
|
625
|
+
buffer.unflushedContent = lines[lines.length - 1];
|
|
626
|
+
}
|
|
627
|
+
// Show the incomplete line (if any) in the dynamic frame
|
|
628
|
+
setStreamingText(buffer.unflushedContent);
|
|
593
629
|
}
|
|
594
630
|
break;
|
|
595
631
|
}
|
|
@@ -613,14 +649,19 @@ export const App = ({ dangerouslySkipPermissions = false, logLevel, sessionId, }
|
|
|
613
649
|
const toolCall = event.toolCall;
|
|
614
650
|
setActiveTool(toolCall.name);
|
|
615
651
|
// If the model streamed some text before invoking this tool,
|
|
616
|
-
// flush
|
|
617
|
-
//
|
|
618
|
-
//
|
|
652
|
+
// flush any remaining unflushed content to <Static> now.
|
|
653
|
+
// The streaming buffer contains text that hasn't been flushed yet
|
|
654
|
+
// (the incomplete final line). We need to flush it before the tool call.
|
|
619
655
|
if (assistantMessageRef.current?.kind === 'streaming_text') {
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
656
|
+
const buffer = streamingBufferRef.current;
|
|
657
|
+
// Flush any remaining unflushed content
|
|
658
|
+
if (buffer.unflushedContent) {
|
|
659
|
+
addStatic(renderFormattedText(buffer.unflushedContent));
|
|
623
660
|
}
|
|
661
|
+
// Add spacing after the streamed text and before the tool call
|
|
662
|
+
addStatic(renderFormattedText('\n'));
|
|
663
|
+
// Reset streaming state and buffer
|
|
664
|
+
streamingBufferRef.current = { unflushedContent: '', hasFlushedAnyLine: false };
|
|
624
665
|
setIsStreaming(false);
|
|
625
666
|
setStreamingText('');
|
|
626
667
|
assistantMessageRef.current = null;
|
|
@@ -679,7 +720,7 @@ export const App = ({ dangerouslySkipPermissions = false, logLevel, sessionId, }
|
|
|
679
720
|
.replace(/\s+/g, ' ')
|
|
680
721
|
.trim()
|
|
681
722
|
.slice(0, 180);
|
|
682
|
-
addStatic(
|
|
723
|
+
addStatic(_jsxs(Text, { dimColor: true, children: ['▶ ', toolCall.name, ': ', compactResult, '\n'] }));
|
|
683
724
|
// Flush the assistant message + tool result into completionMessages
|
|
684
725
|
// for session saving.
|
|
685
726
|
setCompletionMessages((prev) => {
|
|
@@ -746,14 +787,32 @@ export const App = ({ dangerouslySkipPermissions = false, logLevel, sessionId, }
|
|
|
746
787
|
case 'done':
|
|
747
788
|
if (assistantMessageRef.current?.kind === 'streaming_text') {
|
|
748
789
|
const finalRef = assistantMessageRef.current;
|
|
749
|
-
|
|
750
|
-
//
|
|
751
|
-
|
|
752
|
-
if (
|
|
753
|
-
|
|
790
|
+
const buffer = streamingBufferRef.current;
|
|
791
|
+
// Flush any remaining unflushed content from the buffer
|
|
792
|
+
// This is the final incomplete line that was being displayed live
|
|
793
|
+
if (buffer.unflushedContent) {
|
|
794
|
+
// If we've already flushed some lines, just append the remainder
|
|
795
|
+
// Otherwise, normalize and flush the full content
|
|
796
|
+
if (buffer.hasFlushedAnyLine) {
|
|
797
|
+
addStatic(renderFormattedText(buffer.unflushedContent));
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
// Nothing was flushed yet, normalize the full content
|
|
801
|
+
const normalized = normalizeTranscriptText(finalRef.message.content || '');
|
|
802
|
+
if (normalized) {
|
|
803
|
+
addStatic(renderFormattedText(normalized));
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
// Add final spacing after the streamed text
|
|
808
|
+
// Always add one newline - the user message adds another for blank line separation
|
|
809
|
+
if (buffer.unflushedContent) {
|
|
810
|
+
addStatic(renderFormattedText('\n'));
|
|
754
811
|
}
|
|
812
|
+
// Clear streaming state and buffer
|
|
755
813
|
setIsStreaming(false);
|
|
756
814
|
setStreamingText('');
|
|
815
|
+
streamingBufferRef.current = { unflushedContent: '', hasFlushedAnyLine: false };
|
|
757
816
|
setCompletionMessages((prev) => {
|
|
758
817
|
const updated = [...prev];
|
|
759
818
|
updated[finalRef.index] = { ...finalRef.message };
|
|
@@ -799,11 +858,11 @@ export const App = ({ dangerouslySkipPermissions = false, logLevel, sessionId, }
|
|
|
799
858
|
}
|
|
800
859
|
});
|
|
801
860
|
// ─── Render ───
|
|
802
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: staticItems, children: (item) => (_jsx(Text, { children: item.
|
|
861
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: staticItems, children: (item) => (_jsx(Text, { children: item.node }, item.id)) }), helpMessage && (_jsx(LeftBar, { color: "green", marginTop: 1, marginBottom: 1, children: _jsx(Text, { children: helpMessage }) })), !initialized && !error && !needsSetup && _jsx(Text, { children: "Initializing..." }), needsSetup && (_jsx(InlineSetup, { onComplete: (newConfig) => {
|
|
803
862
|
initializeWithConfig(newConfig).catch((err) => {
|
|
804
863
|
setError(`Initialization failed: ${err.message}`);
|
|
805
864
|
});
|
|
806
|
-
} })), isStreaming && (_jsxs(Text, { wrap: "wrap", children: [clipToRows(streamingText, terminalRows), _jsx(Text, { dimColor: true, children: "\u258D" })] })), threadErrors.filter((threadError) => threadError.transient).map((threadError) => (_jsx(LeftBar, { color: "gray", marginBottom: 1, children: _jsx(Text, { color: "gray", children: threadError.message }) }, `thread-error-${threadError.id}`))), pendingApproval && (_jsx(ApprovalPrompt, { request: pendingApproval.request, onRespond: (response) => {
|
|
865
|
+
} })), isStreaming && (_jsxs(Text, { wrap: "wrap", children: [renderFormattedText(clipToRows(streamingText, terminalRows)), _jsx(Text, { dimColor: true, children: "\u258D" })] })), threadErrors.filter((threadError) => threadError.transient).map((threadError) => (_jsx(LeftBar, { color: "gray", marginBottom: 1, children: _jsx(Text, { color: "gray", children: threadError.message }) }, `thread-error-${threadError.id}`))), pendingApproval && (_jsx(ApprovalPrompt, { request: pendingApproval.request, onRespond: (response) => {
|
|
807
866
|
pendingApproval.resolve(response);
|
|
808
867
|
setPendingApproval(null);
|
|
809
868
|
} })), initialized && !pendingApproval && loading && !isStreaming && (_jsx(Box, { children: _jsxs(Text, { color: "green", bold: true, children: [SPINNER_FRAMES[spinnerFrame], ' ', activeTool ? `Running ${activeTool}...` : 'Working...'] }) })), initialized && !pendingApproval && inputText.startsWith('/') && (_jsx(CommandFilter, { inputText: inputText })), _jsx(UsageDisplay, { usage: lastUsage ?? null, totalCost: totalCost }), initialized && !pendingApproval && (_jsx(Box, { 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) })] }) })), 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] })] }))] }));
|
package/dist/agentic-loop.js
CHANGED
|
@@ -281,8 +281,10 @@ export async function runAgenticLoop(client, model, messages, userInput, onEvent
|
|
|
281
281
|
let contextRetryCount = 0;
|
|
282
282
|
let retriggerCount = 0;
|
|
283
283
|
let truncateRetryCount = 0;
|
|
284
|
+
let continueRetryCount = 0;
|
|
284
285
|
const MAX_RETRIGGERS = 3;
|
|
285
286
|
const MAX_TRUNCATE_RETRIES = 5;
|
|
287
|
+
const MAX_CONTINUE_RETRIES = 1;
|
|
286
288
|
const validToolNames = getValidToolNames();
|
|
287
289
|
while (iterationCount < maxIterations) {
|
|
288
290
|
// Check if abort was requested
|
|
@@ -714,6 +716,21 @@ export async function runAgenticLoop(client, model, messages, userInput, onEvent
|
|
|
714
716
|
continue;
|
|
715
717
|
}
|
|
716
718
|
}
|
|
719
|
+
// After truncation retries exhausted, try adding a "continue" message
|
|
720
|
+
if (continueRetryCount < MAX_CONTINUE_RETRIES) {
|
|
721
|
+
continueRetryCount++;
|
|
722
|
+
updatedMessages.push({ role: 'user', content: 'continue' });
|
|
723
|
+
logger.warn('400 error: adding "continue" message to retry', {
|
|
724
|
+
continueRetryCount,
|
|
725
|
+
messageCount: updatedMessages.length,
|
|
726
|
+
});
|
|
727
|
+
onEvent({
|
|
728
|
+
type: 'error',
|
|
729
|
+
error: 'Request failed. Retrying with "continue"...',
|
|
730
|
+
transient: true,
|
|
731
|
+
});
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
717
734
|
}
|
|
718
735
|
// Handle context-window-exceeded (prompt too long) — attempt forced compaction
|
|
719
736
|
// This fires when our token estimate was too low (e.g. base64 images from MCP tools)
|
|
@@ -778,7 +795,7 @@ export async function runAgenticLoop(client, model, messages, userInput, onEvent
|
|
|
778
795
|
if (apiError?.status === 400) {
|
|
779
796
|
onEvent({
|
|
780
797
|
type: 'error',
|
|
781
|
-
error:
|
|
798
|
+
error: `Request failed: ${errMsg}\n\nThe conversation history could not be automatically repaired. Try /clear to start fresh.`,
|
|
782
799
|
transient: false,
|
|
783
800
|
});
|
|
784
801
|
onEvent({ type: 'done' });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import {
|
|
3
|
+
import { renderFormattedText } from '../utils/format-message.js';
|
|
4
4
|
import { LeftBar } from './LeftBar.js';
|
|
5
5
|
export const DEFERRED_TABLE_PLACEHOLDER = 'table loading';
|
|
6
6
|
const graphemeSegmenter = typeof Intl !== 'undefined' && 'Segmenter' in Intl
|
|
@@ -165,6 +165,6 @@ export const FormattedMessage = ({ content, deferTables = false }) => {
|
|
|
165
165
|
// Text Block
|
|
166
166
|
if (!block.content.trim())
|
|
167
167
|
return null;
|
|
168
|
-
return (_jsx(Box, { marginBottom: 0, children: _jsx(Text, { children:
|
|
168
|
+
return (_jsx(Box, { marginBottom: 0, children: _jsx(Text, { children: renderFormattedText(block.content) }) }, index));
|
|
169
169
|
}) }));
|
|
170
170
|
};
|
package/dist/system-prompt.js
CHANGED
|
@@ -130,7 +130,8 @@ GUIDELINES
|
|
|
130
130
|
|
|
131
131
|
OUTPUT FORMAT:
|
|
132
132
|
- You are running in a terminal. Be concise. Optimise for scannability.
|
|
133
|
-
-
|
|
133
|
+
- Use **bold** and *italic* formatting tastefully to highlight key points and structure your responses.
|
|
134
|
+
- Do NOT use # headers, --- dividers, or other structural Markdown.
|
|
134
135
|
- Do NOT use Markdown code fences (backticks) unless the content is actual code or a command.
|
|
135
136
|
- For structured data, use plain text with aligned columns (spaces, not pipes/dashes).
|
|
136
137
|
- Keep tables compact: narrower columns, minimal padding. Wrap cell content rather than making very wide tables.
|
|
@@ -1,26 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
function parseSegments(text) {
|
|
4
|
+
const segments = [];
|
|
5
|
+
// Strip markdown headers
|
|
6
|
+
const cleaned = text.replace(/^#+\s+/gm, '');
|
|
7
|
+
// Pattern to match ***bold italic***, **bold**, *italic*
|
|
8
|
+
const pattern = /(\*\*\*[^*]+?\*\*\*|\*\*[^*]+?\*\*|\*[^\s*][^*]*?\*)/g;
|
|
9
|
+
let lastIndex = 0;
|
|
10
|
+
let match;
|
|
11
|
+
while ((match = pattern.exec(cleaned)) !== null) {
|
|
12
|
+
// Add plain text before match
|
|
13
|
+
if (match.index > lastIndex) {
|
|
14
|
+
segments.push({ text: cleaned.slice(lastIndex, match.index) });
|
|
15
|
+
}
|
|
16
|
+
const fullMatch = match[0];
|
|
17
|
+
let content;
|
|
18
|
+
let bold = false;
|
|
19
|
+
let italic = false;
|
|
20
|
+
if (fullMatch.startsWith('***')) {
|
|
21
|
+
content = fullMatch.slice(3, -3);
|
|
22
|
+
bold = true;
|
|
23
|
+
italic = true;
|
|
24
|
+
}
|
|
25
|
+
else if (fullMatch.startsWith('**')) {
|
|
26
|
+
content = fullMatch.slice(2, -2);
|
|
27
|
+
bold = true;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
content = fullMatch.slice(1, -1);
|
|
31
|
+
italic = true;
|
|
32
|
+
}
|
|
33
|
+
segments.push({ text: content, bold, italic });
|
|
34
|
+
lastIndex = pattern.lastIndex;
|
|
35
|
+
}
|
|
36
|
+
// Add remaining plain text
|
|
37
|
+
if (lastIndex < cleaned.length) {
|
|
38
|
+
segments.push({ text: cleaned.slice(lastIndex) });
|
|
39
|
+
}
|
|
40
|
+
return segments.length > 0 ? segments : [{ text: cleaned }];
|
|
41
|
+
}
|
|
1
42
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Supports:
|
|
5
|
-
* - **bold** → bold text
|
|
6
|
-
* - *italic* → italic text
|
|
7
|
-
* - ***bold italic*** → bold + italic text
|
|
8
|
-
*
|
|
9
|
-
* Returns a string with ANSI escape codes that Ink will render with styling.
|
|
43
|
+
* Render formatted text as Ink Text elements.
|
|
44
|
+
* Returns an array of <Text> components that can be nested inside a parent <Text>.
|
|
10
45
|
*/
|
|
11
|
-
export function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// Strip markdown hashtags (headers)
|
|
18
|
-
result = result.replace(/^#+\s+/gm, '');
|
|
19
|
-
// Replace ***bold italic*** first (to avoid matching ** or * inside)
|
|
20
|
-
result = result.replace(/\*\*\*(.+?)\*\*\*/g, `${BOLD}${ITALIC}$1${RESET}`);
|
|
21
|
-
// Replace **bold**
|
|
22
|
-
result = result.replace(/\*\*(.+?)\*\*/g, `${BOLD}$1${RESET}`);
|
|
23
|
-
// Replace *italic*
|
|
24
|
-
result = result.replace(/\*(.+?)\*/g, `${ITALIC}$1${RESET}`);
|
|
25
|
-
return result;
|
|
46
|
+
export function renderFormattedText(text) {
|
|
47
|
+
const segments = parseSegments(text);
|
|
48
|
+
if (segments.length === 1 && !segments[0].bold && !segments[0].italic) {
|
|
49
|
+
return segments[0].text;
|
|
50
|
+
}
|
|
51
|
+
return segments.map((seg, i) => (_jsx(Text, { bold: seg.bold, italic: seg.italic, children: seg.text }, i)));
|
|
26
52
|
}
|