@kirosnn/mosaic 0.71.0 → 0.74.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/README.md +1 -5
- package/package.json +4 -2
- package/src/agent/Agent.ts +353 -131
- package/src/agent/context.ts +4 -4
- package/src/agent/prompts/systemPrompt.ts +15 -6
- package/src/agent/prompts/toolsPrompt.ts +136 -10
- package/src/agent/provider/anthropic.ts +100 -100
- package/src/agent/provider/google.ts +102 -102
- package/src/agent/provider/mistral.ts +95 -95
- package/src/agent/provider/ollama.ts +77 -60
- package/src/agent/provider/openai.ts +42 -38
- package/src/agent/provider/rateLimit.ts +178 -0
- package/src/agent/provider/xai.ts +99 -99
- package/src/agent/tools/definitions.ts +19 -9
- package/src/agent/tools/executor.ts +95 -85
- package/src/agent/tools/exploreExecutor.ts +8 -10
- package/src/agent/tools/grep.ts +30 -29
- package/src/agent/tools/question.ts +7 -1
- package/src/agent/types.ts +9 -8
- package/src/components/App.tsx +45 -45
- package/src/components/CustomInput.tsx +214 -36
- package/src/components/Main.tsx +552 -339
- package/src/components/Setup.tsx +1 -1
- package/src/components/Welcome.tsx +1 -1
- package/src/components/main/ApprovalPanel.tsx +4 -3
- package/src/components/main/ChatPage.tsx +858 -675
- package/src/components/main/HomePage.tsx +53 -38
- package/src/components/main/QuestionPanel.tsx +52 -7
- package/src/components/main/ThinkingIndicator.tsx +2 -1
- package/src/index.tsx +50 -20
- package/src/mcp/approvalPolicy.ts +156 -0
- package/src/mcp/cli/add.ts +185 -0
- package/src/mcp/cli/doctor.ts +74 -0
- package/src/mcp/cli/index.ts +85 -0
- package/src/mcp/cli/list.ts +50 -0
- package/src/mcp/cli/logs.ts +24 -0
- package/src/mcp/cli/manage.ts +99 -0
- package/src/mcp/cli/show.ts +53 -0
- package/src/mcp/cli/tools.ts +77 -0
- package/src/mcp/config.ts +234 -0
- package/src/mcp/index.ts +80 -0
- package/src/mcp/processManager.ts +304 -0
- package/src/mcp/rateLimiter.ts +50 -0
- package/src/mcp/registry.ts +151 -0
- package/src/mcp/schemaConverter.ts +100 -0
- package/src/mcp/servers/navigation/browser.ts +151 -0
- package/src/mcp/servers/navigation/index.ts +23 -0
- package/src/mcp/servers/navigation/tools.ts +263 -0
- package/src/mcp/servers/navigation/types.ts +17 -0
- package/src/mcp/servers/navigation/utils.ts +20 -0
- package/src/mcp/toolCatalog.ts +182 -0
- package/src/mcp/types.ts +116 -0
- package/src/utils/approvalBridge.ts +17 -5
- package/src/utils/commands/compact.ts +30 -0
- package/src/utils/commands/echo.ts +1 -1
- package/src/utils/commands/index.ts +4 -6
- package/src/utils/commands/new.ts +15 -0
- package/src/utils/commands/types.ts +3 -0
- package/src/utils/config.ts +3 -1
- package/src/utils/diffRendering.tsx +1 -3
- package/src/utils/exploreBridge.ts +10 -0
- package/src/utils/markdown.tsx +220 -122
- package/src/utils/models.ts +31 -9
- package/src/utils/questionBridge.ts +36 -1
- package/src/utils/tokenEstimator.ts +32 -0
- package/src/utils/toolFormatting.ts +317 -7
- package/src/web/app.tsx +72 -72
- package/src/web/components/HomePage.tsx +7 -7
- package/src/web/components/MessageItem.tsx +66 -35
- package/src/web/components/QuestionPanel.tsx +72 -12
- package/src/web/components/Sidebar.tsx +0 -2
- package/src/web/components/ThinkingIndicator.tsx +1 -0
- package/src/web/server.tsx +767 -683
- package/src/utils/commands/redo.ts +0 -74
- package/src/utils/commands/sessions.ts +0 -129
- package/src/utils/commands/undo.ts +0 -75
- package/src/utils/undoRedo.ts +0 -429
- package/src/utils/undoRedoBridge.ts +0 -45
- package/src/utils/undoRedoDb.ts +0 -338
package/src/components/Main.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useState, useEffect, useRef } from "react";
|
|
2
|
-
import type { ImagePart, TextPart, UserContent } from "ai";
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import type { ImagePart, TextPart, UserContent } from "ai";
|
|
3
3
|
import { useKeyboard } from "@opentui/react";
|
|
4
4
|
import { Agent } from "../agent";
|
|
5
5
|
import { saveConversation, addInputToHistory, type ConversationHistory, type ConversationStep } from "../utils/history";
|
|
@@ -9,19 +9,20 @@ import { initializeCommands, isCommand, executeCommand } from '../utils/commands
|
|
|
9
9
|
import type { InputSubmitMeta } from './CustomInput';
|
|
10
10
|
|
|
11
11
|
import { subscribeQuestion, type QuestionRequest } from "../utils/questionBridge";
|
|
12
|
-
import { subscribeApprovalAccepted
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import
|
|
23
|
-
import {
|
|
24
|
-
|
|
12
|
+
import { subscribeApprovalAccepted } from "../utils/approvalBridge";
|
|
13
|
+
import { setExploreAbortController, setExploreToolCallback } from "../utils/exploreBridge";
|
|
14
|
+
import { getCurrentQuestion, cancelQuestion } from "../utils/questionBridge";
|
|
15
|
+
import { getCurrentApproval, cancelApproval } from "../utils/approvalBridge";
|
|
16
|
+
import { BLEND_WORDS, type MainProps, type Message } from "./main/types";
|
|
17
|
+
import { HomePage } from './main/HomePage';
|
|
18
|
+
import { ChatPage } from './main/ChatPage';
|
|
19
|
+
import type { ImageAttachment } from "../utils/images";
|
|
20
|
+
import { subscribeImageCommand, setImageSupport } from "../utils/imageBridge";
|
|
21
|
+
import { findModelsDevModelById, modelAcceptsImages, getModelsDevContextLimit } from "../utils/models";
|
|
22
|
+
import { DEFAULT_SYSTEM_PROMPT, processSystemPrompt } from "../agent/prompts/systemPrompt";
|
|
23
|
+
import { estimateTokensFromText, estimateTokensForContent, getDefaultContextBudget } from "../utils/tokenEstimator";
|
|
24
|
+
|
|
25
|
+
type CompactableMessage = Pick<Message, "role" | "content" | "thinkingContent" | "toolName">;
|
|
25
26
|
|
|
26
27
|
function extractTitle(content: string, alreadyResolved: boolean): { title: string | null; cleanContent: string; isPending: boolean; noTitle: boolean } {
|
|
27
28
|
const trimmed = content.trimStart();
|
|
@@ -49,6 +50,147 @@ function setTerminalTitle(title: string) {
|
|
|
49
50
|
process.title = `⁘ ${title}`;
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
export function normalizeWhitespace(text: string): string {
|
|
54
|
+
return text.replace(/\s+/g, " ").trim();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function truncateText(text: string, maxChars: number): string {
|
|
58
|
+
if (text.length <= maxChars) return text;
|
|
59
|
+
return text.slice(0, Math.max(0, maxChars - 3)) + "...";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function estimateTokensForMessage(message: CompactableMessage): number {
|
|
63
|
+
return estimateTokensForContent(message.content || "", message.thinkingContent || undefined);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function estimateTokensForMessages(messages: CompactableMessage[]): number {
|
|
67
|
+
return messages.reduce((sum, message) => sum + estimateTokensForMessage(message), 0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function estimateTotalTokens(messages: CompactableMessage[], systemPrompt: string): number {
|
|
71
|
+
const systemTokens = estimateTokensFromText(systemPrompt) + 8;
|
|
72
|
+
return systemTokens + estimateTokensForMessages(messages);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function shouldAutoCompact(totalTokens: number, maxContextTokens: number): boolean {
|
|
76
|
+
if (!Number.isFinite(maxContextTokens) || maxContextTokens <= 0) return false;
|
|
77
|
+
const threshold = Math.floor(maxContextTokens * 0.95);
|
|
78
|
+
return totalTokens >= threshold;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function summarizeMessage(message: CompactableMessage, isLastUser: boolean): string {
|
|
82
|
+
if (message.role === "tool") {
|
|
83
|
+
const name = message.toolName || "tool";
|
|
84
|
+
const text = message.content || "";
|
|
85
|
+
const isError = text.toLowerCase().includes('error') || text.toLowerCase().includes('failed');
|
|
86
|
+
const status = isError ? 'FAILED' : 'OK';
|
|
87
|
+
const cleaned = normalizeWhitespace(text);
|
|
88
|
+
return `[tool:${name} ${status}] ${truncateText(cleaned, 120)}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (message.role === "assistant") {
|
|
92
|
+
const cleaned = normalizeWhitespace(message.content || "");
|
|
93
|
+
const sentenceMatch = cleaned.match(/^[^.!?\n]{10,}[.!?]/);
|
|
94
|
+
const summary = sentenceMatch ? sentenceMatch[0] : cleaned;
|
|
95
|
+
return `assistant: ${truncateText(summary, 200)}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const cleaned = normalizeWhitespace(message.content || "");
|
|
99
|
+
const limit = isLastUser ? cleaned.length : 400;
|
|
100
|
+
return `user: ${truncateText(cleaned, limit)}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function buildSummary(messages: CompactableMessage[], maxTokens: number): string {
|
|
104
|
+
const maxChars = Math.max(0, maxTokens * 3);
|
|
105
|
+
const header = "Résumé de conversation (compact):";
|
|
106
|
+
let charCount = header.length + 1;
|
|
107
|
+
const lines: string[] = [];
|
|
108
|
+
|
|
109
|
+
let lastUserIndex = -1;
|
|
110
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
111
|
+
if (messages[i]!.role === 'user') { lastUserIndex = i; break; }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (let i = 0; i < messages.length; i++) {
|
|
115
|
+
if (charCount >= maxChars) break;
|
|
116
|
+
const line = `- ${summarizeMessage(messages[i]!, i === lastUserIndex)}`;
|
|
117
|
+
charCount += line.length + 1;
|
|
118
|
+
lines.push(line);
|
|
119
|
+
}
|
|
120
|
+
const body = lines.join("\n");
|
|
121
|
+
const full = `${header}\n${body}`.trim();
|
|
122
|
+
return truncateText(full, maxChars);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function collectContextFiles(messages: Message[]): string[] {
|
|
126
|
+
const files = new Set<string>();
|
|
127
|
+
for (const message of messages) {
|
|
128
|
+
if (message.role !== "tool") continue;
|
|
129
|
+
if (!message.toolArgs) continue;
|
|
130
|
+
const toolName = message.toolName || "";
|
|
131
|
+
if (!["read", "write", "edit", "list", "grep"].includes(toolName)) continue;
|
|
132
|
+
const path = message.toolArgs.path;
|
|
133
|
+
if (typeof path === "string" && path.trim()) {
|
|
134
|
+
files.add(path.trim());
|
|
135
|
+
}
|
|
136
|
+
const pattern = message.toolArgs.pattern;
|
|
137
|
+
if (toolName === "grep" && typeof pattern === "string" && pattern.trim()) {
|
|
138
|
+
files.add(pattern.trim());
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return Array.from(files.values()).sort((a, b) => a.localeCompare(b));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function appendContextFiles(summary: string, files: string[], maxTokens: number): string {
|
|
145
|
+
if (files.length === 0) return summary;
|
|
146
|
+
const maxChars = Math.max(0, maxTokens * 4);
|
|
147
|
+
const list = files.map(f => `- ${f}`).join("\n");
|
|
148
|
+
const block = `\n\nFichiers conservés après compaction:\n${list}`;
|
|
149
|
+
return truncateText(`${summary}${block}`, maxChars);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function compactMessagesForUi(
|
|
153
|
+
messages: Message[],
|
|
154
|
+
systemPrompt: string,
|
|
155
|
+
maxContextTokens: number,
|
|
156
|
+
createId: () => string,
|
|
157
|
+
summaryOnly: boolean
|
|
158
|
+
): { messages: Message[]; estimatedTokens: number; didCompact: boolean } {
|
|
159
|
+
const systemTokens = estimateTokensFromText(systemPrompt) + 8;
|
|
160
|
+
const totalTokens = systemTokens + estimateTokensForMessages(messages);
|
|
161
|
+
if (totalTokens <= maxContextTokens && !summaryOnly) {
|
|
162
|
+
return { messages, estimatedTokens: totalTokens - systemTokens, didCompact: false };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const summaryTokens = Math.min(2000, Math.max(400, Math.floor(maxContextTokens * 0.2)));
|
|
166
|
+
const recentBudget = Math.max(500, maxContextTokens - summaryTokens);
|
|
167
|
+
|
|
168
|
+
let recentTokens = 0;
|
|
169
|
+
const recent: Message[] = [];
|
|
170
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
171
|
+
const message = messages[i]!;
|
|
172
|
+
const msgTokens = estimateTokensForMessage(message);
|
|
173
|
+
if (recentTokens + msgTokens > recentBudget && recent.length > 0) break;
|
|
174
|
+
recent.unshift(message);
|
|
175
|
+
recentTokens += msgTokens;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const cutoff = messages.length - recent.length;
|
|
179
|
+
const older = cutoff > 0 ? messages.slice(0, cutoff) : [];
|
|
180
|
+
const files = collectContextFiles(messages);
|
|
181
|
+
const summaryBase = buildSummary(summaryOnly ? messages : (older.length > 0 ? older : messages), summaryTokens);
|
|
182
|
+
const summary = appendContextFiles(summaryBase, files, summaryTokens);
|
|
183
|
+
const summaryMessage: Message = {
|
|
184
|
+
id: createId(),
|
|
185
|
+
role: "assistant",
|
|
186
|
+
content: summary
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const nextMessages = summaryOnly ? [summaryMessage] : [summaryMessage, ...recent];
|
|
190
|
+
const estimatedTokens = estimateTokensForMessages(nextMessages);
|
|
191
|
+
return { messages: nextMessages, estimatedTokens, didCompact: true };
|
|
192
|
+
}
|
|
193
|
+
|
|
52
194
|
export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsOpen = false, commandsOpen = false, initialMessage }: MainProps) {
|
|
53
195
|
const [currentPage, setCurrentPage] = useState<"home" | "chat">(initialMessage ? "chat" : "home");
|
|
54
196
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
@@ -58,10 +200,10 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
58
200
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
59
201
|
const [terminalHeight, setTerminalHeight] = useState(process.stdout.rows || 24);
|
|
60
202
|
const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80);
|
|
61
|
-
const [questionRequest, setQuestionRequest] = useState<QuestionRequest | null>(null);
|
|
62
|
-
const [currentTitle, setCurrentTitle] = useState<string | null>(null);
|
|
63
|
-
const [pendingImages, setPendingImages] = useState<ImageAttachment[]>([]);
|
|
64
|
-
const [imagesSupported, setImagesSupported] = useState(false);
|
|
203
|
+
const [questionRequest, setQuestionRequest] = useState<QuestionRequest | null>(null);
|
|
204
|
+
const [currentTitle, setCurrentTitle] = useState<string | null>(null);
|
|
205
|
+
const [pendingImages, setPendingImages] = useState<ImageAttachment[]>([]);
|
|
206
|
+
const [imagesSupported, setImagesSupported] = useState(false);
|
|
65
207
|
const currentTitleRef = useRef<string | null>(null);
|
|
66
208
|
const titleExtractedRef = useRef(false);
|
|
67
209
|
const shouldAutoScroll = useRef(true);
|
|
@@ -71,37 +213,37 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
71
213
|
const commandsOpenRef = useRef(commandsOpen);
|
|
72
214
|
const questionRequestRef = useRef<QuestionRequest | null>(questionRequest);
|
|
73
215
|
const initialMessageProcessed = useRef(false);
|
|
216
|
+
const lastPromptTokensRef = useRef<number>(0);
|
|
74
217
|
const exploreMessageIdRef = useRef<string | null>(null);
|
|
75
218
|
const exploreToolsRef = useRef<Array<{ tool: string; info: string; success: boolean }>>([]);
|
|
76
219
|
const explorePurposeRef = useRef<string>('');
|
|
77
220
|
|
|
78
221
|
const createId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
79
222
|
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
initializeCommands();
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}, []);
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
initializeCommands();
|
|
225
|
+
}, []);
|
|
226
|
+
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
const loadSupport = async () => {
|
|
229
|
+
const config = readConfig();
|
|
230
|
+
if (!config.model) {
|
|
231
|
+
setImagesSupported(false);
|
|
232
|
+
setImageSupport(false);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const result = await findModelsDevModelById(config.model);
|
|
237
|
+
const supported = Boolean(result && result.model && modelAcceptsImages(result.model));
|
|
238
|
+
setImagesSupported(supported);
|
|
239
|
+
setImageSupport(supported);
|
|
240
|
+
} catch {
|
|
241
|
+
setImagesSupported(false);
|
|
242
|
+
setImageSupport(false);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
loadSupport();
|
|
246
|
+
}, []);
|
|
105
247
|
|
|
106
248
|
useEffect(() => {
|
|
107
249
|
let lastExploreTokens = 0;
|
|
@@ -139,41 +281,31 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
139
281
|
};
|
|
140
282
|
}, []);
|
|
141
283
|
|
|
142
|
-
useEffect(() => {
|
|
143
|
-
return
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
setPendingImages((prev) => [...prev, event.image]);
|
|
163
|
-
});
|
|
164
|
-
}, [imagesSupported]);
|
|
165
|
-
|
|
166
|
-
useEffect(() => {
|
|
167
|
-
if (!imagesSupported) {
|
|
168
|
-
setPendingImages([]);
|
|
169
|
-
}
|
|
170
|
-
}, [imagesSupported]);
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
return subscribeImageCommand((event) => {
|
|
286
|
+
if (event.type === "clear") {
|
|
287
|
+
setPendingImages([]);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (event.type === "remove") {
|
|
291
|
+
setPendingImages((prev) => prev.filter((img) => img.id !== event.id));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (!imagesSupported) return;
|
|
295
|
+
setPendingImages((prev) => [...prev, event.image]);
|
|
296
|
+
});
|
|
297
|
+
}, [imagesSupported]);
|
|
298
|
+
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
if (!imagesSupported) {
|
|
301
|
+
setPendingImages([]);
|
|
302
|
+
}
|
|
303
|
+
}, [imagesSupported]);
|
|
171
304
|
|
|
172
305
|
useEffect(() => {
|
|
173
306
|
const handleResize = () => {
|
|
174
307
|
const newWidth = process.stdout.columns || 80;
|
|
175
308
|
const newHeight = process.stdout.rows || 24;
|
|
176
|
-
const oldWidth = terminalWidth;
|
|
177
309
|
const oldHeight = terminalHeight;
|
|
178
310
|
|
|
179
311
|
setTerminalWidth(newWidth);
|
|
@@ -199,13 +331,27 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
199
331
|
useEffect(() => {
|
|
200
332
|
return subscribeApprovalAccepted((accepted) => {
|
|
201
333
|
const isBashTool = accepted.toolName === 'bash';
|
|
334
|
+
const isMcpTool = accepted.toolName.startsWith('mcp__');
|
|
202
335
|
|
|
203
|
-
if (isBashTool) {
|
|
336
|
+
if (isBashTool || isMcpTool) {
|
|
204
337
|
const { name: toolDisplayName, info: toolInfo } = parseToolHeader(accepted.toolName, accepted.args);
|
|
205
338
|
const runningContent = toolInfo ? `${toolDisplayName} (${toolInfo})` : toolDisplayName;
|
|
206
339
|
|
|
207
340
|
setMessages((prev: Message[]) => {
|
|
208
341
|
const newMessages = [...prev];
|
|
342
|
+
|
|
343
|
+
if (isMcpTool) {
|
|
344
|
+
const existingIdx = newMessages.findIndex(m => m.isRunning && m.toolName === accepted.toolName);
|
|
345
|
+
if (existingIdx !== -1) {
|
|
346
|
+
newMessages[existingIdx] = {
|
|
347
|
+
...newMessages[existingIdx]!,
|
|
348
|
+
content: runningContent,
|
|
349
|
+
runningStartTime: Date.now()
|
|
350
|
+
};
|
|
351
|
+
return newMessages;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
209
355
|
newMessages.push({
|
|
210
356
|
id: createId(),
|
|
211
357
|
role: "tool",
|
|
@@ -308,67 +454,98 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
308
454
|
}
|
|
309
455
|
}, [copyRequestId, onCopy, messages]);
|
|
310
456
|
|
|
311
|
-
useKeyboard((key) => {
|
|
312
|
-
if ((key.name === 'c' && key.ctrl) || key.sequence === '\x03') {
|
|
313
|
-
if (getCurrentQuestion()) {
|
|
314
|
-
cancelQuestion();
|
|
315
|
-
}
|
|
316
|
-
if (getCurrentApproval()) {
|
|
317
|
-
cancelApproval();
|
|
318
|
-
}
|
|
319
|
-
abortControllerRef.current?.abort();
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (key.name === 'escape') {
|
|
324
|
-
if (getCurrentQuestion()) {
|
|
325
|
-
cancelQuestion();
|
|
326
|
-
}
|
|
457
|
+
useKeyboard((key) => {
|
|
458
|
+
if ((key.name === 'c' && key.ctrl) || key.sequence === '\x03') {
|
|
459
|
+
if (getCurrentQuestion()) {
|
|
460
|
+
cancelQuestion();
|
|
461
|
+
}
|
|
462
|
+
if (getCurrentApproval()) {
|
|
463
|
+
cancelApproval();
|
|
464
|
+
}
|
|
465
|
+
abortControllerRef.current?.abort();
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (key.name === 'escape') {
|
|
470
|
+
if (getCurrentQuestion()) {
|
|
471
|
+
cancelQuestion();
|
|
472
|
+
}
|
|
327
473
|
if (getCurrentApproval()) {
|
|
328
474
|
cancelApproval();
|
|
329
475
|
}
|
|
330
476
|
abortControllerRef.current?.abort();
|
|
331
477
|
return;
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
const buildUserContent = (text: string, images?: ImageAttachment[]): UserContent => {
|
|
336
|
-
if (!images || images.length === 0) return text;
|
|
337
|
-
const parts: Array<TextPart | ImagePart> = [];
|
|
338
|
-
parts.push({ type: "text", text });
|
|
339
|
-
for (const img of images) {
|
|
340
|
-
parts.push({ type: "image", image: img.data, mimeType: img.mimeType });
|
|
341
|
-
}
|
|
342
|
-
return parts;
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
const buildConversationHistory = (base: Message[], includeImages: boolean) => {
|
|
346
|
-
return base
|
|
347
|
-
.filter((m): m is Message & { role: "user" | "assistant" } => m.role === "user" || m.role === "assistant")
|
|
348
|
-
.map((m) => {
|
|
349
|
-
if (m.role === "user") {
|
|
350
|
-
const content = includeImages ? buildUserContent(m.content, m.images) : m.content;
|
|
351
|
-
return { role: "user" as const, content };
|
|
352
|
-
}
|
|
353
|
-
return { role: "assistant" as const, content: m.content };
|
|
354
|
-
});
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
const handleSubmit = async (value: string, meta?: InputSubmitMeta) => {
|
|
358
|
-
if (isProcessing) return;
|
|
359
|
-
|
|
360
|
-
const hasPastedContent = Boolean(meta?.isPaste && meta.pastedContent);
|
|
361
|
-
const hasImages = imagesSupported && pendingImages.length > 0;
|
|
362
|
-
if (!value.trim() && !hasPastedContent && !hasImages) return;
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const buildUserContent = (text: string, images?: ImageAttachment[]): UserContent => {
|
|
482
|
+
if (!images || images.length === 0) return text;
|
|
483
|
+
const parts: Array<TextPart | ImagePart> = [];
|
|
484
|
+
parts.push({ type: "text", text });
|
|
485
|
+
for (const img of images) {
|
|
486
|
+
parts.push({ type: "image", image: img.data, mimeType: img.mimeType });
|
|
487
|
+
}
|
|
488
|
+
return parts;
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const buildConversationHistory = (base: Message[], includeImages: boolean) => {
|
|
492
|
+
return base
|
|
493
|
+
.filter((m): m is Message & { role: "user" | "assistant" } => m.role === "user" || m.role === "assistant")
|
|
494
|
+
.map((m) => {
|
|
495
|
+
if (m.role === "user") {
|
|
496
|
+
const content = includeImages ? buildUserContent(m.content, m.images) : m.content;
|
|
497
|
+
return { role: "user" as const, content };
|
|
498
|
+
}
|
|
499
|
+
return { role: "assistant" as const, content: m.content };
|
|
500
|
+
});
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const handleSubmit = async (value: string, meta?: InputSubmitMeta) => {
|
|
504
|
+
if (isProcessing) return;
|
|
505
|
+
|
|
506
|
+
const hasPastedContent = Boolean(meta?.isPaste && meta.pastedContent);
|
|
507
|
+
const hasImages = imagesSupported && pendingImages.length > 0;
|
|
508
|
+
if (!value.trim() && !hasPastedContent && !hasImages) return;
|
|
363
509
|
|
|
364
510
|
if (isCommand(value)) {
|
|
365
511
|
const result = await executeCommand(value);
|
|
366
512
|
if (result) {
|
|
513
|
+
if (result.shouldClearMessages === true) {
|
|
514
|
+
const commandMessage: Message = {
|
|
515
|
+
id: createId(),
|
|
516
|
+
role: "slash",
|
|
517
|
+
content: result.content,
|
|
518
|
+
isError: !result.success
|
|
519
|
+
};
|
|
520
|
+
setMessages([commandMessage]);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (result.shouldCompactMessages === true) {
|
|
525
|
+
const config = readConfig();
|
|
526
|
+
const rawSystemPrompt = config.systemPrompt || DEFAULT_SYSTEM_PROMPT;
|
|
527
|
+
const systemPrompt = processSystemPrompt(rawSystemPrompt, true);
|
|
528
|
+
let maxContextTokens = result.compactMaxTokens ?? config.maxContextTokens;
|
|
529
|
+
if (!maxContextTokens && config.provider && config.model) {
|
|
530
|
+
const resolved = await getModelsDevContextLimit(config.provider, config.model);
|
|
531
|
+
if (typeof resolved === "number") {
|
|
532
|
+
maxContextTokens = resolved;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const targetTokens = maxContextTokens ?? getDefaultContextBudget(config.provider);
|
|
536
|
+
let nextTokens = currentTokens;
|
|
537
|
+
setMessages(prev => {
|
|
538
|
+
const compacted = compactMessagesForUi(prev, systemPrompt, targetTokens, createId, true);
|
|
539
|
+
nextTokens = compacted.estimatedTokens;
|
|
540
|
+
return compacted.messages;
|
|
541
|
+
});
|
|
542
|
+
setCurrentTokens(nextTokens);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
367
546
|
if (result.shouldAddToHistory === true) {
|
|
368
547
|
addInputToHistory(value.trim());
|
|
369
548
|
|
|
370
|
-
saveState(messages);
|
|
371
|
-
|
|
372
549
|
const userMessage: Message = {
|
|
373
550
|
id: createId(),
|
|
374
551
|
role: "user",
|
|
@@ -381,6 +558,7 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
381
558
|
const localStartTime = Date.now();
|
|
382
559
|
setProcessingStartTime(localStartTime);
|
|
383
560
|
setCurrentTokens(0);
|
|
561
|
+
lastPromptTokensRef.current = 0;
|
|
384
562
|
shouldAutoScroll.current = true;
|
|
385
563
|
|
|
386
564
|
const conversationId = createId();
|
|
@@ -424,12 +602,12 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
424
602
|
timestamp: Date.now()
|
|
425
603
|
});
|
|
426
604
|
|
|
427
|
-
let responseDuration: number | null = null;
|
|
428
|
-
let responseBlendWord: string |
|
|
429
|
-
|
|
430
|
-
try {
|
|
431
|
-
const providerStatus = await Agent.ensureProviderReady();
|
|
432
|
-
if (!providerStatus.ready) {
|
|
605
|
+
let responseDuration: number | null = null;
|
|
606
|
+
let responseBlendWord: string | undefined = undefined;
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
const providerStatus = await Agent.ensureProviderReady();
|
|
610
|
+
if (!providerStatus.ready) {
|
|
433
611
|
setMessages((prev: Message[]) => {
|
|
434
612
|
const newMessages = [...prev];
|
|
435
613
|
newMessages.push({
|
|
@@ -445,7 +623,7 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
445
623
|
}
|
|
446
624
|
|
|
447
625
|
const agent = new Agent();
|
|
448
|
-
const conversationHistory = buildConversationHistory([...messages, userMessage], imagesSupported);
|
|
626
|
+
const conversationHistory = buildConversationHistory([...messages, userMessage], imagesSupported);
|
|
449
627
|
let assistantChunk = '';
|
|
450
628
|
let thinkingChunk = '';
|
|
451
629
|
const pendingToolCalls = new Map<string, { toolName: string; args: Record<string, unknown>; messageId?: string }>();
|
|
@@ -524,7 +702,8 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
524
702
|
|
|
525
703
|
const needsApproval = event.toolName === 'write' || event.toolName === 'edit' || event.toolName === 'bash';
|
|
526
704
|
const isExploreTool = event.toolName === 'explore';
|
|
527
|
-
const
|
|
705
|
+
const isMcpTool = event.toolName.startsWith('mcp__');
|
|
706
|
+
const showRunning = event.toolName === 'bash' || isMcpTool;
|
|
528
707
|
let runningMessageId: string | undefined;
|
|
529
708
|
|
|
530
709
|
if (isExploreTool) {
|
|
@@ -611,8 +790,8 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
611
790
|
let runningIndex = -1;
|
|
612
791
|
if (runningMessageId) {
|
|
613
792
|
runningIndex = newMessages.findIndex(m => m.id === runningMessageId);
|
|
614
|
-
} else if (toolName === 'bash' || toolName === 'explore') {
|
|
615
|
-
runningIndex = newMessages.findIndex(m => m.
|
|
793
|
+
} else if (toolName === 'bash' || toolName === 'explore' || toolName.startsWith('mcp__')) {
|
|
794
|
+
runningIndex = newMessages.findIndex(m => m.isRunning && m.toolName === toolName);
|
|
616
795
|
}
|
|
617
796
|
|
|
618
797
|
if (runningIndex !== -1) {
|
|
@@ -687,6 +866,7 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
687
866
|
completion: event.usage.completionTokens,
|
|
688
867
|
total: event.usage.totalTokens
|
|
689
868
|
};
|
|
869
|
+
lastPromptTokensRef.current = event.usage.promptTokens;
|
|
690
870
|
setCurrentTokens(event.usage.totalTokens);
|
|
691
871
|
}
|
|
692
872
|
}
|
|
@@ -697,44 +877,44 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
697
877
|
return;
|
|
698
878
|
}
|
|
699
879
|
|
|
700
|
-
if (!streamHadError && assistantChunk.trim()) {
|
|
701
|
-
conversationSteps.push({
|
|
702
|
-
type: 'assistant',
|
|
703
|
-
content: assistantChunk,
|
|
704
|
-
timestamp: Date.now()
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
responseDuration = Date.now() - localStartTime;
|
|
709
|
-
if (responseDuration >= 60000) {
|
|
710
|
-
responseBlendWord = BLEND_WORDS[Math.floor(Math.random() * BLEND_WORDS.length)];
|
|
711
|
-
for (let i = conversationSteps.length - 1; i >= 0; i--) {
|
|
712
|
-
if (conversationSteps[i]?.type === 'assistant') {
|
|
713
|
-
conversationSteps[i] = {
|
|
714
|
-
...conversationSteps[i]!,
|
|
715
|
-
responseDuration,
|
|
716
|
-
blendWord: responseBlendWord
|
|
717
|
-
};
|
|
718
|
-
break;
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
const conversationData: ConversationHistory = {
|
|
724
|
-
id: conversationId,
|
|
725
|
-
timestamp: Date.now(),
|
|
726
|
-
steps: conversationSteps,
|
|
727
|
-
totalSteps: stepCount,
|
|
728
|
-
title: currentTitleRef.current ?? currentTitle ?? null,
|
|
729
|
-
workspace: process.cwd(),
|
|
730
|
-
totalTokens: totalTokens.total > 0 ? totalTokens : undefined,
|
|
731
|
-
model: config.model,
|
|
732
|
-
provider: config.provider
|
|
733
|
-
};
|
|
734
|
-
|
|
735
|
-
saveConversation(conversationData);
|
|
736
|
-
|
|
737
|
-
} catch (error) {
|
|
880
|
+
if (!streamHadError && assistantChunk.trim()) {
|
|
881
|
+
conversationSteps.push({
|
|
882
|
+
type: 'assistant',
|
|
883
|
+
content: assistantChunk,
|
|
884
|
+
timestamp: Date.now()
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
responseDuration = Date.now() - localStartTime;
|
|
889
|
+
if (responseDuration >= 60000) {
|
|
890
|
+
responseBlendWord = BLEND_WORDS[Math.floor(Math.random() * BLEND_WORDS.length)];
|
|
891
|
+
for (let i = conversationSteps.length - 1; i >= 0; i--) {
|
|
892
|
+
if (conversationSteps[i]?.type === 'assistant') {
|
|
893
|
+
conversationSteps[i] = {
|
|
894
|
+
...conversationSteps[i]!,
|
|
895
|
+
responseDuration,
|
|
896
|
+
blendWord: responseBlendWord
|
|
897
|
+
};
|
|
898
|
+
break;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const conversationData: ConversationHistory = {
|
|
904
|
+
id: conversationId,
|
|
905
|
+
timestamp: Date.now(),
|
|
906
|
+
steps: conversationSteps,
|
|
907
|
+
totalSteps: stepCount,
|
|
908
|
+
title: currentTitleRef.current ?? currentTitle ?? null,
|
|
909
|
+
workspace: process.cwd(),
|
|
910
|
+
totalTokens: totalTokens.total > 0 ? totalTokens : undefined,
|
|
911
|
+
model: config.model,
|
|
912
|
+
provider: config.provider
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
saveConversation(conversationData);
|
|
916
|
+
|
|
917
|
+
} catch (error) {
|
|
738
918
|
if (abortController.signal.aborted) {
|
|
739
919
|
notifyAbort();
|
|
740
920
|
return;
|
|
@@ -764,14 +944,14 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
764
944
|
if (abortControllerRef.current === abortController) {
|
|
765
945
|
abortControllerRef.current = null;
|
|
766
946
|
}
|
|
767
|
-
const duration = responseDuration ?? (Date.now() - localStartTime);
|
|
768
|
-
if (duration >= 60000) {
|
|
769
|
-
const blendWord = responseBlendWord ?? BLEND_WORDS[Math.floor(Math.random() * BLEND_WORDS.length)];
|
|
770
|
-
setMessages((prev: Message[]) => {
|
|
771
|
-
const newMessages = [...prev];
|
|
772
|
-
for (let i = newMessages.length - 1; i >= 0; i--) {
|
|
773
|
-
if (newMessages[i]?.role === 'assistant') {
|
|
774
|
-
newMessages[i] = { ...newMessages[i]!, responseDuration: duration, blendWord };
|
|
947
|
+
const duration = responseDuration ?? (Date.now() - localStartTime);
|
|
948
|
+
if (duration >= 60000) {
|
|
949
|
+
const blendWord = responseBlendWord ?? BLEND_WORDS[Math.floor(Math.random() * BLEND_WORDS.length)];
|
|
950
|
+
setMessages((prev: Message[]) => {
|
|
951
|
+
const newMessages = [...prev];
|
|
952
|
+
for (let i = newMessages.length - 1; i >= 0; i--) {
|
|
953
|
+
if (newMessages[i]?.role === 'assistant') {
|
|
954
|
+
newMessages[i] = { ...newMessages[i]!, responseDuration: duration, blendWord };
|
|
775
955
|
break;
|
|
776
956
|
}
|
|
777
957
|
}
|
|
@@ -806,29 +986,28 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
806
986
|
? `${meta!.pastedContent!}${value.trim() ? `\n\n${value}` : ''}`
|
|
807
987
|
: value;
|
|
808
988
|
|
|
809
|
-
addInputToHistory(value.trim() || (hasPastedContent ? '[Pasted text]' : (hasImages ? '[Image]' : value)));
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
}
|
|
826
|
-
|
|
989
|
+
addInputToHistory(value.trim() || (hasPastedContent ? '[Pasted text]' : (hasImages ? '[Image]' : value)));
|
|
990
|
+
|
|
991
|
+
const imagesForMessage = imagesSupported ? pendingImages : [];
|
|
992
|
+
|
|
993
|
+
const userMessage: Message = {
|
|
994
|
+
id: createId(),
|
|
995
|
+
role: "user",
|
|
996
|
+
content: composedContent,
|
|
997
|
+
displayContent: meta?.isPaste ? '[Pasted text]' : undefined,
|
|
998
|
+
images: imagesForMessage.length > 0 ? imagesForMessage : undefined,
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
if (imagesForMessage.length > 0) {
|
|
1002
|
+
setPendingImages([]);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
827
1005
|
setMessages((prev: Message[]) => [...prev, userMessage]);
|
|
828
1006
|
setIsProcessing(true);
|
|
829
1007
|
const localStartTime = Date.now();
|
|
830
1008
|
setProcessingStartTime(localStartTime);
|
|
831
1009
|
setCurrentTokens(0);
|
|
1010
|
+
lastPromptTokensRef.current = 0;
|
|
832
1011
|
shouldAutoScroll.current = true;
|
|
833
1012
|
|
|
834
1013
|
const conversationId = createId();
|
|
@@ -848,6 +1027,18 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
848
1027
|
const estimateTokens = () => Math.ceil(totalChars / 4);
|
|
849
1028
|
setCurrentTokens(estimateTokens());
|
|
850
1029
|
const config = readConfig();
|
|
1030
|
+
const resolveMaxContextTokens = async () => {
|
|
1031
|
+
if (config.maxContextTokens) return config.maxContextTokens;
|
|
1032
|
+
if (config.provider && config.model) {
|
|
1033
|
+
const resolved = await getModelsDevContextLimit(config.provider, config.model);
|
|
1034
|
+
if (typeof resolved === "number") return resolved;
|
|
1035
|
+
}
|
|
1036
|
+
return undefined;
|
|
1037
|
+
};
|
|
1038
|
+
const buildSystemPrompt = () => {
|
|
1039
|
+
const rawSystemPrompt = config.systemPrompt || DEFAULT_SYSTEM_PROMPT;
|
|
1040
|
+
return processSystemPrompt(rawSystemPrompt, true);
|
|
1041
|
+
};
|
|
851
1042
|
const abortController = new AbortController();
|
|
852
1043
|
abortControllerRef.current = abortController;
|
|
853
1044
|
let abortNotified = false;
|
|
@@ -866,19 +1057,19 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
866
1057
|
});
|
|
867
1058
|
};
|
|
868
1059
|
|
|
869
|
-
conversationSteps.push({
|
|
870
|
-
type: 'user',
|
|
871
|
-
content: composedContent,
|
|
872
|
-
timestamp: Date.now(),
|
|
873
|
-
images: imagesForMessage.length > 0 ? imagesForMessage : undefined
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
let responseDuration: number | null = null;
|
|
877
|
-
let responseBlendWord: string |
|
|
878
|
-
|
|
879
|
-
try {
|
|
880
|
-
const providerStatus = await Agent.ensureProviderReady();
|
|
881
|
-
if (!providerStatus.ready) {
|
|
1060
|
+
conversationSteps.push({
|
|
1061
|
+
type: 'user',
|
|
1062
|
+
content: composedContent,
|
|
1063
|
+
timestamp: Date.now(),
|
|
1064
|
+
images: imagesForMessage.length > 0 ? imagesForMessage : undefined
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
let responseDuration: number | null = null;
|
|
1068
|
+
let responseBlendWord: string | undefined = undefined;
|
|
1069
|
+
|
|
1070
|
+
try {
|
|
1071
|
+
const providerStatus = await Agent.ensureProviderReady();
|
|
1072
|
+
if (!providerStatus.ready) {
|
|
882
1073
|
setMessages((prev: Message[]) => {
|
|
883
1074
|
const newMessages = [...prev];
|
|
884
1075
|
newMessages.push({
|
|
@@ -894,43 +1085,43 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
894
1085
|
}
|
|
895
1086
|
|
|
896
1087
|
const agent = new Agent();
|
|
897
|
-
const conversationHistory = buildConversationHistory([...messages, userMessage], imagesSupported);
|
|
898
|
-
let assistantChunk = '';
|
|
899
|
-
let thinkingChunk = '';
|
|
900
|
-
const pendingToolCalls = new Map<string, { toolName: string; args: Record<string, unknown>; messageId?: string }>();
|
|
901
|
-
let assistantMessageId: string | null = null;
|
|
902
|
-
let streamHadError = false;
|
|
903
|
-
titleExtractedRef.current = false;
|
|
904
|
-
|
|
905
|
-
for await (const event of agent.streamMessages(conversationHistory, { abortSignal: abortController.signal })) {
|
|
906
|
-
if (event.type === 'reasoning-delta') {
|
|
907
|
-
thinkingChunk += event.content;
|
|
908
|
-
totalChars += event.content.length;
|
|
909
|
-
setCurrentTokens(estimateTokens());
|
|
910
|
-
|
|
911
|
-
if (assistantMessageId === null) {
|
|
912
|
-
assistantMessageId = createId();
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
const currentMessageId = assistantMessageId;
|
|
916
|
-
setMessages((prev: Message[]) => {
|
|
917
|
-
const newMessages = [...prev];
|
|
918
|
-
const messageIndex = newMessages.findIndex(m => m.id === currentMessageId);
|
|
919
|
-
|
|
920
|
-
if (messageIndex === -1) {
|
|
921
|
-
newMessages.push({ id: currentMessageId, role: "assistant", content: '', thinkingContent: thinkingChunk });
|
|
922
|
-
} else {
|
|
923
|
-
newMessages[messageIndex] = {
|
|
924
|
-
...newMessages[messageIndex]!,
|
|
925
|
-
thinkingContent: thinkingChunk
|
|
926
|
-
};
|
|
927
|
-
}
|
|
928
|
-
return newMessages;
|
|
929
|
-
});
|
|
930
|
-
} else if (event.type === 'text-delta') {
|
|
931
|
-
assistantChunk += event.content;
|
|
932
|
-
totalChars += event.content.length;
|
|
933
|
-
setCurrentTokens(estimateTokens());
|
|
1088
|
+
const conversationHistory = buildConversationHistory([...messages, userMessage], imagesSupported);
|
|
1089
|
+
let assistantChunk = '';
|
|
1090
|
+
let thinkingChunk = '';
|
|
1091
|
+
const pendingToolCalls = new Map<string, { toolName: string; args: Record<string, unknown>; messageId?: string }>();
|
|
1092
|
+
let assistantMessageId: string | null = null;
|
|
1093
|
+
let streamHadError = false;
|
|
1094
|
+
titleExtractedRef.current = false;
|
|
1095
|
+
|
|
1096
|
+
for await (const event of agent.streamMessages(conversationHistory, { abortSignal: abortController.signal })) {
|
|
1097
|
+
if (event.type === 'reasoning-delta') {
|
|
1098
|
+
thinkingChunk += event.content;
|
|
1099
|
+
totalChars += event.content.length;
|
|
1100
|
+
setCurrentTokens(estimateTokens());
|
|
1101
|
+
|
|
1102
|
+
if (assistantMessageId === null) {
|
|
1103
|
+
assistantMessageId = createId();
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const currentMessageId = assistantMessageId;
|
|
1107
|
+
setMessages((prev: Message[]) => {
|
|
1108
|
+
const newMessages = [...prev];
|
|
1109
|
+
const messageIndex = newMessages.findIndex(m => m.id === currentMessageId);
|
|
1110
|
+
|
|
1111
|
+
if (messageIndex === -1) {
|
|
1112
|
+
newMessages.push({ id: currentMessageId, role: "assistant", content: '', thinkingContent: thinkingChunk });
|
|
1113
|
+
} else {
|
|
1114
|
+
newMessages[messageIndex] = {
|
|
1115
|
+
...newMessages[messageIndex]!,
|
|
1116
|
+
thinkingContent: thinkingChunk
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
return newMessages;
|
|
1120
|
+
});
|
|
1121
|
+
} else if (event.type === 'text-delta') {
|
|
1122
|
+
assistantChunk += event.content;
|
|
1123
|
+
totalChars += event.content.length;
|
|
1124
|
+
setCurrentTokens(estimateTokens());
|
|
934
1125
|
|
|
935
1126
|
const { title, cleanContent, isPending, noTitle } = extractTitle(assistantChunk, titleExtractedRef.current);
|
|
936
1127
|
|
|
@@ -950,22 +1141,22 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
950
1141
|
}
|
|
951
1142
|
|
|
952
1143
|
const displayContent = cleanContent;
|
|
953
|
-
const currentMessageId = assistantMessageId;
|
|
954
|
-
setMessages((prev: Message[]) => {
|
|
955
|
-
const newMessages = [...prev];
|
|
956
|
-
const messageIndex = newMessages.findIndex(m => m.id === currentMessageId);
|
|
957
|
-
|
|
958
|
-
if (messageIndex === -1) {
|
|
959
|
-
newMessages.push({ id: currentMessageId, role: "assistant", content: displayContent, thinkingContent: thinkingChunk });
|
|
960
|
-
} else {
|
|
961
|
-
newMessages[messageIndex] = {
|
|
962
|
-
...newMessages[messageIndex]!,
|
|
963
|
-
content: displayContent,
|
|
964
|
-
thinkingContent: thinkingChunk
|
|
965
|
-
};
|
|
966
|
-
}
|
|
967
|
-
return newMessages;
|
|
968
|
-
});
|
|
1144
|
+
const currentMessageId = assistantMessageId;
|
|
1145
|
+
setMessages((prev: Message[]) => {
|
|
1146
|
+
const newMessages = [...prev];
|
|
1147
|
+
const messageIndex = newMessages.findIndex(m => m.id === currentMessageId);
|
|
1148
|
+
|
|
1149
|
+
if (messageIndex === -1) {
|
|
1150
|
+
newMessages.push({ id: currentMessageId, role: "assistant", content: displayContent, thinkingContent: thinkingChunk });
|
|
1151
|
+
} else {
|
|
1152
|
+
newMessages[messageIndex] = {
|
|
1153
|
+
...newMessages[messageIndex]!,
|
|
1154
|
+
content: displayContent,
|
|
1155
|
+
thinkingContent: thinkingChunk
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
return newMessages;
|
|
1159
|
+
});
|
|
969
1160
|
} else if (event.type === 'step-start') {
|
|
970
1161
|
stepCount++;
|
|
971
1162
|
} else if (event.type === 'tool-call-end') {
|
|
@@ -973,6 +1164,7 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
973
1164
|
setCurrentTokens(estimateTokens());
|
|
974
1165
|
|
|
975
1166
|
const isExploreTool = event.toolName === 'explore';
|
|
1167
|
+
const isMcpTool = event.toolName.startsWith('mcp__');
|
|
976
1168
|
let runningMessageId: string | undefined;
|
|
977
1169
|
|
|
978
1170
|
if (isExploreTool) {
|
|
@@ -980,8 +1172,13 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
980
1172
|
exploreToolsRef.current = [];
|
|
981
1173
|
const purpose = (event.args.purpose as string) || 'exploring...';
|
|
982
1174
|
explorePurposeRef.current = purpose;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (isExploreTool || isMcpTool) {
|
|
983
1178
|
runningMessageId = createId();
|
|
984
|
-
|
|
1179
|
+
if (isExploreTool) {
|
|
1180
|
+
exploreMessageIdRef.current = runningMessageId;
|
|
1181
|
+
}
|
|
985
1182
|
const { name: toolDisplayName, info: toolInfo } = parseToolHeader(event.toolName, event.args);
|
|
986
1183
|
const runningContent = toolInfo ? `${toolDisplayName} (${toolInfo})` : toolDisplayName;
|
|
987
1184
|
|
|
@@ -1053,7 +1250,7 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
1053
1250
|
let runningIndex = -1;
|
|
1054
1251
|
if (runningMessageId) {
|
|
1055
1252
|
runningIndex = newMessages.findIndex(m => m.id === runningMessageId);
|
|
1056
|
-
} else if (toolName === 'bash' || toolName === 'explore') {
|
|
1253
|
+
} else if (toolName === 'bash' || toolName === 'explore' || toolName.startsWith('mcp__')) {
|
|
1057
1254
|
runningIndex = newMessages.findIndex(m => m.isRunning && m.toolName === toolName);
|
|
1058
1255
|
}
|
|
1059
1256
|
|
|
@@ -1082,15 +1279,15 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
1082
1279
|
return newMessages;
|
|
1083
1280
|
});
|
|
1084
1281
|
|
|
1085
|
-
assistantChunk = '';
|
|
1086
|
-
thinkingChunk = '';
|
|
1087
|
-
assistantMessageId = null;
|
|
1088
|
-
} else if (event.type === 'error') {
|
|
1089
|
-
if (abortController.signal.aborted) {
|
|
1090
|
-
notifyAbort();
|
|
1091
|
-
streamHadError = true;
|
|
1092
|
-
break;
|
|
1093
|
-
}
|
|
1282
|
+
assistantChunk = '';
|
|
1283
|
+
thinkingChunk = '';
|
|
1284
|
+
assistantMessageId = null;
|
|
1285
|
+
} else if (event.type === 'error') {
|
|
1286
|
+
if (abortController.signal.aborted) {
|
|
1287
|
+
notifyAbort();
|
|
1288
|
+
streamHadError = true;
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1094
1291
|
if (assistantChunk.trim()) {
|
|
1095
1292
|
conversationSteps.push({
|
|
1096
1293
|
type: 'assistant',
|
|
@@ -1117,11 +1314,11 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
1117
1314
|
return newMessages;
|
|
1118
1315
|
});
|
|
1119
1316
|
|
|
1120
|
-
assistantChunk = '';
|
|
1121
|
-
thinkingChunk = '';
|
|
1122
|
-
assistantMessageId = null;
|
|
1123
|
-
streamHadError = true;
|
|
1124
|
-
break;
|
|
1317
|
+
assistantChunk = '';
|
|
1318
|
+
thinkingChunk = '';
|
|
1319
|
+
assistantMessageId = null;
|
|
1320
|
+
streamHadError = true;
|
|
1321
|
+
break;
|
|
1125
1322
|
} else if (event.type === 'finish') {
|
|
1126
1323
|
if (event.usage && event.usage.totalTokens > 0) {
|
|
1127
1324
|
totalTokens = {
|
|
@@ -1129,6 +1326,7 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
1129
1326
|
completion: event.usage.completionTokens,
|
|
1130
1327
|
total: event.usage.totalTokens
|
|
1131
1328
|
};
|
|
1329
|
+
lastPromptTokensRef.current = event.usage.promptTokens;
|
|
1132
1330
|
setCurrentTokens(event.usage.totalTokens);
|
|
1133
1331
|
}
|
|
1134
1332
|
}
|
|
@@ -1139,42 +1337,57 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
1139
1337
|
return;
|
|
1140
1338
|
}
|
|
1141
1339
|
|
|
1142
|
-
if (!streamHadError && assistantChunk.trim()) {
|
|
1143
|
-
conversationSteps.push({
|
|
1144
|
-
type: 'assistant',
|
|
1145
|
-
content: assistantChunk,
|
|
1146
|
-
timestamp: Date.now()
|
|
1147
|
-
});
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
responseDuration = Date.now() - localStartTime;
|
|
1151
|
-
if (responseDuration >= 60000) {
|
|
1152
|
-
responseBlendWord = BLEND_WORDS[Math.floor(Math.random() * BLEND_WORDS.length)];
|
|
1153
|
-
for (let i = conversationSteps.length - 1; i >= 0; i--) {
|
|
1154
|
-
if (conversationSteps[i]?.type === 'assistant') {
|
|
1155
|
-
conversationSteps[i] = {
|
|
1156
|
-
...conversationSteps[i]!,
|
|
1157
|
-
responseDuration,
|
|
1158
|
-
blendWord: responseBlendWord
|
|
1159
|
-
};
|
|
1160
|
-
break;
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
const conversationData: ConversationHistory = {
|
|
1166
|
-
id: conversationId,
|
|
1167
|
-
timestamp: Date.now(),
|
|
1168
|
-
steps: conversationSteps,
|
|
1169
|
-
totalSteps: stepCount,
|
|
1170
|
-
title: currentTitleRef.current ?? currentTitle ?? null,
|
|
1171
|
-
workspace: process.cwd(),
|
|
1172
|
-
totalTokens: totalTokens.total > 0 ? totalTokens : undefined,
|
|
1173
|
-
model: config.model,
|
|
1174
|
-
provider: config.provider
|
|
1175
|
-
};
|
|
1176
|
-
|
|
1177
|
-
saveConversation(conversationData);
|
|
1340
|
+
if (!streamHadError && assistantChunk.trim()) {
|
|
1341
|
+
conversationSteps.push({
|
|
1342
|
+
type: 'assistant',
|
|
1343
|
+
content: assistantChunk,
|
|
1344
|
+
timestamp: Date.now()
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
responseDuration = Date.now() - localStartTime;
|
|
1349
|
+
if (responseDuration >= 60000) {
|
|
1350
|
+
responseBlendWord = BLEND_WORDS[Math.floor(Math.random() * BLEND_WORDS.length)];
|
|
1351
|
+
for (let i = conversationSteps.length - 1; i >= 0; i--) {
|
|
1352
|
+
if (conversationSteps[i]?.type === 'assistant') {
|
|
1353
|
+
conversationSteps[i] = {
|
|
1354
|
+
...conversationSteps[i]!,
|
|
1355
|
+
responseDuration,
|
|
1356
|
+
blendWord: responseBlendWord
|
|
1357
|
+
};
|
|
1358
|
+
break;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const conversationData: ConversationHistory = {
|
|
1364
|
+
id: conversationId,
|
|
1365
|
+
timestamp: Date.now(),
|
|
1366
|
+
steps: conversationSteps,
|
|
1367
|
+
totalSteps: stepCount,
|
|
1368
|
+
title: currentTitleRef.current ?? currentTitle ?? null,
|
|
1369
|
+
workspace: process.cwd(),
|
|
1370
|
+
totalTokens: totalTokens.total > 0 ? totalTokens : undefined,
|
|
1371
|
+
model: config.model,
|
|
1372
|
+
provider: config.provider
|
|
1373
|
+
};
|
|
1374
|
+
|
|
1375
|
+
saveConversation(conversationData);
|
|
1376
|
+
const resolvedMax = await resolveMaxContextTokens();
|
|
1377
|
+
const maxContextTokens = resolvedMax ?? getDefaultContextBudget(config.provider);
|
|
1378
|
+
if (!abortController.signal.aborted) {
|
|
1379
|
+
const realPromptTokens = lastPromptTokensRef.current;
|
|
1380
|
+
const systemPrompt = buildSystemPrompt();
|
|
1381
|
+
setMessages(prev => {
|
|
1382
|
+
const usedTokens = realPromptTokens > 0
|
|
1383
|
+
? realPromptTokens
|
|
1384
|
+
: estimateTotalTokens(prev, systemPrompt);
|
|
1385
|
+
if (!shouldAutoCompact(usedTokens, maxContextTokens)) return prev;
|
|
1386
|
+
const compacted = compactMessagesForUi(prev, systemPrompt, maxContextTokens, createId, true);
|
|
1387
|
+
setCurrentTokens(compacted.estimatedTokens);
|
|
1388
|
+
return compacted.messages;
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1178
1391
|
|
|
1179
1392
|
} catch (error) {
|
|
1180
1393
|
if (abortController.signal.aborted) {
|
|
@@ -1206,14 +1419,14 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
1206
1419
|
if (abortControllerRef.current === abortController) {
|
|
1207
1420
|
abortControllerRef.current = null;
|
|
1208
1421
|
}
|
|
1209
|
-
const duration = responseDuration ?? (Date.now() - localStartTime);
|
|
1210
|
-
if (duration >= 60000) {
|
|
1211
|
-
const blendWord = responseBlendWord ?? BLEND_WORDS[Math.floor(Math.random() * BLEND_WORDS.length)];
|
|
1212
|
-
setMessages((prev: Message[]) => {
|
|
1213
|
-
const newMessages = [...prev];
|
|
1214
|
-
for (let i = newMessages.length - 1; i >= 0; i--) {
|
|
1215
|
-
if (newMessages[i]?.role === 'assistant') {
|
|
1216
|
-
newMessages[i] = { ...newMessages[i]!, responseDuration: duration, blendWord };
|
|
1422
|
+
const duration = responseDuration ?? (Date.now() - localStartTime);
|
|
1423
|
+
if (duration >= 60000) {
|
|
1424
|
+
const blendWord = responseBlendWord ?? BLEND_WORDS[Math.floor(Math.random() * BLEND_WORDS.length)];
|
|
1425
|
+
setMessages((prev: Message[]) => {
|
|
1426
|
+
const newMessages = [...prev];
|
|
1427
|
+
for (let i = newMessages.length - 1; i >= 0; i--) {
|
|
1428
|
+
if (newMessages[i]?.role === 'assistant') {
|
|
1429
|
+
newMessages[i] = { ...newMessages[i]!, responseDuration: duration, blendWord };
|
|
1217
1430
|
break;
|
|
1218
1431
|
}
|
|
1219
1432
|
}
|
|
@@ -1250,18 +1463,18 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
1250
1463
|
}
|
|
1251
1464
|
|
|
1252
1465
|
return (
|
|
1253
|
-
<ChatPage
|
|
1254
|
-
messages={messages}
|
|
1255
|
-
isProcessing={isProcessing}
|
|
1256
|
-
processingStartTime={processingStartTime}
|
|
1257
|
-
currentTokens={currentTokens}
|
|
1258
|
-
scrollOffset={scrollOffset}
|
|
1259
|
-
terminalHeight={terminalHeight}
|
|
1260
|
-
terminalWidth={terminalWidth}
|
|
1261
|
-
pasteRequestId={pasteRequestId}
|
|
1262
|
-
shortcutsOpen={shortcutsOpen}
|
|
1263
|
-
onSubmit={handleSubmit}
|
|
1264
|
-
pendingImages={pendingImages}
|
|
1265
|
-
/>
|
|
1266
|
-
);
|
|
1267
|
-
}
|
|
1466
|
+
<ChatPage
|
|
1467
|
+
messages={messages}
|
|
1468
|
+
isProcessing={isProcessing}
|
|
1469
|
+
processingStartTime={processingStartTime}
|
|
1470
|
+
currentTokens={currentTokens}
|
|
1471
|
+
scrollOffset={scrollOffset}
|
|
1472
|
+
terminalHeight={terminalHeight}
|
|
1473
|
+
terminalWidth={terminalWidth}
|
|
1474
|
+
pasteRequestId={pasteRequestId}
|
|
1475
|
+
shortcutsOpen={shortcutsOpen}
|
|
1476
|
+
onSubmit={handleSubmit}
|
|
1477
|
+
pendingImages={pendingImages}
|
|
1478
|
+
/>
|
|
1479
|
+
);
|
|
1480
|
+
}
|