@kirosnn/mosaic 0.73.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 -1
- package/package.json +1 -1
- package/src/agent/prompts/systemPrompt.ts +1 -1
- package/src/agent/prompts/toolsPrompt.ts +64 -3
- package/src/components/Main.tsx +1480 -1459
- package/src/mcp/approvalPolicy.ts +155 -147
- package/src/mcp/cli/doctor.ts +0 -3
- package/src/mcp/config.ts +234 -223
- package/src/mcp/processManager.ts +303 -298
- 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 +181 -168
- package/src/mcp/types.ts +115 -94
- package/src/utils/markdown.tsx +60 -26
- package/src/utils/toolFormatting.ts +55 -6
- package/src/web/components/MessageItem.tsx +44 -13
- package/src/mcp/servers/navigation.ts +0 -854
package/src/components/Main.tsx
CHANGED
|
@@ -1,1459 +1,1480 @@
|
|
|
1
|
-
import { useState, useEffect, useRef } from "react";
|
|
2
|
-
import type { ImagePart, TextPart, UserContent } from "ai";
|
|
3
|
-
import { useKeyboard } from "@opentui/react";
|
|
4
|
-
import { Agent } from "../agent";
|
|
5
|
-
import { saveConversation, addInputToHistory, type ConversationHistory, type ConversationStep } from "../utils/history";
|
|
6
|
-
import { readConfig } from "../utils/config";
|
|
7
|
-
import { DEFAULT_MAX_TOOL_LINES, formatToolMessage, formatErrorMessage, parseToolHeader } from '../utils/toolFormatting';
|
|
8
|
-
import { initializeCommands, isCommand, executeCommand } from '../utils/commands';
|
|
9
|
-
import type { InputSubmitMeta } from './CustomInput';
|
|
10
|
-
|
|
11
|
-
import { subscribeQuestion, type QuestionRequest } from "../utils/questionBridge";
|
|
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">;
|
|
26
|
-
|
|
27
|
-
function extractTitle(content: string, alreadyResolved: boolean): { title: string | null; cleanContent: string; isPending: boolean; noTitle: boolean } {
|
|
28
|
-
const trimmed = content.trimStart();
|
|
29
|
-
|
|
30
|
-
const titleMatch = trimmed.match(/^<title>(.*?)<\/title>\s*/s);
|
|
31
|
-
if (titleMatch) {
|
|
32
|
-
const title = alreadyResolved ? null : (titleMatch[1]?.trim() || null);
|
|
33
|
-
const cleanContent = trimmed.replace(/^<title>.*?<\/title>\s*/s, '');
|
|
34
|
-
return { title, cleanContent, isPending: false, noTitle: false };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (alreadyResolved) {
|
|
38
|
-
return { title: null, cleanContent: content, isPending: false, noTitle: false };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const partialTitlePattern = /^<(t(i(t(l(e(>.*)?)?)?)?)?)?$/i;
|
|
42
|
-
if (partialTitlePattern.test(trimmed) || (trimmed.startsWith('<title>') && !trimmed.includes('</title>'))) {
|
|
43
|
-
return { title: null, cleanContent: '', isPending: true, noTitle: false };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return { title: null, cleanContent: content, isPending: false, noTitle: true };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function setTerminalTitle(title: string) {
|
|
50
|
-
process.title = `⁘ ${title}`;
|
|
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
|
-
|
|
194
|
-
export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsOpen = false, commandsOpen = false, initialMessage }: MainProps) {
|
|
195
|
-
const [currentPage, setCurrentPage] = useState<"home" | "chat">(initialMessage ? "chat" : "home");
|
|
196
|
-
const [messages, setMessages] = useState<Message[]>([]);
|
|
197
|
-
const [isProcessing, setIsProcessing] = useState(false);
|
|
198
|
-
const [processingStartTime, setProcessingStartTime] = useState<number | null>(null);
|
|
199
|
-
const [currentTokens, setCurrentTokens] = useState(0);
|
|
200
|
-
const [scrollOffset, setScrollOffset] = useState(0);
|
|
201
|
-
const [terminalHeight, setTerminalHeight] = useState(process.stdout.rows || 24);
|
|
202
|
-
const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80);
|
|
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);
|
|
207
|
-
const currentTitleRef = useRef<string | null>(null);
|
|
208
|
-
const titleExtractedRef = useRef(false);
|
|
209
|
-
const shouldAutoScroll = useRef(true);
|
|
210
|
-
const abortControllerRef = useRef<AbortController | null>(null);
|
|
211
|
-
const currentPageRef = useRef(currentPage);
|
|
212
|
-
const shortcutsOpenRef = useRef(shortcutsOpen);
|
|
213
|
-
const commandsOpenRef = useRef(commandsOpen);
|
|
214
|
-
const questionRequestRef = useRef<QuestionRequest | null>(questionRequest);
|
|
215
|
-
const initialMessageProcessed = useRef(false);
|
|
216
|
-
const lastPromptTokensRef = useRef<number>(0);
|
|
217
|
-
const exploreMessageIdRef = useRef<string | null>(null);
|
|
218
|
-
const exploreToolsRef = useRef<Array<{ tool: string; info: string; success: boolean }>>([]);
|
|
219
|
-
const explorePurposeRef = useRef<string>('');
|
|
220
|
-
|
|
221
|
-
const createId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
222
|
-
|
|
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
|
-
}, []);
|
|
247
|
-
|
|
248
|
-
useEffect(() => {
|
|
249
|
-
let lastExploreTokens = 0;
|
|
250
|
-
setExploreToolCallback((toolName, args, result, totalTokens) => {
|
|
251
|
-
const info = (args.path || args.pattern || args.query || '') as string;
|
|
252
|
-
const shortInfo = info.length > 40 ? info.substring(0, 37) + '...' : info;
|
|
253
|
-
exploreToolsRef.current.push({ tool: toolName, info: shortInfo, success: result.success });
|
|
254
|
-
|
|
255
|
-
const tokenDelta = totalTokens - lastExploreTokens;
|
|
256
|
-
lastExploreTokens = totalTokens;
|
|
257
|
-
if (tokenDelta > 0) {
|
|
258
|
-
setCurrentTokens(prev => prev + tokenDelta);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (exploreMessageIdRef.current) {
|
|
262
|
-
setMessages((prev: Message[]) => {
|
|
263
|
-
const newMessages = [...prev];
|
|
264
|
-
const idx = newMessages.findIndex(m => m.id === exploreMessageIdRef.current);
|
|
265
|
-
if (idx !== -1) {
|
|
266
|
-
const toolLines = exploreToolsRef.current.map(t => {
|
|
267
|
-
const icon = t.success ? '→' : '-';
|
|
268
|
-
return ` ${icon} ${t.tool}(${t.info})`;
|
|
269
|
-
});
|
|
270
|
-
const purpose = explorePurposeRef.current;
|
|
271
|
-
const newContent = `Explore (${purpose})\n${toolLines.join('\n')}`;
|
|
272
|
-
newMessages[idx] = { ...newMessages[idx]!, content: newContent };
|
|
273
|
-
}
|
|
274
|
-
return newMessages;
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
return () => {
|
|
280
|
-
setExploreToolCallback(null);
|
|
281
|
-
};
|
|
282
|
-
}, []);
|
|
283
|
-
|
|
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]);
|
|
304
|
-
|
|
305
|
-
useEffect(() => {
|
|
306
|
-
const handleResize = () => {
|
|
307
|
-
const newWidth = process.stdout.columns || 80;
|
|
308
|
-
const newHeight = process.stdout.rows || 24;
|
|
309
|
-
const oldHeight = terminalHeight;
|
|
310
|
-
|
|
311
|
-
setTerminalWidth(newWidth);
|
|
312
|
-
setTerminalHeight(newHeight);
|
|
313
|
-
|
|
314
|
-
if (shouldAutoScroll.current) {
|
|
315
|
-
setScrollOffset(0);
|
|
316
|
-
} else if (oldHeight !== newHeight) {
|
|
317
|
-
const heightDiff = newHeight - oldHeight;
|
|
318
|
-
setScrollOffset(prev => Math.max(0, prev - heightDiff));
|
|
319
|
-
}
|
|
320
|
-
};
|
|
321
|
-
process.stdout.on('resize', handleResize);
|
|
322
|
-
return () => {
|
|
323
|
-
process.stdout.off('resize', handleResize);
|
|
324
|
-
};
|
|
325
|
-
}, [terminalWidth, terminalHeight]);
|
|
326
|
-
|
|
327
|
-
useEffect(() => {
|
|
328
|
-
return subscribeQuestion(setQuestionRequest);
|
|
329
|
-
}, []);
|
|
330
|
-
|
|
331
|
-
useEffect(() => {
|
|
332
|
-
return subscribeApprovalAccepted((accepted) => {
|
|
333
|
-
const isBashTool = accepted.toolName === 'bash';
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
newMessages
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
const
|
|
1031
|
-
if (
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
toolName,
|
|
1221
|
-
toolArgs,
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
content:
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
return
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
}
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import type { ImagePart, TextPart, UserContent } from "ai";
|
|
3
|
+
import { useKeyboard } from "@opentui/react";
|
|
4
|
+
import { Agent } from "../agent";
|
|
5
|
+
import { saveConversation, addInputToHistory, type ConversationHistory, type ConversationStep } from "../utils/history";
|
|
6
|
+
import { readConfig } from "../utils/config";
|
|
7
|
+
import { DEFAULT_MAX_TOOL_LINES, formatToolMessage, formatErrorMessage, parseToolHeader } from '../utils/toolFormatting';
|
|
8
|
+
import { initializeCommands, isCommand, executeCommand } from '../utils/commands';
|
|
9
|
+
import type { InputSubmitMeta } from './CustomInput';
|
|
10
|
+
|
|
11
|
+
import { subscribeQuestion, type QuestionRequest } from "../utils/questionBridge";
|
|
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">;
|
|
26
|
+
|
|
27
|
+
function extractTitle(content: string, alreadyResolved: boolean): { title: string | null; cleanContent: string; isPending: boolean; noTitle: boolean } {
|
|
28
|
+
const trimmed = content.trimStart();
|
|
29
|
+
|
|
30
|
+
const titleMatch = trimmed.match(/^<title>(.*?)<\/title>\s*/s);
|
|
31
|
+
if (titleMatch) {
|
|
32
|
+
const title = alreadyResolved ? null : (titleMatch[1]?.trim() || null);
|
|
33
|
+
const cleanContent = trimmed.replace(/^<title>.*?<\/title>\s*/s, '');
|
|
34
|
+
return { title, cleanContent, isPending: false, noTitle: false };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (alreadyResolved) {
|
|
38
|
+
return { title: null, cleanContent: content, isPending: false, noTitle: false };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const partialTitlePattern = /^<(t(i(t(l(e(>.*)?)?)?)?)?)?$/i;
|
|
42
|
+
if (partialTitlePattern.test(trimmed) || (trimmed.startsWith('<title>') && !trimmed.includes('</title>'))) {
|
|
43
|
+
return { title: null, cleanContent: '', isPending: true, noTitle: false };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { title: null, cleanContent: content, isPending: false, noTitle: true };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function setTerminalTitle(title: string) {
|
|
50
|
+
process.title = `⁘ ${title}`;
|
|
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
|
+
|
|
194
|
+
export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsOpen = false, commandsOpen = false, initialMessage }: MainProps) {
|
|
195
|
+
const [currentPage, setCurrentPage] = useState<"home" | "chat">(initialMessage ? "chat" : "home");
|
|
196
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
197
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
198
|
+
const [processingStartTime, setProcessingStartTime] = useState<number | null>(null);
|
|
199
|
+
const [currentTokens, setCurrentTokens] = useState(0);
|
|
200
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
201
|
+
const [terminalHeight, setTerminalHeight] = useState(process.stdout.rows || 24);
|
|
202
|
+
const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80);
|
|
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);
|
|
207
|
+
const currentTitleRef = useRef<string | null>(null);
|
|
208
|
+
const titleExtractedRef = useRef(false);
|
|
209
|
+
const shouldAutoScroll = useRef(true);
|
|
210
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
211
|
+
const currentPageRef = useRef(currentPage);
|
|
212
|
+
const shortcutsOpenRef = useRef(shortcutsOpen);
|
|
213
|
+
const commandsOpenRef = useRef(commandsOpen);
|
|
214
|
+
const questionRequestRef = useRef<QuestionRequest | null>(questionRequest);
|
|
215
|
+
const initialMessageProcessed = useRef(false);
|
|
216
|
+
const lastPromptTokensRef = useRef<number>(0);
|
|
217
|
+
const exploreMessageIdRef = useRef<string | null>(null);
|
|
218
|
+
const exploreToolsRef = useRef<Array<{ tool: string; info: string; success: boolean }>>([]);
|
|
219
|
+
const explorePurposeRef = useRef<string>('');
|
|
220
|
+
|
|
221
|
+
const createId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
222
|
+
|
|
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
|
+
}, []);
|
|
247
|
+
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
let lastExploreTokens = 0;
|
|
250
|
+
setExploreToolCallback((toolName, args, result, totalTokens) => {
|
|
251
|
+
const info = (args.path || args.pattern || args.query || '') as string;
|
|
252
|
+
const shortInfo = info.length > 40 ? info.substring(0, 37) + '...' : info;
|
|
253
|
+
exploreToolsRef.current.push({ tool: toolName, info: shortInfo, success: result.success });
|
|
254
|
+
|
|
255
|
+
const tokenDelta = totalTokens - lastExploreTokens;
|
|
256
|
+
lastExploreTokens = totalTokens;
|
|
257
|
+
if (tokenDelta > 0) {
|
|
258
|
+
setCurrentTokens(prev => prev + tokenDelta);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (exploreMessageIdRef.current) {
|
|
262
|
+
setMessages((prev: Message[]) => {
|
|
263
|
+
const newMessages = [...prev];
|
|
264
|
+
const idx = newMessages.findIndex(m => m.id === exploreMessageIdRef.current);
|
|
265
|
+
if (idx !== -1) {
|
|
266
|
+
const toolLines = exploreToolsRef.current.map(t => {
|
|
267
|
+
const icon = t.success ? '→' : '-';
|
|
268
|
+
return ` ${icon} ${t.tool}(${t.info})`;
|
|
269
|
+
});
|
|
270
|
+
const purpose = explorePurposeRef.current;
|
|
271
|
+
const newContent = `Explore (${purpose})\n${toolLines.join('\n')}`;
|
|
272
|
+
newMessages[idx] = { ...newMessages[idx]!, content: newContent };
|
|
273
|
+
}
|
|
274
|
+
return newMessages;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
return () => {
|
|
280
|
+
setExploreToolCallback(null);
|
|
281
|
+
};
|
|
282
|
+
}, []);
|
|
283
|
+
|
|
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]);
|
|
304
|
+
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
const handleResize = () => {
|
|
307
|
+
const newWidth = process.stdout.columns || 80;
|
|
308
|
+
const newHeight = process.stdout.rows || 24;
|
|
309
|
+
const oldHeight = terminalHeight;
|
|
310
|
+
|
|
311
|
+
setTerminalWidth(newWidth);
|
|
312
|
+
setTerminalHeight(newHeight);
|
|
313
|
+
|
|
314
|
+
if (shouldAutoScroll.current) {
|
|
315
|
+
setScrollOffset(0);
|
|
316
|
+
} else if (oldHeight !== newHeight) {
|
|
317
|
+
const heightDiff = newHeight - oldHeight;
|
|
318
|
+
setScrollOffset(prev => Math.max(0, prev - heightDiff));
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
process.stdout.on('resize', handleResize);
|
|
322
|
+
return () => {
|
|
323
|
+
process.stdout.off('resize', handleResize);
|
|
324
|
+
};
|
|
325
|
+
}, [terminalWidth, terminalHeight]);
|
|
326
|
+
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
return subscribeQuestion(setQuestionRequest);
|
|
329
|
+
}, []);
|
|
330
|
+
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
return subscribeApprovalAccepted((accepted) => {
|
|
333
|
+
const isBashTool = accepted.toolName === 'bash';
|
|
334
|
+
const isMcpTool = accepted.toolName.startsWith('mcp__');
|
|
335
|
+
|
|
336
|
+
if (isBashTool || isMcpTool) {
|
|
337
|
+
const { name: toolDisplayName, info: toolInfo } = parseToolHeader(accepted.toolName, accepted.args);
|
|
338
|
+
const runningContent = toolInfo ? `${toolDisplayName} (${toolInfo})` : toolDisplayName;
|
|
339
|
+
|
|
340
|
+
setMessages((prev: Message[]) => {
|
|
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
|
+
|
|
355
|
+
newMessages.push({
|
|
356
|
+
id: createId(),
|
|
357
|
+
role: "tool",
|
|
358
|
+
content: runningContent,
|
|
359
|
+
toolName: accepted.toolName,
|
|
360
|
+
toolArgs: accepted.args,
|
|
361
|
+
success: true,
|
|
362
|
+
isRunning: true,
|
|
363
|
+
runningStartTime: Date.now()
|
|
364
|
+
});
|
|
365
|
+
return newMessages;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}, []);
|
|
370
|
+
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
currentPageRef.current = currentPage;
|
|
373
|
+
}, [currentPage]);
|
|
374
|
+
|
|
375
|
+
useEffect(() => {
|
|
376
|
+
shortcutsOpenRef.current = shortcutsOpen;
|
|
377
|
+
}, [shortcutsOpen]);
|
|
378
|
+
|
|
379
|
+
useEffect(() => {
|
|
380
|
+
commandsOpenRef.current = commandsOpen;
|
|
381
|
+
}, [commandsOpen]);
|
|
382
|
+
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
questionRequestRef.current = questionRequest;
|
|
385
|
+
}, [questionRequest]);
|
|
386
|
+
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
if (questionRequest) {
|
|
389
|
+
shouldAutoScroll.current = true;
|
|
390
|
+
setScrollOffset(0);
|
|
391
|
+
}
|
|
392
|
+
}, [questionRequest]);
|
|
393
|
+
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
if (currentPage !== "chat") return;
|
|
396
|
+
|
|
397
|
+
process.stdin.setRawMode(true);
|
|
398
|
+
process.stdout.write('\x1b[?1000h');
|
|
399
|
+
process.stdout.write('\x1b[?1003h');
|
|
400
|
+
process.stdout.write('\x1b[?1006h');
|
|
401
|
+
|
|
402
|
+
const handleData = (data: Buffer) => {
|
|
403
|
+
const str = data.toString();
|
|
404
|
+
|
|
405
|
+
if (str.match(/\x1b\[<(\d+);(\d+);(\d+)([mM])/)) {
|
|
406
|
+
const match = str.match(/\x1b\[<(\d+);(\d+);(\d+)([mM])/);
|
|
407
|
+
if (match) {
|
|
408
|
+
const button = parseInt(match[1] || '0');
|
|
409
|
+
|
|
410
|
+
if (button === 64) {
|
|
411
|
+
shouldAutoScroll.current = false;
|
|
412
|
+
setScrollOffset((prev) => prev + 1);
|
|
413
|
+
} else if (button === 65) {
|
|
414
|
+
setScrollOffset((prev) => {
|
|
415
|
+
const newOffset = Math.max(0, prev - 1);
|
|
416
|
+
if (newOffset === 0) {
|
|
417
|
+
shouldAutoScroll.current = true;
|
|
418
|
+
}
|
|
419
|
+
return newOffset;
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
process.stdin.on('data', handleData);
|
|
427
|
+
|
|
428
|
+
return () => {
|
|
429
|
+
process.stdin.off('data', handleData);
|
|
430
|
+
process.stdout.write('\x1b[?1000l');
|
|
431
|
+
process.stdout.write('\x1b[?1003l');
|
|
432
|
+
process.stdout.write('\x1b[?1006l');
|
|
433
|
+
};
|
|
434
|
+
}, [currentPage]);
|
|
435
|
+
|
|
436
|
+
useEffect(() => {
|
|
437
|
+
if (currentPage === "chat") {
|
|
438
|
+
setScrollOffset((prevOffset) => {
|
|
439
|
+
if (shouldAutoScroll.current || prevOffset < 5) {
|
|
440
|
+
shouldAutoScroll.current = true;
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
return prevOffset;
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}, [messages, currentPage]);
|
|
447
|
+
|
|
448
|
+
useEffect(() => {
|
|
449
|
+
if (copyRequestId > 0 && onCopy && messages.length > 0) {
|
|
450
|
+
const lastAssistantMessage = messages.slice().reverse().find(m => m.role === 'assistant');
|
|
451
|
+
if (lastAssistantMessage) {
|
|
452
|
+
onCopy(lastAssistantMessage.content);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}, [copyRequestId, onCopy, messages]);
|
|
456
|
+
|
|
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
|
+
}
|
|
473
|
+
if (getCurrentApproval()) {
|
|
474
|
+
cancelApproval();
|
|
475
|
+
}
|
|
476
|
+
abortControllerRef.current?.abort();
|
|
477
|
+
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;
|
|
509
|
+
|
|
510
|
+
if (isCommand(value)) {
|
|
511
|
+
const result = await executeCommand(value);
|
|
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
|
+
|
|
546
|
+
if (result.shouldAddToHistory === true) {
|
|
547
|
+
addInputToHistory(value.trim());
|
|
548
|
+
|
|
549
|
+
const userMessage: Message = {
|
|
550
|
+
id: createId(),
|
|
551
|
+
role: "user",
|
|
552
|
+
content: result.content,
|
|
553
|
+
displayContent: value,
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
setMessages((prev: Message[]) => [...prev, userMessage]);
|
|
557
|
+
setIsProcessing(true);
|
|
558
|
+
const localStartTime = Date.now();
|
|
559
|
+
setProcessingStartTime(localStartTime);
|
|
560
|
+
setCurrentTokens(0);
|
|
561
|
+
lastPromptTokensRef.current = 0;
|
|
562
|
+
shouldAutoScroll.current = true;
|
|
563
|
+
|
|
564
|
+
const conversationId = createId();
|
|
565
|
+
const conversationSteps: ConversationStep[] = [];
|
|
566
|
+
let totalTokens = { prompt: 0, completion: 0, total: 0 };
|
|
567
|
+
let stepCount = 0;
|
|
568
|
+
let totalChars = 0;
|
|
569
|
+
for (const m of messages) {
|
|
570
|
+
if (m.role === 'assistant') {
|
|
571
|
+
totalChars += m.content.length;
|
|
572
|
+
if (m.thinkingContent) totalChars += m.thinkingContent.length;
|
|
573
|
+
} else if (m.role === 'tool') {
|
|
574
|
+
totalChars += m.content.length;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const estimateTokens = () => Math.ceil(totalChars / 4);
|
|
579
|
+
setCurrentTokens(estimateTokens());
|
|
580
|
+
const config = readConfig();
|
|
581
|
+
const abortController = new AbortController();
|
|
582
|
+
abortControllerRef.current = abortController;
|
|
583
|
+
let abortNotified = false;
|
|
584
|
+
const notifyAbort = () => {
|
|
585
|
+
if (abortNotified) return;
|
|
586
|
+
abortNotified = true;
|
|
587
|
+
setMessages((prev: Message[]) => {
|
|
588
|
+
const newMessages = [...prev];
|
|
589
|
+
newMessages.push({
|
|
590
|
+
id: createId(),
|
|
591
|
+
role: "tool",
|
|
592
|
+
success: false,
|
|
593
|
+
content: "Request interrupted by user. \n↪ What should Mosaic do instead?"
|
|
594
|
+
});
|
|
595
|
+
return newMessages;
|
|
596
|
+
});
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
conversationSteps.push({
|
|
600
|
+
type: 'user',
|
|
601
|
+
content: result.content,
|
|
602
|
+
timestamp: Date.now()
|
|
603
|
+
});
|
|
604
|
+
|
|
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) {
|
|
611
|
+
setMessages((prev: Message[]) => {
|
|
612
|
+
const newMessages = [...prev];
|
|
613
|
+
newMessages.push({
|
|
614
|
+
id: createId(),
|
|
615
|
+
role: "assistant",
|
|
616
|
+
content: `Ollama error: ${providerStatus.error || 'Could not start Ollama. Make sure Ollama is installed.'}`,
|
|
617
|
+
isError: true
|
|
618
|
+
});
|
|
619
|
+
return newMessages;
|
|
620
|
+
});
|
|
621
|
+
setIsProcessing(false);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const agent = new Agent();
|
|
626
|
+
const conversationHistory = buildConversationHistory([...messages, userMessage], imagesSupported);
|
|
627
|
+
let assistantChunk = '';
|
|
628
|
+
let thinkingChunk = '';
|
|
629
|
+
const pendingToolCalls = new Map<string, { toolName: string; args: Record<string, unknown>; messageId?: string }>();
|
|
630
|
+
let assistantMessageId: string | null = null;
|
|
631
|
+
let streamHadError = false;
|
|
632
|
+
titleExtractedRef.current = false;
|
|
633
|
+
|
|
634
|
+
for await (const event of agent.streamMessages(conversationHistory, { abortSignal: abortController.signal })) {
|
|
635
|
+
if (event.type === 'reasoning-delta') {
|
|
636
|
+
thinkingChunk += event.content;
|
|
637
|
+
totalChars += event.content.length;
|
|
638
|
+
setCurrentTokens(estimateTokens());
|
|
639
|
+
|
|
640
|
+
if (assistantMessageId === null) {
|
|
641
|
+
assistantMessageId = createId();
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const currentMessageId = assistantMessageId;
|
|
645
|
+
setMessages((prev: Message[]) => {
|
|
646
|
+
const newMessages = [...prev];
|
|
647
|
+
const messageIndex = newMessages.findIndex(m => m.id === currentMessageId);
|
|
648
|
+
|
|
649
|
+
if (messageIndex === -1) {
|
|
650
|
+
newMessages.push({ id: currentMessageId, role: "assistant", content: '', thinkingContent: thinkingChunk });
|
|
651
|
+
} else {
|
|
652
|
+
newMessages[messageIndex] = {
|
|
653
|
+
...newMessages[messageIndex]!,
|
|
654
|
+
thinkingContent: thinkingChunk
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
return newMessages;
|
|
658
|
+
});
|
|
659
|
+
} else if (event.type === 'text-delta') {
|
|
660
|
+
assistantChunk += event.content;
|
|
661
|
+
totalChars += event.content.length;
|
|
662
|
+
setCurrentTokens(estimateTokens());
|
|
663
|
+
|
|
664
|
+
const { title, cleanContent, isPending, noTitle } = extractTitle(assistantChunk, titleExtractedRef.current);
|
|
665
|
+
|
|
666
|
+
if (title) {
|
|
667
|
+
titleExtractedRef.current = true;
|
|
668
|
+
currentTitleRef.current = title;
|
|
669
|
+
setCurrentTitle(title);
|
|
670
|
+
setTerminalTitle(title);
|
|
671
|
+
} else if (noTitle) {
|
|
672
|
+
titleExtractedRef.current = true;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (isPending) continue;
|
|
676
|
+
|
|
677
|
+
if (assistantMessageId === null) {
|
|
678
|
+
assistantMessageId = createId();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const displayContent = cleanContent;
|
|
682
|
+
const currentMessageId = assistantMessageId;
|
|
683
|
+
setMessages((prev: Message[]) => {
|
|
684
|
+
const newMessages = [...prev];
|
|
685
|
+
const messageIndex = newMessages.findIndex(m => m.id === currentMessageId);
|
|
686
|
+
|
|
687
|
+
if (messageIndex === -1) {
|
|
688
|
+
newMessages.push({ id: currentMessageId, role: "assistant", content: displayContent, thinkingContent: thinkingChunk });
|
|
689
|
+
} else {
|
|
690
|
+
newMessages[messageIndex] = {
|
|
691
|
+
...newMessages[messageIndex]!,
|
|
692
|
+
content: displayContent
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
return newMessages;
|
|
696
|
+
});
|
|
697
|
+
} else if (event.type === 'step-start') {
|
|
698
|
+
stepCount++;
|
|
699
|
+
} else if (event.type === 'tool-call-end') {
|
|
700
|
+
totalChars += JSON.stringify(event.args).length;
|
|
701
|
+
setCurrentTokens(estimateTokens());
|
|
702
|
+
|
|
703
|
+
const needsApproval = event.toolName === 'write' || event.toolName === 'edit' || event.toolName === 'bash';
|
|
704
|
+
const isExploreTool = event.toolName === 'explore';
|
|
705
|
+
const isMcpTool = event.toolName.startsWith('mcp__');
|
|
706
|
+
const showRunning = event.toolName === 'bash' || isMcpTool;
|
|
707
|
+
let runningMessageId: string | undefined;
|
|
708
|
+
|
|
709
|
+
if (isExploreTool) {
|
|
710
|
+
setExploreAbortController(abortController);
|
|
711
|
+
exploreToolsRef.current = [];
|
|
712
|
+
const purpose = (event.args.purpose as string) || 'exploring...';
|
|
713
|
+
explorePurposeRef.current = purpose;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (!needsApproval) {
|
|
717
|
+
runningMessageId = createId();
|
|
718
|
+
const { name: toolDisplayName, info: toolInfo } = parseToolHeader(event.toolName, event.args);
|
|
719
|
+
const runningContent = toolInfo ? `${toolDisplayName} (${toolInfo})` : toolDisplayName;
|
|
720
|
+
|
|
721
|
+
if (isExploreTool) {
|
|
722
|
+
exploreMessageIdRef.current = runningMessageId;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
setMessages((prev: Message[]) => {
|
|
726
|
+
const newMessages = [...prev];
|
|
727
|
+
newMessages.push({
|
|
728
|
+
id: runningMessageId!,
|
|
729
|
+
role: "tool",
|
|
730
|
+
content: runningContent,
|
|
731
|
+
toolName: event.toolName,
|
|
732
|
+
toolArgs: event.args,
|
|
733
|
+
success: true,
|
|
734
|
+
isRunning: showRunning || isExploreTool,
|
|
735
|
+
runningStartTime: (showRunning || isExploreTool) ? Date.now() : undefined
|
|
736
|
+
});
|
|
737
|
+
return newMessages;
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
pendingToolCalls.set(event.toolCallId, {
|
|
742
|
+
toolName: event.toolName,
|
|
743
|
+
args: event.args,
|
|
744
|
+
messageId: runningMessageId
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
} else if (event.type === 'tool-result') {
|
|
748
|
+
const pending = pendingToolCalls.get(event.toolCallId);
|
|
749
|
+
const toolName = pending?.toolName ?? event.toolName;
|
|
750
|
+
const toolArgs = pending?.args ?? {};
|
|
751
|
+
const runningMessageId = pending?.messageId;
|
|
752
|
+
pendingToolCalls.delete(event.toolCallId);
|
|
753
|
+
|
|
754
|
+
if (toolName === 'explore') {
|
|
755
|
+
exploreMessageIdRef.current = null;
|
|
756
|
+
setExploreAbortController(null);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const { content: toolContent, success } = formatToolMessage(
|
|
760
|
+
toolName,
|
|
761
|
+
toolArgs,
|
|
762
|
+
event.result,
|
|
763
|
+
{ maxLines: DEFAULT_MAX_TOOL_LINES }
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
const toolResultStr = typeof event.result === 'string' ? event.result : JSON.stringify(event.result);
|
|
767
|
+
totalChars += toolResultStr.length;
|
|
768
|
+
setCurrentTokens(estimateTokens());
|
|
769
|
+
|
|
770
|
+
if (assistantChunk.trim()) {
|
|
771
|
+
conversationSteps.push({
|
|
772
|
+
type: 'assistant',
|
|
773
|
+
content: assistantChunk,
|
|
774
|
+
timestamp: Date.now()
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
conversationSteps.push({
|
|
779
|
+
type: 'tool',
|
|
780
|
+
content: toolContent,
|
|
781
|
+
toolName,
|
|
782
|
+
toolArgs,
|
|
783
|
+
toolResult: event.result,
|
|
784
|
+
timestamp: Date.now()
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
setMessages((prev: Message[]) => {
|
|
788
|
+
const newMessages = [...prev];
|
|
789
|
+
|
|
790
|
+
let runningIndex = -1;
|
|
791
|
+
if (runningMessageId) {
|
|
792
|
+
runningIndex = newMessages.findIndex(m => m.id === runningMessageId);
|
|
793
|
+
} else if (toolName === 'bash' || toolName === 'explore' || toolName.startsWith('mcp__')) {
|
|
794
|
+
runningIndex = newMessages.findIndex(m => m.isRunning && m.toolName === toolName);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (runningIndex !== -1) {
|
|
798
|
+
newMessages[runningIndex] = {
|
|
799
|
+
...newMessages[runningIndex]!,
|
|
800
|
+
content: toolContent,
|
|
801
|
+
toolArgs: toolArgs,
|
|
802
|
+
toolResult: event.result,
|
|
803
|
+
success,
|
|
804
|
+
isRunning: false,
|
|
805
|
+
runningStartTime: undefined,
|
|
806
|
+
timestamp: Date.now()
|
|
807
|
+
};
|
|
808
|
+
return newMessages;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
newMessages.push({
|
|
812
|
+
id: createId(),
|
|
813
|
+
role: "tool",
|
|
814
|
+
content: toolContent,
|
|
815
|
+
toolName,
|
|
816
|
+
toolArgs: toolArgs,
|
|
817
|
+
toolResult: event.result,
|
|
818
|
+
success: success,
|
|
819
|
+
timestamp: Date.now()
|
|
820
|
+
});
|
|
821
|
+
return newMessages;
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
assistantChunk = '';
|
|
825
|
+
assistantMessageId = null;
|
|
826
|
+
} else if (event.type === 'error') {
|
|
827
|
+
if (abortController.signal.aborted) {
|
|
828
|
+
notifyAbort();
|
|
829
|
+
streamHadError = true;
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
if (assistantChunk.trim()) {
|
|
833
|
+
conversationSteps.push({
|
|
834
|
+
type: 'assistant',
|
|
835
|
+
content: assistantChunk,
|
|
836
|
+
timestamp: Date.now()
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const errorContent = formatErrorMessage('API', event.error);
|
|
841
|
+
conversationSteps.push({
|
|
842
|
+
type: 'assistant',
|
|
843
|
+
content: errorContent,
|
|
844
|
+
timestamp: Date.now()
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
setMessages((prev: Message[]) => {
|
|
848
|
+
const newMessages = [...prev];
|
|
849
|
+
newMessages.push({
|
|
850
|
+
id: createId(),
|
|
851
|
+
role: 'assistant',
|
|
852
|
+
content: errorContent,
|
|
853
|
+
isError: true,
|
|
854
|
+
});
|
|
855
|
+
return newMessages;
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
assistantChunk = '';
|
|
859
|
+
assistantMessageId = null;
|
|
860
|
+
streamHadError = true;
|
|
861
|
+
break;
|
|
862
|
+
} else if (event.type === 'finish') {
|
|
863
|
+
if (event.usage && event.usage.totalTokens > 0) {
|
|
864
|
+
totalTokens = {
|
|
865
|
+
prompt: event.usage.promptTokens,
|
|
866
|
+
completion: event.usage.completionTokens,
|
|
867
|
+
total: event.usage.totalTokens
|
|
868
|
+
};
|
|
869
|
+
lastPromptTokensRef.current = event.usage.promptTokens;
|
|
870
|
+
setCurrentTokens(event.usage.totalTokens);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (abortController.signal.aborted) {
|
|
876
|
+
notifyAbort();
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
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) {
|
|
918
|
+
if (abortController.signal.aborted) {
|
|
919
|
+
notifyAbort();
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
|
|
923
|
+
const errorContent = formatErrorMessage('Mosaic', errorMessage);
|
|
924
|
+
setMessages((prev: Message[]) => {
|
|
925
|
+
const newMessages = [...prev];
|
|
926
|
+
if (newMessages[newMessages.length - 1]?.role === 'assistant' && newMessages[newMessages.length - 1]?.content === '') {
|
|
927
|
+
newMessages[newMessages.length - 1] = {
|
|
928
|
+
id: newMessages[newMessages.length - 1]!.id,
|
|
929
|
+
role: "assistant",
|
|
930
|
+
content: errorContent,
|
|
931
|
+
isError: true
|
|
932
|
+
};
|
|
933
|
+
} else {
|
|
934
|
+
newMessages.push({
|
|
935
|
+
id: createId(),
|
|
936
|
+
role: "assistant",
|
|
937
|
+
content: errorContent,
|
|
938
|
+
isError: true
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
return newMessages;
|
|
942
|
+
});
|
|
943
|
+
} finally {
|
|
944
|
+
if (abortControllerRef.current === abortController) {
|
|
945
|
+
abortControllerRef.current = null;
|
|
946
|
+
}
|
|
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 };
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return newMessages;
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
setIsProcessing(false);
|
|
962
|
+
setProcessingStartTime(null);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const commandMessage: Message = {
|
|
969
|
+
id: createId(),
|
|
970
|
+
role: "slash",
|
|
971
|
+
content: result.content,
|
|
972
|
+
isError: !result.success
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
setMessages((prev: Message[]) => [...prev, commandMessage]);
|
|
976
|
+
|
|
977
|
+
if (result.shouldAddToHistory !== false) {
|
|
978
|
+
addInputToHistory(value.trim());
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const composedContent = hasPastedContent
|
|
986
|
+
? `${meta!.pastedContent!}${value.trim() ? `\n\n${value}` : ''}`
|
|
987
|
+
: value;
|
|
988
|
+
|
|
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
|
+
|
|
1005
|
+
setMessages((prev: Message[]) => [...prev, userMessage]);
|
|
1006
|
+
setIsProcessing(true);
|
|
1007
|
+
const localStartTime = Date.now();
|
|
1008
|
+
setProcessingStartTime(localStartTime);
|
|
1009
|
+
setCurrentTokens(0);
|
|
1010
|
+
lastPromptTokensRef.current = 0;
|
|
1011
|
+
shouldAutoScroll.current = true;
|
|
1012
|
+
|
|
1013
|
+
const conversationId = createId();
|
|
1014
|
+
const conversationSteps: ConversationStep[] = [];
|
|
1015
|
+
let totalTokens = { prompt: 0, completion: 0, total: 0 };
|
|
1016
|
+
let stepCount = 0;
|
|
1017
|
+
let totalChars = 0;
|
|
1018
|
+
for (const m of messages) {
|
|
1019
|
+
if (m.role === 'assistant') {
|
|
1020
|
+
totalChars += m.content.length;
|
|
1021
|
+
if (m.thinkingContent) totalChars += m.thinkingContent.length;
|
|
1022
|
+
} else if (m.role === 'tool') {
|
|
1023
|
+
totalChars += m.content.length;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const estimateTokens = () => Math.ceil(totalChars / 4);
|
|
1028
|
+
setCurrentTokens(estimateTokens());
|
|
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
|
+
};
|
|
1042
|
+
const abortController = new AbortController();
|
|
1043
|
+
abortControllerRef.current = abortController;
|
|
1044
|
+
let abortNotified = false;
|
|
1045
|
+
const notifyAbort = () => {
|
|
1046
|
+
if (abortNotified) return;
|
|
1047
|
+
abortNotified = true;
|
|
1048
|
+
setMessages((prev: Message[]) => {
|
|
1049
|
+
const newMessages = [...prev];
|
|
1050
|
+
newMessages.push({
|
|
1051
|
+
id: createId(),
|
|
1052
|
+
role: "tool",
|
|
1053
|
+
success: false,
|
|
1054
|
+
content: "Generation aborted. \n↪ What should Mosaic do instead?"
|
|
1055
|
+
});
|
|
1056
|
+
return newMessages;
|
|
1057
|
+
});
|
|
1058
|
+
};
|
|
1059
|
+
|
|
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) {
|
|
1073
|
+
setMessages((prev: Message[]) => {
|
|
1074
|
+
const newMessages = [...prev];
|
|
1075
|
+
newMessages.push({
|
|
1076
|
+
id: createId(),
|
|
1077
|
+
role: "assistant",
|
|
1078
|
+
content: `Ollama error: ${providerStatus.error || 'Could not start Ollama. Make sure Ollama is installed.'}`,
|
|
1079
|
+
isError: true
|
|
1080
|
+
});
|
|
1081
|
+
return newMessages;
|
|
1082
|
+
});
|
|
1083
|
+
setIsProcessing(false);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const agent = new Agent();
|
|
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());
|
|
1125
|
+
|
|
1126
|
+
const { title, cleanContent, isPending, noTitle } = extractTitle(assistantChunk, titleExtractedRef.current);
|
|
1127
|
+
|
|
1128
|
+
if (title) {
|
|
1129
|
+
titleExtractedRef.current = true;
|
|
1130
|
+
currentTitleRef.current = title;
|
|
1131
|
+
setCurrentTitle(title);
|
|
1132
|
+
setTerminalTitle(title);
|
|
1133
|
+
} else if (noTitle) {
|
|
1134
|
+
titleExtractedRef.current = true;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (isPending) continue;
|
|
1138
|
+
|
|
1139
|
+
if (assistantMessageId === null) {
|
|
1140
|
+
assistantMessageId = createId();
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const displayContent = cleanContent;
|
|
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
|
+
});
|
|
1160
|
+
} else if (event.type === 'step-start') {
|
|
1161
|
+
stepCount++;
|
|
1162
|
+
} else if (event.type === 'tool-call-end') {
|
|
1163
|
+
totalChars += JSON.stringify(event.args).length;
|
|
1164
|
+
setCurrentTokens(estimateTokens());
|
|
1165
|
+
|
|
1166
|
+
const isExploreTool = event.toolName === 'explore';
|
|
1167
|
+
const isMcpTool = event.toolName.startsWith('mcp__');
|
|
1168
|
+
let runningMessageId: string | undefined;
|
|
1169
|
+
|
|
1170
|
+
if (isExploreTool) {
|
|
1171
|
+
setExploreAbortController(abortController);
|
|
1172
|
+
exploreToolsRef.current = [];
|
|
1173
|
+
const purpose = (event.args.purpose as string) || 'exploring...';
|
|
1174
|
+
explorePurposeRef.current = purpose;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (isExploreTool || isMcpTool) {
|
|
1178
|
+
runningMessageId = createId();
|
|
1179
|
+
if (isExploreTool) {
|
|
1180
|
+
exploreMessageIdRef.current = runningMessageId;
|
|
1181
|
+
}
|
|
1182
|
+
const { name: toolDisplayName, info: toolInfo } = parseToolHeader(event.toolName, event.args);
|
|
1183
|
+
const runningContent = toolInfo ? `${toolDisplayName} (${toolInfo})` : toolDisplayName;
|
|
1184
|
+
|
|
1185
|
+
setMessages((prev: Message[]) => {
|
|
1186
|
+
const newMessages = [...prev];
|
|
1187
|
+
newMessages.push({
|
|
1188
|
+
id: runningMessageId!,
|
|
1189
|
+
role: "tool",
|
|
1190
|
+
content: runningContent,
|
|
1191
|
+
toolName: event.toolName,
|
|
1192
|
+
toolArgs: event.args,
|
|
1193
|
+
success: true,
|
|
1194
|
+
isRunning: true,
|
|
1195
|
+
runningStartTime: Date.now()
|
|
1196
|
+
});
|
|
1197
|
+
return newMessages;
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
pendingToolCalls.set(event.toolCallId, {
|
|
1202
|
+
toolName: event.toolName,
|
|
1203
|
+
args: event.args,
|
|
1204
|
+
messageId: runningMessageId
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
} else if (event.type === 'tool-result') {
|
|
1208
|
+
const pending = pendingToolCalls.get(event.toolCallId);
|
|
1209
|
+
const toolName = pending?.toolName ?? event.toolName;
|
|
1210
|
+
const toolArgs = pending?.args ?? {};
|
|
1211
|
+
const runningMessageId = pending?.messageId;
|
|
1212
|
+
pendingToolCalls.delete(event.toolCallId);
|
|
1213
|
+
|
|
1214
|
+
if (toolName === 'explore') {
|
|
1215
|
+
exploreMessageIdRef.current = null;
|
|
1216
|
+
setExploreAbortController(null);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const { content: toolContent, success } = formatToolMessage(
|
|
1220
|
+
toolName,
|
|
1221
|
+
toolArgs,
|
|
1222
|
+
event.result,
|
|
1223
|
+
{ maxLines: DEFAULT_MAX_TOOL_LINES }
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
const toolResultStr = typeof event.result === 'string' ? event.result : JSON.stringify(event.result);
|
|
1227
|
+
totalChars += toolResultStr.length;
|
|
1228
|
+
setCurrentTokens(estimateTokens());
|
|
1229
|
+
|
|
1230
|
+
if (assistantChunk.trim()) {
|
|
1231
|
+
conversationSteps.push({
|
|
1232
|
+
type: 'assistant',
|
|
1233
|
+
content: assistantChunk,
|
|
1234
|
+
timestamp: Date.now()
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
conversationSteps.push({
|
|
1239
|
+
type: 'tool',
|
|
1240
|
+
content: toolContent,
|
|
1241
|
+
toolName,
|
|
1242
|
+
toolArgs,
|
|
1243
|
+
toolResult: event.result,
|
|
1244
|
+
timestamp: Date.now()
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
setMessages((prev: Message[]) => {
|
|
1248
|
+
const newMessages = [...prev];
|
|
1249
|
+
|
|
1250
|
+
let runningIndex = -1;
|
|
1251
|
+
if (runningMessageId) {
|
|
1252
|
+
runningIndex = newMessages.findIndex(m => m.id === runningMessageId);
|
|
1253
|
+
} else if (toolName === 'bash' || toolName === 'explore' || toolName.startsWith('mcp__')) {
|
|
1254
|
+
runningIndex = newMessages.findIndex(m => m.isRunning && m.toolName === toolName);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (runningIndex !== -1) {
|
|
1258
|
+
newMessages[runningIndex] = {
|
|
1259
|
+
...newMessages[runningIndex]!,
|
|
1260
|
+
content: toolContent,
|
|
1261
|
+
toolArgs: toolArgs,
|
|
1262
|
+
toolResult: event.result,
|
|
1263
|
+
success,
|
|
1264
|
+
isRunning: false,
|
|
1265
|
+
runningStartTime: undefined
|
|
1266
|
+
};
|
|
1267
|
+
return newMessages;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
newMessages.push({
|
|
1271
|
+
id: createId(),
|
|
1272
|
+
role: "tool",
|
|
1273
|
+
content: toolContent,
|
|
1274
|
+
toolName,
|
|
1275
|
+
toolArgs: toolArgs,
|
|
1276
|
+
toolResult: event.result,
|
|
1277
|
+
success: success
|
|
1278
|
+
});
|
|
1279
|
+
return newMessages;
|
|
1280
|
+
});
|
|
1281
|
+
|
|
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
|
+
}
|
|
1291
|
+
if (assistantChunk.trim()) {
|
|
1292
|
+
conversationSteps.push({
|
|
1293
|
+
type: 'assistant',
|
|
1294
|
+
content: assistantChunk,
|
|
1295
|
+
timestamp: Date.now()
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const errorContent = formatErrorMessage('API', event.error);
|
|
1300
|
+
conversationSteps.push({
|
|
1301
|
+
type: 'assistant',
|
|
1302
|
+
content: errorContent,
|
|
1303
|
+
timestamp: Date.now()
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
setMessages((prev: Message[]) => {
|
|
1307
|
+
const newMessages = [...prev];
|
|
1308
|
+
newMessages.push({
|
|
1309
|
+
id: createId(),
|
|
1310
|
+
role: 'assistant',
|
|
1311
|
+
content: errorContent,
|
|
1312
|
+
isError: true,
|
|
1313
|
+
});
|
|
1314
|
+
return newMessages;
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
assistantChunk = '';
|
|
1318
|
+
thinkingChunk = '';
|
|
1319
|
+
assistantMessageId = null;
|
|
1320
|
+
streamHadError = true;
|
|
1321
|
+
break;
|
|
1322
|
+
} else if (event.type === 'finish') {
|
|
1323
|
+
if (event.usage && event.usage.totalTokens > 0) {
|
|
1324
|
+
totalTokens = {
|
|
1325
|
+
prompt: event.usage.promptTokens,
|
|
1326
|
+
completion: event.usage.completionTokens,
|
|
1327
|
+
total: event.usage.totalTokens
|
|
1328
|
+
};
|
|
1329
|
+
lastPromptTokensRef.current = event.usage.promptTokens;
|
|
1330
|
+
setCurrentTokens(event.usage.totalTokens);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
if (abortController.signal.aborted) {
|
|
1336
|
+
notifyAbort();
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
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
|
+
}
|
|
1391
|
+
|
|
1392
|
+
} catch (error) {
|
|
1393
|
+
if (abortController.signal.aborted) {
|
|
1394
|
+
notifyAbort();
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
|
|
1398
|
+
const errorContent = formatErrorMessage('Mosaic', errorMessage);
|
|
1399
|
+
setMessages((prev: Message[]) => {
|
|
1400
|
+
const newMessages = [...prev];
|
|
1401
|
+
if (newMessages[newMessages.length - 1]?.role === 'assistant' && newMessages[newMessages.length - 1]?.content === '') {
|
|
1402
|
+
newMessages[newMessages.length - 1] = {
|
|
1403
|
+
id: newMessages[newMessages.length - 1]!.id,
|
|
1404
|
+
role: "assistant",
|
|
1405
|
+
content: errorContent,
|
|
1406
|
+
isError: true
|
|
1407
|
+
};
|
|
1408
|
+
} else {
|
|
1409
|
+
newMessages.push({
|
|
1410
|
+
id: createId(),
|
|
1411
|
+
role: "assistant",
|
|
1412
|
+
content: errorContent,
|
|
1413
|
+
isError: true
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
return newMessages;
|
|
1417
|
+
});
|
|
1418
|
+
} finally {
|
|
1419
|
+
if (abortControllerRef.current === abortController) {
|
|
1420
|
+
abortControllerRef.current = null;
|
|
1421
|
+
}
|
|
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 };
|
|
1430
|
+
break;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return newMessages;
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
setIsProcessing(false);
|
|
1437
|
+
setProcessingStartTime(null);
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
|
|
1441
|
+
useEffect(() => {
|
|
1442
|
+
if (initialMessage && !initialMessageProcessed.current && currentPage === "chat") {
|
|
1443
|
+
initialMessageProcessed.current = true;
|
|
1444
|
+
handleSubmit(initialMessage);
|
|
1445
|
+
}
|
|
1446
|
+
}, [initialMessage, currentPage, handleSubmit]);
|
|
1447
|
+
|
|
1448
|
+
if (currentPage === "home") {
|
|
1449
|
+
const handleHomeSubmit = (value: string, meta?: InputSubmitMeta) => {
|
|
1450
|
+
const hasPastedContent = Boolean(meta?.isPaste && meta.pastedContent);
|
|
1451
|
+
if (!value.trim() && !hasPastedContent) return;
|
|
1452
|
+
setCurrentPage("chat");
|
|
1453
|
+
handleSubmit(value, meta);
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
return (
|
|
1457
|
+
<HomePage
|
|
1458
|
+
onSubmit={handleHomeSubmit}
|
|
1459
|
+
pasteRequestId={pasteRequestId}
|
|
1460
|
+
shortcutsOpen={shortcutsOpen}
|
|
1461
|
+
/>
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
return (
|
|
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
|
+
}
|