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