@kirosnn/mosaic 0.0.9 → 0.71.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/LICENSE +1 -1
- package/README.md +83 -19
- package/package.json +52 -47
- package/src/agent/prompts/systemPrompt.ts +198 -68
- package/src/agent/prompts/toolsPrompt.ts +217 -135
- package/src/agent/provider/anthropic.ts +19 -15
- package/src/agent/provider/google.ts +21 -17
- package/src/agent/provider/ollama.ts +80 -41
- package/src/agent/provider/openai.ts +107 -67
- package/src/agent/provider/reasoning.ts +29 -0
- package/src/agent/provider/xai.ts +19 -15
- package/src/agent/tools/definitions.ts +9 -5
- package/src/agent/tools/executor.ts +655 -46
- package/src/agent/tools/exploreExecutor.ts +12 -12
- package/src/agent/tools/fetch.ts +58 -0
- package/src/agent/tools/glob.ts +20 -4
- package/src/agent/tools/grep.ts +62 -8
- package/src/agent/tools/plan.ts +27 -0
- package/src/agent/tools/read.ts +2 -0
- package/src/agent/types.ts +6 -6
- package/src/components/App.tsx +67 -25
- package/src/components/CustomInput.tsx +274 -68
- package/src/components/Main.tsx +323 -168
- package/src/components/ShortcutsModal.tsx +11 -8
- package/src/components/main/ChatPage.tsx +217 -58
- package/src/components/main/HomePage.tsx +5 -1
- package/src/components/main/ThinkingIndicator.tsx +11 -1
- package/src/components/main/types.ts +11 -10
- package/src/index.tsx +3 -5
- package/src/utils/approvalBridge.ts +29 -8
- package/src/utils/approvalModeBridge.ts +17 -0
- package/src/utils/commands/approvals.ts +48 -0
- package/src/utils/commands/image.ts +109 -0
- package/src/utils/commands/index.ts +5 -1
- package/src/utils/diffRendering.tsx +13 -14
- package/src/utils/history.ts +82 -40
- package/src/utils/imageBridge.ts +28 -0
- package/src/utils/images.ts +31 -0
- package/src/utils/models.ts +0 -7
- package/src/utils/notificationBridge.ts +23 -0
- package/src/utils/toolFormatting.ts +162 -43
- package/src/web/app.tsx +94 -34
- package/src/web/assets/css/ChatPage.css +102 -30
- package/src/web/assets/css/MessageItem.css +26 -29
- package/src/web/assets/css/ThinkingIndicator.css +44 -6
- package/src/web/assets/css/ToolMessage.css +36 -14
- package/src/web/components/ChatPage.tsx +228 -105
- package/src/web/components/HomePage.tsx +6 -6
- package/src/web/components/MessageItem.tsx +88 -89
- package/src/web/components/Setup.tsx +1 -1
- package/src/web/components/Sidebar.tsx +1 -1
- package/src/web/components/ThinkingIndicator.tsx +40 -21
- package/src/web/router.ts +1 -1
- package/src/web/server.tsx +187 -39
- package/src/web/storage.ts +23 -1
- package/src/web/types.ts +7 -6
package/src/components/Main.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useState, useEffect, useRef } from "react";
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import type { ImagePart, TextPart, UserContent } from "ai";
|
|
2
3
|
import { useKeyboard } from "@opentui/react";
|
|
3
4
|
import { Agent } from "../agent";
|
|
4
5
|
import { saveConversation, addInputToHistory, type ConversationHistory, type ConversationStep } from "../utils/history";
|
|
@@ -13,11 +14,14 @@ import { subscribeUndoRedo } from "../utils/undoRedoBridge";
|
|
|
13
14
|
import { setExploreAbortController, setExploreToolCallback, abortExplore } from "../utils/exploreBridge";
|
|
14
15
|
import { initializeSession, saveState } from "../utils/undoRedo";
|
|
15
16
|
import { resetFileChanges } from "../utils/fileChangeTracker";
|
|
16
|
-
import { getCurrentQuestion, cancelQuestion } from "../utils/questionBridge";
|
|
17
|
-
import { getCurrentApproval, cancelApproval } from "../utils/approvalBridge";
|
|
18
|
-
import { BLEND_WORDS, type MainProps, type Message } from "./main/types";
|
|
19
|
-
import { HomePage } from './main/HomePage';
|
|
20
|
-
import { ChatPage } from './main/ChatPage';
|
|
17
|
+
import { getCurrentQuestion, cancelQuestion } from "../utils/questionBridge";
|
|
18
|
+
import { getCurrentApproval, cancelApproval } from "../utils/approvalBridge";
|
|
19
|
+
import { BLEND_WORDS, type MainProps, type Message } from "./main/types";
|
|
20
|
+
import { HomePage } from './main/HomePage';
|
|
21
|
+
import { ChatPage } from './main/ChatPage';
|
|
22
|
+
import type { ImageAttachment } from "../utils/images";
|
|
23
|
+
import { subscribeImageCommand, setImageSupport } from "../utils/imageBridge";
|
|
24
|
+
import { findModelsDevModelById, modelAcceptsImages } from "../utils/models";
|
|
21
25
|
|
|
22
26
|
function extractTitle(content: string, alreadyResolved: boolean): { title: string | null; cleanContent: string; isPending: boolean; noTitle: boolean } {
|
|
23
27
|
const trimmed = content.trimStart();
|
|
@@ -54,8 +58,10 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
54
58
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
55
59
|
const [terminalHeight, setTerminalHeight] = useState(process.stdout.rows || 24);
|
|
56
60
|
const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80);
|
|
57
|
-
const [questionRequest, setQuestionRequest] = useState<QuestionRequest | null>(null);
|
|
58
|
-
const [currentTitle, setCurrentTitle] = useState<string | null>(null);
|
|
61
|
+
const [questionRequest, setQuestionRequest] = useState<QuestionRequest | null>(null);
|
|
62
|
+
const [currentTitle, setCurrentTitle] = useState<string | null>(null);
|
|
63
|
+
const [pendingImages, setPendingImages] = useState<ImageAttachment[]>([]);
|
|
64
|
+
const [imagesSupported, setImagesSupported] = useState(false);
|
|
59
65
|
const currentTitleRef = useRef<string | null>(null);
|
|
60
66
|
const titleExtractedRef = useRef(false);
|
|
61
67
|
const shouldAutoScroll = useRef(true);
|
|
@@ -71,10 +77,31 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
71
77
|
|
|
72
78
|
const createId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
73
79
|
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
initializeCommands();
|
|
76
|
-
initializeSession();
|
|
77
|
-
}, []);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
initializeCommands();
|
|
82
|
+
initializeSession();
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const loadSupport = async () => {
|
|
87
|
+
const config = readConfig();
|
|
88
|
+
if (!config.model) {
|
|
89
|
+
setImagesSupported(false);
|
|
90
|
+
setImageSupport(false);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const result = await findModelsDevModelById(config.model);
|
|
95
|
+
const supported = Boolean(result && result.model && modelAcceptsImages(result.model));
|
|
96
|
+
setImagesSupported(supported);
|
|
97
|
+
setImageSupport(supported);
|
|
98
|
+
} catch {
|
|
99
|
+
setImagesSupported(false);
|
|
100
|
+
setImageSupport(false);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
loadSupport();
|
|
104
|
+
}, []);
|
|
78
105
|
|
|
79
106
|
useEffect(() => {
|
|
80
107
|
let lastExploreTokens = 0;
|
|
@@ -95,7 +122,7 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
95
122
|
const idx = newMessages.findIndex(m => m.id === exploreMessageIdRef.current);
|
|
96
123
|
if (idx !== -1) {
|
|
97
124
|
const toolLines = exploreToolsRef.current.map(t => {
|
|
98
|
-
const icon = t.success ? '
|
|
125
|
+
const icon = t.success ? '→' : '-';
|
|
99
126
|
return ` ${icon} ${t.tool}(${t.info})`;
|
|
100
127
|
});
|
|
101
128
|
const purpose = explorePurposeRef.current;
|
|
@@ -112,14 +139,35 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
112
139
|
};
|
|
113
140
|
}, []);
|
|
114
141
|
|
|
115
|
-
useEffect(() => {
|
|
116
|
-
return subscribeUndoRedo((state, action) => {
|
|
117
|
-
if (state) {
|
|
118
|
-
setMessages(state.messages);
|
|
119
|
-
resetFileChanges();
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
}, []);
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
return subscribeUndoRedo((state, action) => {
|
|
144
|
+
if (state) {
|
|
145
|
+
setMessages(state.messages);
|
|
146
|
+
resetFileChanges();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
return subscribeImageCommand((event) => {
|
|
153
|
+
if (event.type === "clear") {
|
|
154
|
+
setPendingImages([]);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (event.type === "remove") {
|
|
158
|
+
setPendingImages((prev) => prev.filter((img) => img.id !== event.id));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (!imagesSupported) return;
|
|
162
|
+
setPendingImages((prev) => [...prev, event.image]);
|
|
163
|
+
});
|
|
164
|
+
}, [imagesSupported]);
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (!imagesSupported) {
|
|
168
|
+
setPendingImages([]);
|
|
169
|
+
}
|
|
170
|
+
}, [imagesSupported]);
|
|
123
171
|
|
|
124
172
|
useEffect(() => {
|
|
125
173
|
const handleResize = () => {
|
|
@@ -260,24 +308,58 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
260
308
|
}
|
|
261
309
|
}, [copyRequestId, onCopy, messages]);
|
|
262
310
|
|
|
263
|
-
useKeyboard((key) => {
|
|
264
|
-
if (key.name === '
|
|
265
|
-
if (getCurrentQuestion()) {
|
|
266
|
-
cancelQuestion();
|
|
267
|
-
}
|
|
311
|
+
useKeyboard((key) => {
|
|
312
|
+
if ((key.name === 'c' && key.ctrl) || key.sequence === '\x03') {
|
|
313
|
+
if (getCurrentQuestion()) {
|
|
314
|
+
cancelQuestion();
|
|
315
|
+
}
|
|
316
|
+
if (getCurrentApproval()) {
|
|
317
|
+
cancelApproval();
|
|
318
|
+
}
|
|
319
|
+
abortControllerRef.current?.abort();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (key.name === 'escape') {
|
|
324
|
+
if (getCurrentQuestion()) {
|
|
325
|
+
cancelQuestion();
|
|
326
|
+
}
|
|
268
327
|
if (getCurrentApproval()) {
|
|
269
328
|
cancelApproval();
|
|
270
329
|
}
|
|
271
330
|
abortControllerRef.current?.abort();
|
|
272
331
|
return;
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const buildUserContent = (text: string, images?: ImageAttachment[]): UserContent => {
|
|
336
|
+
if (!images || images.length === 0) return text;
|
|
337
|
+
const parts: Array<TextPart | ImagePart> = [];
|
|
338
|
+
parts.push({ type: "text", text });
|
|
339
|
+
for (const img of images) {
|
|
340
|
+
parts.push({ type: "image", image: img.data, mimeType: img.mimeType });
|
|
341
|
+
}
|
|
342
|
+
return parts;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const buildConversationHistory = (base: Message[], includeImages: boolean) => {
|
|
346
|
+
return base
|
|
347
|
+
.filter((m): m is Message & { role: "user" | "assistant" } => m.role === "user" || m.role === "assistant")
|
|
348
|
+
.map((m) => {
|
|
349
|
+
if (m.role === "user") {
|
|
350
|
+
const content = includeImages ? buildUserContent(m.content, m.images) : m.content;
|
|
351
|
+
return { role: "user" as const, content };
|
|
352
|
+
}
|
|
353
|
+
return { role: "assistant" as const, content: m.content };
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const handleSubmit = async (value: string, meta?: InputSubmitMeta) => {
|
|
358
|
+
if (isProcessing) return;
|
|
359
|
+
|
|
360
|
+
const hasPastedContent = Boolean(meta?.isPaste && meta.pastedContent);
|
|
361
|
+
const hasImages = imagesSupported && pendingImages.length > 0;
|
|
362
|
+
if (!value.trim() && !hasPastedContent && !hasImages) return;
|
|
281
363
|
|
|
282
364
|
if (isCommand(value)) {
|
|
283
365
|
const result = await executeCommand(value);
|
|
@@ -342,9 +424,12 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
342
424
|
timestamp: Date.now()
|
|
343
425
|
});
|
|
344
426
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
427
|
+
let responseDuration: number | null = null;
|
|
428
|
+
let responseBlendWord: string | null = null;
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
const providerStatus = await Agent.ensureProviderReady();
|
|
432
|
+
if (!providerStatus.ready) {
|
|
348
433
|
setMessages((prev: Message[]) => {
|
|
349
434
|
const newMessages = [...prev];
|
|
350
435
|
newMessages.push({
|
|
@@ -360,9 +445,7 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
360
445
|
}
|
|
361
446
|
|
|
362
447
|
const agent = new Agent();
|
|
363
|
-
const conversationHistory = [...messages, userMessage]
|
|
364
|
-
.filter((m): m is Message & { role: 'user' | 'assistant' } => m.role === 'user' || m.role === 'assistant')
|
|
365
|
-
.map((m) => ({ role: m.role, content: m.content }));
|
|
448
|
+
const conversationHistory = buildConversationHistory([...messages, userMessage], imagesSupported);
|
|
366
449
|
let assistantChunk = '';
|
|
367
450
|
let thinkingChunk = '';
|
|
368
451
|
const pendingToolCalls = new Map<string, { toolName: string; args: Record<string, unknown>; messageId?: string }>();
|
|
@@ -614,27 +697,44 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
614
697
|
return;
|
|
615
698
|
}
|
|
616
699
|
|
|
617
|
-
if (!streamHadError && assistantChunk.trim()) {
|
|
618
|
-
conversationSteps.push({
|
|
619
|
-
type: 'assistant',
|
|
620
|
-
content: assistantChunk,
|
|
621
|
-
timestamp: Date.now()
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
700
|
+
if (!streamHadError && assistantChunk.trim()) {
|
|
701
|
+
conversationSteps.push({
|
|
702
|
+
type: 'assistant',
|
|
703
|
+
content: assistantChunk,
|
|
704
|
+
timestamp: Date.now()
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
responseDuration = Date.now() - localStartTime;
|
|
709
|
+
if (responseDuration >= 60000) {
|
|
710
|
+
responseBlendWord = BLEND_WORDS[Math.floor(Math.random() * BLEND_WORDS.length)];
|
|
711
|
+
for (let i = conversationSteps.length - 1; i >= 0; i--) {
|
|
712
|
+
if (conversationSteps[i]?.type === 'assistant') {
|
|
713
|
+
conversationSteps[i] = {
|
|
714
|
+
...conversationSteps[i]!,
|
|
715
|
+
responseDuration,
|
|
716
|
+
blendWord: responseBlendWord
|
|
717
|
+
};
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const conversationData: ConversationHistory = {
|
|
724
|
+
id: conversationId,
|
|
725
|
+
timestamp: Date.now(),
|
|
726
|
+
steps: conversationSteps,
|
|
727
|
+
totalSteps: stepCount,
|
|
728
|
+
title: currentTitleRef.current ?? currentTitle ?? null,
|
|
729
|
+
workspace: process.cwd(),
|
|
730
|
+
totalTokens: totalTokens.total > 0 ? totalTokens : undefined,
|
|
731
|
+
model: config.model,
|
|
732
|
+
provider: config.provider
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
saveConversation(conversationData);
|
|
736
|
+
|
|
737
|
+
} catch (error) {
|
|
638
738
|
if (abortController.signal.aborted) {
|
|
639
739
|
notifyAbort();
|
|
640
740
|
return;
|
|
@@ -664,14 +764,14 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
664
764
|
if (abortControllerRef.current === abortController) {
|
|
665
765
|
abortControllerRef.current = null;
|
|
666
766
|
}
|
|
667
|
-
const duration = Date.now() - localStartTime;
|
|
668
|
-
if (duration >= 60000) {
|
|
669
|
-
const blendWord = BLEND_WORDS[Math.floor(Math.random() * BLEND_WORDS.length)];
|
|
670
|
-
setMessages((prev: Message[]) => {
|
|
671
|
-
const newMessages = [...prev];
|
|
672
|
-
for (let i = newMessages.length - 1; i >= 0; i--) {
|
|
673
|
-
if (newMessages[i]?.role === 'assistant') {
|
|
674
|
-
newMessages[i] = { ...newMessages[i]!, responseDuration: duration, blendWord };
|
|
767
|
+
const duration = responseDuration ?? (Date.now() - localStartTime);
|
|
768
|
+
if (duration >= 60000) {
|
|
769
|
+
const blendWord = responseBlendWord ?? BLEND_WORDS[Math.floor(Math.random() * BLEND_WORDS.length)];
|
|
770
|
+
setMessages((prev: Message[]) => {
|
|
771
|
+
const newMessages = [...prev];
|
|
772
|
+
for (let i = newMessages.length - 1; i >= 0; i--) {
|
|
773
|
+
if (newMessages[i]?.role === 'assistant') {
|
|
774
|
+
newMessages[i] = { ...newMessages[i]!, responseDuration: duration, blendWord };
|
|
675
775
|
break;
|
|
676
776
|
}
|
|
677
777
|
}
|
|
@@ -706,17 +806,24 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
706
806
|
? `${meta!.pastedContent!}${value.trim() ? `\n\n${value}` : ''}`
|
|
707
807
|
: value;
|
|
708
808
|
|
|
709
|
-
addInputToHistory(value.trim() || (hasPastedContent ? '[Pasted text]' : value));
|
|
809
|
+
addInputToHistory(value.trim() || (hasPastedContent ? '[Pasted text]' : (hasImages ? '[Image]' : value)));
|
|
710
810
|
|
|
711
811
|
saveState(messages);
|
|
712
812
|
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
813
|
+
const imagesForMessage = imagesSupported ? pendingImages : [];
|
|
814
|
+
|
|
815
|
+
const userMessage: Message = {
|
|
816
|
+
id: createId(),
|
|
817
|
+
role: "user",
|
|
818
|
+
content: composedContent,
|
|
819
|
+
displayContent: meta?.isPaste ? '[Pasted text]' : undefined,
|
|
820
|
+
images: imagesForMessage.length > 0 ? imagesForMessage : undefined,
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
if (imagesForMessage.length > 0) {
|
|
824
|
+
setPendingImages([]);
|
|
825
|
+
}
|
|
826
|
+
|
|
720
827
|
setMessages((prev: Message[]) => [...prev, userMessage]);
|
|
721
828
|
setIsProcessing(true);
|
|
722
829
|
const localStartTime = Date.now();
|
|
@@ -759,15 +866,19 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
759
866
|
});
|
|
760
867
|
};
|
|
761
868
|
|
|
762
|
-
conversationSteps.push({
|
|
763
|
-
type: 'user',
|
|
764
|
-
content: composedContent,
|
|
765
|
-
timestamp: Date.now()
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
869
|
+
conversationSteps.push({
|
|
870
|
+
type: 'user',
|
|
871
|
+
content: composedContent,
|
|
872
|
+
timestamp: Date.now(),
|
|
873
|
+
images: imagesForMessage.length > 0 ? imagesForMessage : undefined
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
let responseDuration: number | null = null;
|
|
877
|
+
let responseBlendWord: string | null = null;
|
|
878
|
+
|
|
879
|
+
try {
|
|
880
|
+
const providerStatus = await Agent.ensureProviderReady();
|
|
881
|
+
if (!providerStatus.ready) {
|
|
771
882
|
setMessages((prev: Message[]) => {
|
|
772
883
|
const newMessages = [...prev];
|
|
773
884
|
newMessages.push({
|
|
@@ -783,20 +894,43 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
783
894
|
}
|
|
784
895
|
|
|
785
896
|
const agent = new Agent();
|
|
786
|
-
const conversationHistory = [...messages, userMessage]
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
let
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
897
|
+
const conversationHistory = buildConversationHistory([...messages, userMessage], imagesSupported);
|
|
898
|
+
let assistantChunk = '';
|
|
899
|
+
let thinkingChunk = '';
|
|
900
|
+
const pendingToolCalls = new Map<string, { toolName: string; args: Record<string, unknown>; messageId?: string }>();
|
|
901
|
+
let assistantMessageId: string | null = null;
|
|
902
|
+
let streamHadError = false;
|
|
903
|
+
titleExtractedRef.current = false;
|
|
904
|
+
|
|
905
|
+
for await (const event of agent.streamMessages(conversationHistory, { abortSignal: abortController.signal })) {
|
|
906
|
+
if (event.type === 'reasoning-delta') {
|
|
907
|
+
thinkingChunk += event.content;
|
|
908
|
+
totalChars += event.content.length;
|
|
909
|
+
setCurrentTokens(estimateTokens());
|
|
910
|
+
|
|
911
|
+
if (assistantMessageId === null) {
|
|
912
|
+
assistantMessageId = createId();
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const currentMessageId = assistantMessageId;
|
|
916
|
+
setMessages((prev: Message[]) => {
|
|
917
|
+
const newMessages = [...prev];
|
|
918
|
+
const messageIndex = newMessages.findIndex(m => m.id === currentMessageId);
|
|
919
|
+
|
|
920
|
+
if (messageIndex === -1) {
|
|
921
|
+
newMessages.push({ id: currentMessageId, role: "assistant", content: '', thinkingContent: thinkingChunk });
|
|
922
|
+
} else {
|
|
923
|
+
newMessages[messageIndex] = {
|
|
924
|
+
...newMessages[messageIndex]!,
|
|
925
|
+
thinkingContent: thinkingChunk
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
return newMessages;
|
|
929
|
+
});
|
|
930
|
+
} else if (event.type === 'text-delta') {
|
|
931
|
+
assistantChunk += event.content;
|
|
932
|
+
totalChars += event.content.length;
|
|
933
|
+
setCurrentTokens(estimateTokens());
|
|
800
934
|
|
|
801
935
|
const { title, cleanContent, isPending, noTitle } = extractTitle(assistantChunk, titleExtractedRef.current);
|
|
802
936
|
|
|
@@ -816,21 +950,22 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
816
950
|
}
|
|
817
951
|
|
|
818
952
|
const displayContent = cleanContent;
|
|
819
|
-
const currentMessageId = assistantMessageId;
|
|
820
|
-
setMessages((prev: Message[]) => {
|
|
821
|
-
const newMessages = [...prev];
|
|
822
|
-
const messageIndex = newMessages.findIndex(m => m.id === currentMessageId);
|
|
823
|
-
|
|
824
|
-
if (messageIndex === -1) {
|
|
825
|
-
newMessages.push({ id: currentMessageId, role: "assistant", content: displayContent });
|
|
826
|
-
} else {
|
|
827
|
-
newMessages[messageIndex] = {
|
|
828
|
-
...newMessages[messageIndex]!,
|
|
829
|
-
content: displayContent
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
953
|
+
const currentMessageId = assistantMessageId;
|
|
954
|
+
setMessages((prev: Message[]) => {
|
|
955
|
+
const newMessages = [...prev];
|
|
956
|
+
const messageIndex = newMessages.findIndex(m => m.id === currentMessageId);
|
|
957
|
+
|
|
958
|
+
if (messageIndex === -1) {
|
|
959
|
+
newMessages.push({ id: currentMessageId, role: "assistant", content: displayContent, thinkingContent: thinkingChunk });
|
|
960
|
+
} else {
|
|
961
|
+
newMessages[messageIndex] = {
|
|
962
|
+
...newMessages[messageIndex]!,
|
|
963
|
+
content: displayContent,
|
|
964
|
+
thinkingContent: thinkingChunk
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
return newMessages;
|
|
968
|
+
});
|
|
834
969
|
} else if (event.type === 'step-start') {
|
|
835
970
|
stepCount++;
|
|
836
971
|
} else if (event.type === 'tool-call-end') {
|
|
@@ -947,14 +1082,15 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
947
1082
|
return newMessages;
|
|
948
1083
|
});
|
|
949
1084
|
|
|
950
|
-
assistantChunk = '';
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1085
|
+
assistantChunk = '';
|
|
1086
|
+
thinkingChunk = '';
|
|
1087
|
+
assistantMessageId = null;
|
|
1088
|
+
} else if (event.type === 'error') {
|
|
1089
|
+
if (abortController.signal.aborted) {
|
|
1090
|
+
notifyAbort();
|
|
1091
|
+
streamHadError = true;
|
|
1092
|
+
break;
|
|
1093
|
+
}
|
|
958
1094
|
if (assistantChunk.trim()) {
|
|
959
1095
|
conversationSteps.push({
|
|
960
1096
|
type: 'assistant',
|
|
@@ -981,10 +1117,11 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
981
1117
|
return newMessages;
|
|
982
1118
|
});
|
|
983
1119
|
|
|
984
|
-
assistantChunk = '';
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1120
|
+
assistantChunk = '';
|
|
1121
|
+
thinkingChunk = '';
|
|
1122
|
+
assistantMessageId = null;
|
|
1123
|
+
streamHadError = true;
|
|
1124
|
+
break;
|
|
988
1125
|
} else if (event.type === 'finish') {
|
|
989
1126
|
if (event.usage && event.usage.totalTokens > 0) {
|
|
990
1127
|
totalTokens = {
|
|
@@ -1002,25 +1139,42 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
1002
1139
|
return;
|
|
1003
1140
|
}
|
|
1004
1141
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1142
|
+
if (!streamHadError && assistantChunk.trim()) {
|
|
1143
|
+
conversationSteps.push({
|
|
1144
|
+
type: 'assistant',
|
|
1145
|
+
content: assistantChunk,
|
|
1146
|
+
timestamp: Date.now()
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
responseDuration = Date.now() - localStartTime;
|
|
1151
|
+
if (responseDuration >= 60000) {
|
|
1152
|
+
responseBlendWord = BLEND_WORDS[Math.floor(Math.random() * BLEND_WORDS.length)];
|
|
1153
|
+
for (let i = conversationSteps.length - 1; i >= 0; i--) {
|
|
1154
|
+
if (conversationSteps[i]?.type === 'assistant') {
|
|
1155
|
+
conversationSteps[i] = {
|
|
1156
|
+
...conversationSteps[i]!,
|
|
1157
|
+
responseDuration,
|
|
1158
|
+
blendWord: responseBlendWord
|
|
1159
|
+
};
|
|
1160
|
+
break;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const conversationData: ConversationHistory = {
|
|
1166
|
+
id: conversationId,
|
|
1167
|
+
timestamp: Date.now(),
|
|
1168
|
+
steps: conversationSteps,
|
|
1169
|
+
totalSteps: stepCount,
|
|
1170
|
+
title: currentTitleRef.current ?? currentTitle ?? null,
|
|
1171
|
+
workspace: process.cwd(),
|
|
1172
|
+
totalTokens: totalTokens.total > 0 ? totalTokens : undefined,
|
|
1173
|
+
model: config.model,
|
|
1174
|
+
provider: config.provider
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
saveConversation(conversationData);
|
|
1024
1178
|
|
|
1025
1179
|
} catch (error) {
|
|
1026
1180
|
if (abortController.signal.aborted) {
|
|
@@ -1052,14 +1206,14 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
1052
1206
|
if (abortControllerRef.current === abortController) {
|
|
1053
1207
|
abortControllerRef.current = null;
|
|
1054
1208
|
}
|
|
1055
|
-
const duration = Date.now() - localStartTime;
|
|
1056
|
-
if (duration >= 60000) {
|
|
1057
|
-
const blendWord = BLEND_WORDS[Math.floor(Math.random() * BLEND_WORDS.length)];
|
|
1058
|
-
setMessages((prev: Message[]) => {
|
|
1059
|
-
const newMessages = [...prev];
|
|
1060
|
-
for (let i = newMessages.length - 1; i >= 0; i--) {
|
|
1061
|
-
if (newMessages[i]?.role === 'assistant') {
|
|
1062
|
-
newMessages[i] = { ...newMessages[i]!, responseDuration: duration, blendWord };
|
|
1209
|
+
const duration = responseDuration ?? (Date.now() - localStartTime);
|
|
1210
|
+
if (duration >= 60000) {
|
|
1211
|
+
const blendWord = responseBlendWord ?? BLEND_WORDS[Math.floor(Math.random() * BLEND_WORDS.length)];
|
|
1212
|
+
setMessages((prev: Message[]) => {
|
|
1213
|
+
const newMessages = [...prev];
|
|
1214
|
+
for (let i = newMessages.length - 1; i >= 0; i--) {
|
|
1215
|
+
if (newMessages[i]?.role === 'assistant') {
|
|
1216
|
+
newMessages[i] = { ...newMessages[i]!, responseDuration: duration, blendWord };
|
|
1063
1217
|
break;
|
|
1064
1218
|
}
|
|
1065
1219
|
}
|
|
@@ -1096,17 +1250,18 @@ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsO
|
|
|
1096
1250
|
}
|
|
1097
1251
|
|
|
1098
1252
|
return (
|
|
1099
|
-
<ChatPage
|
|
1100
|
-
messages={messages}
|
|
1101
|
-
isProcessing={isProcessing}
|
|
1102
|
-
processingStartTime={processingStartTime}
|
|
1103
|
-
currentTokens={currentTokens}
|
|
1104
|
-
scrollOffset={scrollOffset}
|
|
1105
|
-
terminalHeight={terminalHeight}
|
|
1106
|
-
terminalWidth={terminalWidth}
|
|
1107
|
-
pasteRequestId={pasteRequestId}
|
|
1108
|
-
shortcutsOpen={shortcutsOpen}
|
|
1109
|
-
onSubmit={handleSubmit}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1253
|
+
<ChatPage
|
|
1254
|
+
messages={messages}
|
|
1255
|
+
isProcessing={isProcessing}
|
|
1256
|
+
processingStartTime={processingStartTime}
|
|
1257
|
+
currentTokens={currentTokens}
|
|
1258
|
+
scrollOffset={scrollOffset}
|
|
1259
|
+
terminalHeight={terminalHeight}
|
|
1260
|
+
terminalWidth={terminalWidth}
|
|
1261
|
+
pasteRequestId={pasteRequestId}
|
|
1262
|
+
shortcutsOpen={shortcutsOpen}
|
|
1263
|
+
onSubmit={handleSubmit}
|
|
1264
|
+
pendingImages={pendingImages}
|
|
1265
|
+
/>
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
@@ -10,13 +10,16 @@ interface ShortcutsModalProps {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function ShortcutsModal({ activeTab }: ShortcutsModalProps) {
|
|
13
|
-
const shortcutsGeneral: ShortcutItem[] = [
|
|
14
|
-
{ keys: "Ctrl+P / Alt+P", description: "Open/close this shortcuts panel" },
|
|
15
|
-
{ keys: "Alt+V (or Ctrl+V)", description: "Paste from clipboard into the focused input" },
|
|
16
|
-
{ keys: "
|
|
17
|
-
{ keys: "
|
|
18
|
-
{ keys: "
|
|
19
|
-
|
|
13
|
+
const shortcutsGeneral: ShortcutItem[] = [
|
|
14
|
+
{ keys: "Ctrl+P / Alt+P", description: "Open/close this shortcuts panel" },
|
|
15
|
+
{ keys: "Alt+V (or Ctrl+V)", description: "Paste from clipboard into the focused input" },
|
|
16
|
+
{ keys: "Ctrl+C", description: "Cancel the current request" },
|
|
17
|
+
{ keys: "Alt+C (or Cmd+C)", description: "Copy the last assistant message" },
|
|
18
|
+
{ keys: "Shift+Tab", description: "Toggle auto-approve for agent changes" },
|
|
19
|
+
{ keys: "Enter", description: "Confirm / submit" },
|
|
20
|
+
{ keys: "↑/↓ (or j/k)", description: "Navigate lists" },
|
|
21
|
+
{ keys: "PageUp/PageDown", description: "Scroll chat faster" },
|
|
22
|
+
];
|
|
20
23
|
|
|
21
24
|
const shortcutsSetup: ShortcutItem[] = [
|
|
22
25
|
{ keys: "Esc", description: "Go back to the previous step" },
|
|
@@ -64,4 +67,4 @@ export function ShortcutsModal({ activeTab }: ShortcutsModalProps) {
|
|
|
64
67
|
</box>
|
|
65
68
|
</box>
|
|
66
69
|
);
|
|
67
|
-
}
|
|
70
|
+
}
|