@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
|
@@ -1,33 +1,57 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useEffect, useState, useRef } from "react";
|
|
2
2
|
import { TextAttributes } from "@opentui/core";
|
|
3
3
|
import { renderMarkdownSegment, parseAndWrapMarkdown } from "../../utils/markdown";
|
|
4
4
|
import { getToolParagraphIndent, getToolWrapTarget, getToolWrapWidth } from "../../utils/toolFormatting";
|
|
5
5
|
import { subscribeQuestion, answerQuestion, type QuestionRequest } from "../../utils/questionBridge";
|
|
6
6
|
import { subscribeApproval, respondApproval, type ApprovalRequest } from "../../utils/approvalBridge";
|
|
7
|
+
import { subscribeApprovalMode } from "../../utils/approvalModeBridge";
|
|
8
|
+
import { shouldRequireApprovals } from "../../utils/config";
|
|
7
9
|
import { subscribeFileChanges } from "../../utils/fileChangesBridge";
|
|
8
10
|
import type { FileChanges } from "../../utils/fileChangeTracker";
|
|
9
11
|
import { CustomInput } from "../CustomInput";
|
|
10
12
|
import type { Message } from "./types";
|
|
13
|
+
import type { ImageAttachment } from "../../utils/images";
|
|
11
14
|
import { wrapText } from "./wrapText";
|
|
12
15
|
import { QuestionPanel } from "./QuestionPanel";
|
|
13
16
|
import { ApprovalPanel } from "./ApprovalPanel";
|
|
14
|
-
import { ThinkingIndicatorBlock, getBottomReservedLinesForInputBar, getInputBarBaseLines,
|
|
17
|
+
import { ThinkingIndicatorBlock, getBottomReservedLinesForInputBar, getInputBarBaseLines, formatElapsedTime } from "./ThinkingIndicator";
|
|
15
18
|
import { renderInlineDiffLine, getDiffLineBackground } from "../../utils/diffRendering";
|
|
16
19
|
|
|
17
|
-
function renderToolText(content: string, paragraphIndex: number, indent: number) {
|
|
20
|
+
function renderToolText(content: string, paragraphIndex: number, indent: number, wrappedLineIndex: number) {
|
|
18
21
|
if (paragraphIndex === 0) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
if (wrappedLineIndex === 0) {
|
|
23
|
+
const match = content.match(/^(.+?)\s*(\(.*)$/);
|
|
24
|
+
if (match) {
|
|
25
|
+
const [, toolName, toolInfo] = match;
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
<text fg="white">{toolName} </text>
|
|
29
|
+
<text fg="white" attributes={TextAttributes.DIM}>{toolInfo}</text>
|
|
30
|
+
</>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
return <text fg="white" attributes={TextAttributes.DIM}>{` ${content || ' '}`}</text>;
|
|
28
35
|
}
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
const planMatch = content.match(/^(\s*)>\s*(\[[~x ]\])?\s*(.*)$/);
|
|
39
|
+
if (planMatch) {
|
|
40
|
+
const [, leading, bracket, rest] = planMatch;
|
|
41
|
+
const bracketColor = bracket === '[~]' ? '#ffca38' : 'white';
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
<text fg="white">{leading || ''}</text>
|
|
45
|
+
<text fg="#ffca38">{'>'}</text>
|
|
46
|
+
<text fg="white"> </text>
|
|
47
|
+
{bracket ? <text fg={bracketColor}>{bracket}</text> : null}
|
|
48
|
+
{bracket ? <text fg="white"> </text> : null}
|
|
49
|
+
<text fg="white">{rest || ' '}</text>
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
31
55
|
const diffLineRender = renderInlineDiffLine(content);
|
|
32
56
|
if (diffLineRender) {
|
|
33
57
|
return diffLineRender;
|
|
@@ -36,6 +60,46 @@ function renderToolText(content: string, paragraphIndex: number, indent: number)
|
|
|
36
60
|
return <text fg="white">{`${' '.repeat(indent)}${content || ' '}`}</text>;
|
|
37
61
|
}
|
|
38
62
|
|
|
63
|
+
function getPlanProgress(messages: Message[]): { inProgressStep?: string; nextStep?: string } {
|
|
64
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
65
|
+
const message = messages[i];
|
|
66
|
+
if (!message || message.role !== 'tool' || message.toolName !== 'plan') continue;
|
|
67
|
+
const result = message.toolResult;
|
|
68
|
+
if (!result || typeof result !== 'object') continue;
|
|
69
|
+
const obj = result as Record<string, unknown>;
|
|
70
|
+
const planItems = Array.isArray(obj.plan) ? obj.plan : [];
|
|
71
|
+
const normalized = planItems
|
|
72
|
+
.map((item) => {
|
|
73
|
+
if (!item || typeof item !== 'object') return null;
|
|
74
|
+
const entry = item as Record<string, unknown>;
|
|
75
|
+
const step = typeof entry.step === 'string' ? entry.step.trim() : '';
|
|
76
|
+
const status = typeof entry.status === 'string' ? entry.status : 'pending';
|
|
77
|
+
if (!step) return null;
|
|
78
|
+
return { step, status };
|
|
79
|
+
})
|
|
80
|
+
.filter((item): item is { step: string; status: string } => !!item);
|
|
81
|
+
|
|
82
|
+
if (normalized.length === 0) return {};
|
|
83
|
+
|
|
84
|
+
const inProgressIndex = normalized.findIndex(item => item.status === 'in_progress');
|
|
85
|
+
const inProgressStep = inProgressIndex >= 0 ? normalized[inProgressIndex]?.step : undefined;
|
|
86
|
+
let nextStep: string | undefined;
|
|
87
|
+
|
|
88
|
+
if (inProgressIndex >= 0) {
|
|
89
|
+
const after = normalized.slice(inProgressIndex + 1).find(item => item.status === 'pending');
|
|
90
|
+
nextStep = after?.step;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!nextStep) {
|
|
94
|
+
nextStep = normalized.find(item => item.status === 'pending')?.step;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { inProgressStep, nextStep };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
|
|
39
103
|
interface ChatPageProps {
|
|
40
104
|
messages: Message[];
|
|
41
105
|
isProcessing: boolean;
|
|
@@ -47,6 +111,7 @@ interface ChatPageProps {
|
|
|
47
111
|
pasteRequestId: number;
|
|
48
112
|
shortcutsOpen: boolean;
|
|
49
113
|
onSubmit: (value: string, meta?: import("../CustomInput").InputSubmitMeta) => void;
|
|
114
|
+
pendingImages: ImageAttachment[];
|
|
50
115
|
}
|
|
51
116
|
|
|
52
117
|
export function ChatPage({
|
|
@@ -60,12 +125,15 @@ export function ChatPage({
|
|
|
60
125
|
pasteRequestId,
|
|
61
126
|
shortcutsOpen,
|
|
62
127
|
onSubmit,
|
|
128
|
+
pendingImages,
|
|
63
129
|
}: ChatPageProps) {
|
|
64
130
|
const maxWidth = Math.max(20, terminalWidth - 6);
|
|
65
131
|
const [questionRequest, setQuestionRequest] = useState<QuestionRequest | null>(null);
|
|
66
132
|
const [approvalRequest, setApprovalRequest] = useState<ApprovalRequest | null>(null);
|
|
67
133
|
const [fileChanges, setFileChanges] = useState<FileChanges>({ linesAdded: 0, linesRemoved: 0, filesModified: 0 });
|
|
68
134
|
const [, setTimerTick] = useState(0);
|
|
135
|
+
const [requireApprovals, setRequireApprovals] = useState(shouldRequireApprovals());
|
|
136
|
+
const scrollboxRef = useRef<any>(null);
|
|
69
137
|
|
|
70
138
|
useEffect(() => {
|
|
71
139
|
return subscribeQuestion(setQuestionRequest);
|
|
@@ -75,6 +143,12 @@ export function ChatPage({
|
|
|
75
143
|
return subscribeApproval(setApprovalRequest);
|
|
76
144
|
}, []);
|
|
77
145
|
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
return subscribeApprovalMode((require) => {
|
|
148
|
+
setRequireApprovals(require);
|
|
149
|
+
});
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
78
152
|
useEffect(() => {
|
|
79
153
|
return subscribeFileChanges(setFileChanges);
|
|
80
154
|
}, []);
|
|
@@ -88,11 +162,22 @@ export function ChatPage({
|
|
|
88
162
|
}, 1000);
|
|
89
163
|
return () => clearInterval(interval);
|
|
90
164
|
}, [messages]);
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
const sb = scrollboxRef.current;
|
|
167
|
+
if (sb?.verticalScrollBar) {
|
|
168
|
+
sb.verticalScrollBar.visible = false;
|
|
169
|
+
}
|
|
170
|
+
}, []);
|
|
91
171
|
|
|
172
|
+
const planProgress = getPlanProgress(messages);
|
|
173
|
+
const extraInputLines = pendingImages.length > 0 ? 1 : 0;
|
|
174
|
+
const inputBarBaseLines = getInputBarBaseLines() + extraInputLines;
|
|
92
175
|
const bottomReservedLines = getBottomReservedLinesForInputBar({
|
|
93
176
|
isProcessing,
|
|
94
177
|
hasQuestion: Boolean(questionRequest) || Boolean(approvalRequest),
|
|
95
|
-
|
|
178
|
+
inProgressStep: planProgress.inProgressStep,
|
|
179
|
+
nextStep: planProgress.nextStep,
|
|
180
|
+
}) + extraInputLines;
|
|
96
181
|
const viewportHeight = Math.max(5, terminalHeight - (bottomReservedLines + 2));
|
|
97
182
|
|
|
98
183
|
interface RenderItem {
|
|
@@ -100,9 +185,11 @@ export function ChatPage({
|
|
|
100
185
|
type: 'line' | 'question' | 'approval' | 'blend';
|
|
101
186
|
content?: string;
|
|
102
187
|
role: "user" | "assistant" | "tool" | "slash";
|
|
188
|
+
toolName?: string;
|
|
103
189
|
isFirst: boolean;
|
|
104
190
|
indent?: number;
|
|
105
191
|
paragraphIndex?: number;
|
|
192
|
+
wrappedLineIndex?: number;
|
|
106
193
|
segments?: import("../../utils/markdown").MarkdownSegment[];
|
|
107
194
|
success?: boolean;
|
|
108
195
|
isError?: boolean;
|
|
@@ -114,6 +201,7 @@ export function ChatPage({
|
|
|
114
201
|
blendWord?: string;
|
|
115
202
|
isRunning?: boolean;
|
|
116
203
|
runningStartTime?: number;
|
|
204
|
+
isThinking?: boolean;
|
|
117
205
|
}
|
|
118
206
|
|
|
119
207
|
const allItems: RenderItem[] = [];
|
|
@@ -138,6 +226,49 @@ export function ChatPage({
|
|
|
138
226
|
}
|
|
139
227
|
|
|
140
228
|
if (messageRole === 'assistant') {
|
|
229
|
+
if (message.thinkingContent) {
|
|
230
|
+
const headerLines = wrapText('Thinking:', maxWidth);
|
|
231
|
+
for (let i = 0; i < headerLines.length; i++) {
|
|
232
|
+
allItems.push({
|
|
233
|
+
key: `${messageKey}-thinking-header-${i}`,
|
|
234
|
+
type: 'line',
|
|
235
|
+
content: headerLines[i] || '',
|
|
236
|
+
role: messageRole,
|
|
237
|
+
isFirst: false,
|
|
238
|
+
visualLines: 1,
|
|
239
|
+
isThinking: true
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const thinkingLines = message.thinkingContent.split('\n');
|
|
244
|
+
for (let i = 0; i < thinkingLines.length; i++) {
|
|
245
|
+
const wrapped = wrapText(thinkingLines[i] || '', Math.max(10, maxWidth - 2));
|
|
246
|
+
for (let j = 0; j < wrapped.length; j++) {
|
|
247
|
+
allItems.push({
|
|
248
|
+
key: `${messageKey}-thinking-${i}-${j}`,
|
|
249
|
+
type: 'line',
|
|
250
|
+
content: wrapped[j] || '',
|
|
251
|
+
role: messageRole,
|
|
252
|
+
isFirst: false,
|
|
253
|
+
indent: 2,
|
|
254
|
+
visualLines: 1,
|
|
255
|
+
isThinking: true
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
allItems.push({
|
|
261
|
+
key: `${messageKey}-thinking-spacer`,
|
|
262
|
+
type: 'line',
|
|
263
|
+
content: '',
|
|
264
|
+
role: messageRole,
|
|
265
|
+
isFirst: false,
|
|
266
|
+
isSpacer: true,
|
|
267
|
+
visualLines: 1,
|
|
268
|
+
isThinking: true
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
141
272
|
const blocks = parseAndWrapMarkdown(message.content, maxWidth);
|
|
142
273
|
let isFirstContent = true;
|
|
143
274
|
|
|
@@ -153,6 +284,7 @@ export function ChatPage({
|
|
|
153
284
|
type: 'line',
|
|
154
285
|
content: wrapped.text || '',
|
|
155
286
|
role: messageRole,
|
|
287
|
+
toolName: message.toolName,
|
|
156
288
|
isFirst: isFirstContent && j === 0,
|
|
157
289
|
segments: wrapped.segments,
|
|
158
290
|
isError: message.isError,
|
|
@@ -165,6 +297,26 @@ export function ChatPage({
|
|
|
165
297
|
}
|
|
166
298
|
}
|
|
167
299
|
} else {
|
|
300
|
+
if (messageRole === "user" && message.images && message.images.length > 0) {
|
|
301
|
+
for (let i = 0; i < message.images.length; i++) {
|
|
302
|
+
const image = message.images[i]!;
|
|
303
|
+
allItems.push({
|
|
304
|
+
key: `${messageKey}-image-${i}`,
|
|
305
|
+
type: "line",
|
|
306
|
+
content: `[image] ${image.name}`,
|
|
307
|
+
role: messageRole,
|
|
308
|
+
toolName: message.toolName,
|
|
309
|
+
isFirst: i === 0,
|
|
310
|
+
indent: 0,
|
|
311
|
+
paragraphIndex: 0,
|
|
312
|
+
wrappedLineIndex: 0,
|
|
313
|
+
success: (messageRole === "tool" || messageRole === "slash") ? message.success : undefined,
|
|
314
|
+
isSpacer: false,
|
|
315
|
+
visualLines: 1
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
168
320
|
const messageText = message.displayContent ?? message.content;
|
|
169
321
|
const paragraphs = messageText.split('\n');
|
|
170
322
|
let isFirstContent = true;
|
|
@@ -177,9 +329,11 @@ export function ChatPage({
|
|
|
177
329
|
type: 'line',
|
|
178
330
|
content: '',
|
|
179
331
|
role: messageRole,
|
|
332
|
+
toolName: message.toolName,
|
|
180
333
|
isFirst: false,
|
|
181
334
|
indent: messageRole === 'tool' ? getToolParagraphIndent(i) : 0,
|
|
182
335
|
paragraphIndex: i,
|
|
336
|
+
wrappedLineIndex: 0,
|
|
183
337
|
success: (messageRole === 'tool' || messageRole === 'slash') ? message.success : undefined,
|
|
184
338
|
isSpacer: messageRole !== 'tool' && messageRole !== 'slash',
|
|
185
339
|
visualLines: 1,
|
|
@@ -197,9 +351,11 @@ export function ChatPage({
|
|
|
197
351
|
type: 'line',
|
|
198
352
|
content: wrappedLines[j] || '',
|
|
199
353
|
role: messageRole,
|
|
354
|
+
toolName: message.toolName,
|
|
200
355
|
isFirst: isFirstContent && i === 0 && j === 0,
|
|
201
356
|
indent,
|
|
202
357
|
paragraphIndex: i,
|
|
358
|
+
wrappedLineIndex: j,
|
|
203
359
|
success: (messageRole === 'tool' || messageRole === 'slash') ? message.success : undefined,
|
|
204
360
|
isSpacer: false,
|
|
205
361
|
visualLines: 1,
|
|
@@ -218,6 +374,7 @@ export function ChatPage({
|
|
|
218
374
|
type: 'line',
|
|
219
375
|
content: '',
|
|
220
376
|
role: messageRole,
|
|
377
|
+
toolName: message.toolName,
|
|
221
378
|
isFirst: false,
|
|
222
379
|
indent: 2,
|
|
223
380
|
paragraphIndex: 1,
|
|
@@ -242,6 +399,7 @@ export function ChatPage({
|
|
|
242
399
|
type: 'line',
|
|
243
400
|
content: '',
|
|
244
401
|
role: messageRole,
|
|
402
|
+
toolName: message.toolName,
|
|
245
403
|
isFirst: false,
|
|
246
404
|
isSpacer: true,
|
|
247
405
|
visualLines: 1
|
|
@@ -339,45 +497,29 @@ export function ChatPage({
|
|
|
339
497
|
const totalVisualLines = allItems.reduce((sum, item) => sum + item.visualLines, 0);
|
|
340
498
|
const maxScrollOffset = Math.max(0, totalVisualLines - viewportHeight);
|
|
341
499
|
const clampedScrollOffset = Math.max(0, Math.min(scrollOffset, maxScrollOffset));
|
|
500
|
+
const scrollYPosition = Math.max(0, totalVisualLines - viewportHeight - clampedScrollOffset);
|
|
342
501
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
visibleLines = [];
|
|
347
|
-
} else {
|
|
348
|
-
const targetEndLine = totalVisualLines - clampedScrollOffset;
|
|
349
|
-
const targetStartLine = Math.max(0, targetEndLine - viewportHeight);
|
|
350
|
-
|
|
351
|
-
let currentLine = 0;
|
|
352
|
-
let startIdx = -1;
|
|
353
|
-
let endIdx = -1;
|
|
354
|
-
|
|
355
|
-
for (let i = 0; i < allItems.length; i++) {
|
|
356
|
-
const item = allItems[i]!;
|
|
357
|
-
const itemEndLine = currentLine + item.visualLines;
|
|
358
|
-
|
|
359
|
-
if (startIdx === -1 && itemEndLine > targetStartLine) {
|
|
360
|
-
startIdx = i;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (endIdx === -1 && itemEndLine >= targetEndLine) {
|
|
364
|
-
endIdx = i + 1;
|
|
365
|
-
break;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
currentLine = itemEndLine;
|
|
502
|
+
useEffect(() => {
|
|
503
|
+
if (scrollboxRef.current && typeof scrollboxRef.current.scrollTop === 'number') {
|
|
504
|
+
scrollboxRef.current.scrollTop = scrollYPosition;
|
|
369
505
|
}
|
|
370
|
-
|
|
371
|
-
if (startIdx === -1) startIdx = 0;
|
|
372
|
-
if (endIdx === -1) endIdx = allItems.length;
|
|
373
|
-
|
|
374
|
-
visibleLines = allItems.slice(startIdx, endIdx);
|
|
375
|
-
}
|
|
506
|
+
}, [scrollYPosition]);
|
|
376
507
|
|
|
377
508
|
return (
|
|
378
509
|
<box flexDirection="column" width="100%" height="100%" position="relative">
|
|
379
|
-
<
|
|
380
|
-
{
|
|
510
|
+
<scrollbox
|
|
511
|
+
ref={scrollboxRef}
|
|
512
|
+
scrollY
|
|
513
|
+
stickyScroll={scrollOffset === 0}
|
|
514
|
+
stickyStart="bottom"
|
|
515
|
+
viewportCulling
|
|
516
|
+
width="100%"
|
|
517
|
+
height={viewportHeight}
|
|
518
|
+
paddingLeft={1}
|
|
519
|
+
paddingRight={1}
|
|
520
|
+
paddingTop={1}
|
|
521
|
+
>
|
|
522
|
+
{allItems.map((item) => {
|
|
381
523
|
if (item.type === 'question') {
|
|
382
524
|
const req = item.questionRequest;
|
|
383
525
|
if (!req) return null;
|
|
@@ -410,7 +552,7 @@ export function ChatPage({
|
|
|
410
552
|
if (item.blendDuration && item.blendDuration > 60000) {
|
|
411
553
|
const timeStr = formatElapsedTime(item.blendDuration, false);
|
|
412
554
|
return (
|
|
413
|
-
<box key={item.key} flexDirection="row" width="100%" paddingLeft={1}>
|
|
555
|
+
<box key={item.key} flexDirection="row" width="100%" paddingLeft={1} marginBottom={1}>
|
|
414
556
|
<text attributes={TextAttributes.DIM}>⁘ {item.blendWord} for {timeStr}</text>
|
|
415
557
|
</box>
|
|
416
558
|
);
|
|
@@ -419,7 +561,7 @@ export function ChatPage({
|
|
|
419
561
|
}
|
|
420
562
|
|
|
421
563
|
const showErrorBar = item.role === "assistant" && item.isError && item.isFirst && item.content;
|
|
422
|
-
const showToolBar = item.role === "tool" && item.isSpacer === false;
|
|
564
|
+
const showToolBar = item.role === "tool" && item.isSpacer === false && item.toolName !== "plan";
|
|
423
565
|
const showSlashBar = item.role === "slash" && item.isSpacer === false;
|
|
424
566
|
const showToolBackground = item.role === "tool" && item.isSpacer === false;
|
|
425
567
|
const showSlashBackground = item.role === "slash" && item.isSpacer === false;
|
|
@@ -452,11 +594,13 @@ export function ChatPage({
|
|
|
452
594
|
{showErrorBar && (
|
|
453
595
|
<text fg="#ff3838">▎ </text>
|
|
454
596
|
)}
|
|
455
|
-
{item.
|
|
597
|
+
{item.isThinking ? (
|
|
598
|
+
<text fg="#9a9a9a" attributes={TextAttributes.DIM}>{`${' '.repeat(item.indent || 0)}${item.content || ' '}`}</text>
|
|
599
|
+
) : item.role === "tool" ? (
|
|
456
600
|
isRunningTool && item.runningStartTime && item.paragraphIndex === 1 ? (
|
|
457
601
|
<text fg="#ffffff" attributes={TextAttributes.DIM}> Running... {Math.floor((Date.now() - item.runningStartTime) / 1000)}s</text>
|
|
458
602
|
) : (
|
|
459
|
-
renderToolText(item.content || ' ', item.paragraphIndex || 0, item.indent || 0)
|
|
603
|
+
renderToolText(item.content || ' ', item.paragraphIndex || 0, item.indent || 0, item.wrappedLineIndex || 0)
|
|
460
604
|
)
|
|
461
605
|
) : item.role === "user" || item.role === "slash" ? (
|
|
462
606
|
<text fg="white">{`${' '.repeat(item.indent || 0)}${item.content || ' '}`}</text>
|
|
@@ -470,7 +614,7 @@ export function ChatPage({
|
|
|
470
614
|
</box>
|
|
471
615
|
);
|
|
472
616
|
})}
|
|
473
|
-
</
|
|
617
|
+
</scrollbox>
|
|
474
618
|
|
|
475
619
|
<box
|
|
476
620
|
position="absolute"
|
|
@@ -484,16 +628,23 @@ export function ChatPage({
|
|
|
484
628
|
paddingTop={0}
|
|
485
629
|
paddingBottom={0}
|
|
486
630
|
flexShrink={0}
|
|
487
|
-
minHeight={
|
|
631
|
+
minHeight={inputBarBaseLines}
|
|
488
632
|
minWidth="100%"
|
|
489
633
|
>
|
|
634
|
+
{pendingImages.length > 0 && (
|
|
635
|
+
<box flexDirection="row" width="100%" marginBottom={1}>
|
|
636
|
+
<text fg="#ffca38">Images: </text>
|
|
637
|
+
<text fg="gray">{pendingImages.map((img) => img.name).join(", ")}</text>
|
|
638
|
+
</box>
|
|
639
|
+
)}
|
|
490
640
|
<box flexDirection="row" alignItems="center" width="100%" flexGrow={1} minWidth={0}>
|
|
491
641
|
<box flexGrow={1} flexShrink={1} minWidth={0}>
|
|
492
642
|
<CustomInput
|
|
493
643
|
onSubmit={onSubmit}
|
|
494
644
|
placeholder="Type your message..."
|
|
495
|
-
focused={!
|
|
645
|
+
focused={!shortcutsOpen && !questionRequest && !approvalRequest}
|
|
496
646
|
pasteRequestId={shortcutsOpen ? 0 : pasteRequestId}
|
|
647
|
+
submitDisabled={isProcessing || shortcutsOpen || Boolean(questionRequest) || Boolean(approvalRequest)}
|
|
497
648
|
/>
|
|
498
649
|
</box>
|
|
499
650
|
</box>
|
|
@@ -501,16 +652,24 @@ export function ChatPage({
|
|
|
501
652
|
|
|
502
653
|
<box position="absolute" bottom={0} left={0} right={0} flexDirection="row" paddingLeft={1} paddingRight={1} justifyContent="space-between">
|
|
503
654
|
<box flexDirection="row" gap={1}>
|
|
504
|
-
<text
|
|
655
|
+
<text fg="#ffca38">{requireApprovals ? '' : '⏵⏵ auto-accept edits on'}</text>
|
|
656
|
+
<text attributes={TextAttributes.DIM}>{requireApprovals ? '' : ' — '}</text>
|
|
505
657
|
<text fg="#4d8f29">+{fileChanges.linesAdded}</text>
|
|
506
658
|
<text fg="#d73a49">-{fileChanges.linesRemoved}</text>
|
|
507
659
|
</box>
|
|
508
660
|
<text attributes={TextAttributes.DIM}>ctrl+o to see commands — ctrl+p to view shortcuts</text>
|
|
509
661
|
</box>
|
|
510
662
|
|
|
511
|
-
<box position="absolute" bottom={
|
|
512
|
-
<ThinkingIndicatorBlock
|
|
663
|
+
<box position="absolute" bottom={inputBarBaseLines + 1} left={0} right={0} flexDirection="column" paddingLeft={1} paddingRight={1}>
|
|
664
|
+
<ThinkingIndicatorBlock
|
|
665
|
+
isProcessing={isProcessing}
|
|
666
|
+
hasQuestion={Boolean(questionRequest) || Boolean(approvalRequest)}
|
|
667
|
+
startTime={processingStartTime}
|
|
668
|
+
tokens={currentTokens}
|
|
669
|
+
inProgressStep={planProgress.inProgressStep}
|
|
670
|
+
nextStep={planProgress.nextStep}
|
|
671
|
+
/>
|
|
513
672
|
</box>
|
|
514
673
|
</box>
|
|
515
674
|
);
|
|
516
|
-
}
|
|
675
|
+
}
|
|
@@ -17,7 +17,11 @@ const TIPS = [
|
|
|
17
17
|
"Use /clear to reset the current chat session.",
|
|
18
18
|
"Use Tab to autocomplete commands and arguments.",
|
|
19
19
|
"Use Ctrl + K to clear the current input line.",
|
|
20
|
+
"Use Ctrl + G to edit the current input in your system editor.",
|
|
20
21
|
"Use /help to display the list of available commands.",
|
|
22
|
+
"Select text with the mouse to copy it automatically.",
|
|
23
|
+
"Attach an image with /image <path>.",
|
|
24
|
+
"Paste an image with Ctrl/Alt + V (or Cmd + V on macOS).",
|
|
21
25
|
];
|
|
22
26
|
|
|
23
27
|
export function HomePage({ onSubmit, pasteRequestId, shortcutsOpen }: HomePageProps) {
|
|
@@ -108,4 +112,4 @@ export function HomePage({ onSubmit, pasteRequestId, shortcutsOpen }: HomePagePr
|
|
|
108
112
|
</box>
|
|
109
113
|
</box>
|
|
110
114
|
);
|
|
111
|
-
}
|
|
115
|
+
}
|
|
@@ -7,6 +7,7 @@ interface ThinkingIndicatorProps {
|
|
|
7
7
|
hasQuestion: boolean;
|
|
8
8
|
startTime?: number | null;
|
|
9
9
|
tokens?: number;
|
|
10
|
+
nextStep?: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function getInputBarBaseLines(): number {
|
|
@@ -22,7 +23,8 @@ export function shouldShowThinkingIndicator({ isProcessing, hasQuestion }: Think
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export function getBottomReservedLinesForInputBar(props: ThinkingIndicatorProps): number {
|
|
25
|
-
|
|
26
|
+
const indicatorLines = shouldShowThinkingIndicator(props) ? (props.nextStep ? 3 : 2) : 0;
|
|
27
|
+
return getInputBarBaseLines() + indicatorLines + 2;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export function formatElapsedTime(ms: number | null | undefined, fromStartTime: boolean = true): string {
|
|
@@ -93,6 +95,14 @@ export function ThinkingIndicatorBlock(props: ThinkingIndicatorProps) {
|
|
|
93
95
|
return (
|
|
94
96
|
<box flexDirection="column" width="100%">
|
|
95
97
|
<ThinkingIndicator {...props} />
|
|
98
|
+
{props.nextStep ? (
|
|
99
|
+
<box flexDirection="row" width="100%" paddingLeft={2}>
|
|
100
|
+
<text fg="#ffca38" attributes={TextAttributes.BOLD}>⎿ </text>
|
|
101
|
+
<text fg="#ffca38" attributes={TextAttributes.BOLD}>Next:</text>
|
|
102
|
+
<text> </text>
|
|
103
|
+
<text fg="white" attributes={TextAttributes.DIM}>{props.nextStep}</text>
|
|
104
|
+
</box>
|
|
105
|
+
) : null}
|
|
96
106
|
<box flexDirection="row" width="100%">
|
|
97
107
|
<text> </text>
|
|
98
108
|
</box>
|
|
@@ -26,15 +26,16 @@ export const THINKING_WORDS = [
|
|
|
26
26
|
"Revolutionizing"
|
|
27
27
|
];
|
|
28
28
|
|
|
29
|
-
export interface Message {
|
|
30
|
-
id: string;
|
|
31
|
-
role: "user" | "assistant" | "tool" | "slash";
|
|
32
|
-
displayRole?: "user" | "assistant" | "tool" | "slash";
|
|
33
|
-
displayContent?: string;
|
|
34
|
-
content: string;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
export interface Message {
|
|
30
|
+
id: string;
|
|
31
|
+
role: "user" | "assistant" | "tool" | "slash";
|
|
32
|
+
displayRole?: "user" | "assistant" | "tool" | "slash";
|
|
33
|
+
displayContent?: string;
|
|
34
|
+
content: string;
|
|
35
|
+
images?: import("../../utils/images").ImageAttachment[];
|
|
36
|
+
toolName?: string;
|
|
37
|
+
toolArgs?: Record<string, unknown>;
|
|
38
|
+
toolResult?: unknown;
|
|
38
39
|
success?: boolean;
|
|
39
40
|
isError?: boolean;
|
|
40
41
|
responseDuration?: number;
|
|
@@ -52,4 +53,4 @@ export interface MainProps {
|
|
|
52
53
|
shortcutsOpen?: boolean;
|
|
53
54
|
commandsOpen?: boolean;
|
|
54
55
|
initialMessage?: string;
|
|
55
|
-
}
|
|
56
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -54,8 +54,9 @@ class CLI {
|
|
|
54
54
|
parsed.directory = args[i + 1];
|
|
55
55
|
i += 2;
|
|
56
56
|
} else if (arg === 'run') {
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
const message = args.slice(i + 1).join(' ');
|
|
58
|
+
if (message) parsed.initialMessage = message;
|
|
59
|
+
i = args.length;
|
|
59
60
|
} else if (arg === 'uninstall') {
|
|
60
61
|
parsed.uninstall = true;
|
|
61
62
|
if (args[i + 1] === '--force') {
|
|
@@ -82,9 +83,6 @@ class CLI {
|
|
|
82
83
|
const gold = (text: string) => `\x1b[38;2;255;202;56m${text}\x1b[0m`;
|
|
83
84
|
|
|
84
85
|
console.log('');
|
|
85
|
-
console.log(gold('███╗ ███╗'));
|
|
86
|
-
console.log(gold('████╗ ████║'));
|
|
87
|
-
console.log(gold('███╔████╔███║'));
|
|
88
86
|
console.log(`
|
|
89
87
|
Mosaic - AI-powered coding agent
|
|
90
88
|
|
|
@@ -28,6 +28,11 @@ let listeners = new Set<ApprovalListener>();
|
|
|
28
28
|
let acceptedListeners = new Set<ApprovalAcceptedListener>();
|
|
29
29
|
let pendingResolve: ((response: ApprovalResponse) => void) | null = null;
|
|
30
30
|
let pendingReject: ((reason?: any) => void) | null = null;
|
|
31
|
+
let queuedRequests: {
|
|
32
|
+
request: ApprovalRequest;
|
|
33
|
+
resolve: (response: ApprovalResponse) => void;
|
|
34
|
+
reject: (reason?: any) => void;
|
|
35
|
+
}[] = [];
|
|
31
36
|
|
|
32
37
|
function notify(): void {
|
|
33
38
|
for (const listener of listeners) {
|
|
@@ -69,10 +74,6 @@ export async function requestApproval(
|
|
|
69
74
|
args: Record<string, unknown>,
|
|
70
75
|
preview: { title: string; content: string; details?: string[] }
|
|
71
76
|
): Promise<{ approved: boolean; customResponse?: string }> {
|
|
72
|
-
if (pendingResolve) {
|
|
73
|
-
throw new Error('An approval request is already pending');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
77
|
const request: ApprovalRequest = {
|
|
77
78
|
id: createId(),
|
|
78
79
|
toolName,
|
|
@@ -80,12 +81,16 @@ export async function requestApproval(
|
|
|
80
81
|
args,
|
|
81
82
|
};
|
|
82
83
|
|
|
83
|
-
currentRequest = request;
|
|
84
|
-
notify();
|
|
85
|
-
|
|
86
84
|
const response = await new Promise<ApprovalResponse>((resolve, reject) => {
|
|
85
|
+
if (pendingResolve) {
|
|
86
|
+
queuedRequests.push({ request, resolve, reject });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
currentRequest = request;
|
|
87
91
|
pendingResolve = resolve;
|
|
88
92
|
pendingReject = reject;
|
|
93
|
+
notify();
|
|
89
94
|
});
|
|
90
95
|
|
|
91
96
|
return { approved: response.approved, customResponse: response.customResponse };
|
|
@@ -114,6 +119,14 @@ export function respondApproval(approved: boolean, customResponse?: string): voi
|
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
resolve(response);
|
|
122
|
+
|
|
123
|
+
const next = queuedRequests.shift();
|
|
124
|
+
if (next) {
|
|
125
|
+
currentRequest = next.request;
|
|
126
|
+
pendingResolve = next.resolve;
|
|
127
|
+
pendingReject = next.reject;
|
|
128
|
+
notify();
|
|
129
|
+
}
|
|
117
130
|
}
|
|
118
131
|
|
|
119
132
|
export function cancelApproval(): void {
|
|
@@ -126,4 +139,12 @@ export function cancelApproval(): void {
|
|
|
126
139
|
notify();
|
|
127
140
|
|
|
128
141
|
reject(new Error('Interrupted by user'));
|
|
129
|
-
|
|
142
|
+
|
|
143
|
+
const next = queuedRequests.shift();
|
|
144
|
+
if (next) {
|
|
145
|
+
currentRequest = next.request;
|
|
146
|
+
pendingResolve = next.resolve;
|
|
147
|
+
pendingReject = next.reject;
|
|
148
|
+
notify();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { shouldRequireApprovals } from './config'
|
|
2
|
+
|
|
3
|
+
type ApprovalModeListener = (requireApprovals: boolean) => void
|
|
4
|
+
|
|
5
|
+
const listeners = new Set<ApprovalModeListener>()
|
|
6
|
+
|
|
7
|
+
export function subscribeApprovalMode(listener: ApprovalModeListener): () => void {
|
|
8
|
+
listeners.add(listener)
|
|
9
|
+
listener(shouldRequireApprovals())
|
|
10
|
+
return () => {
|
|
11
|
+
listeners.delete(listener)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function emitApprovalMode(requireApprovals: boolean): void {
|
|
16
|
+
listeners.forEach((listener) => listener(requireApprovals))
|
|
17
|
+
}
|