@kirosnn/mosaic 0.0.91 → 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.
Files changed (56) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -2
  3. package/package.json +52 -47
  4. package/src/agent/prompts/systemPrompt.ts +198 -68
  5. package/src/agent/prompts/toolsPrompt.ts +217 -135
  6. package/src/agent/provider/anthropic.ts +19 -15
  7. package/src/agent/provider/google.ts +21 -17
  8. package/src/agent/provider/ollama.ts +80 -41
  9. package/src/agent/provider/openai.ts +107 -67
  10. package/src/agent/provider/reasoning.ts +29 -0
  11. package/src/agent/provider/xai.ts +19 -15
  12. package/src/agent/tools/definitions.ts +9 -5
  13. package/src/agent/tools/executor.ts +655 -46
  14. package/src/agent/tools/exploreExecutor.ts +12 -12
  15. package/src/agent/tools/fetch.ts +58 -0
  16. package/src/agent/tools/glob.ts +20 -4
  17. package/src/agent/tools/grep.ts +62 -8
  18. package/src/agent/tools/plan.ts +27 -0
  19. package/src/agent/tools/read.ts +2 -0
  20. package/src/agent/types.ts +6 -6
  21. package/src/components/App.tsx +67 -25
  22. package/src/components/CustomInput.tsx +274 -68
  23. package/src/components/Main.tsx +323 -168
  24. package/src/components/ShortcutsModal.tsx +11 -8
  25. package/src/components/main/ChatPage.tsx +217 -58
  26. package/src/components/main/HomePage.tsx +5 -1
  27. package/src/components/main/ThinkingIndicator.tsx +11 -1
  28. package/src/components/main/types.ts +11 -10
  29. package/src/index.tsx +3 -5
  30. package/src/utils/approvalBridge.ts +29 -8
  31. package/src/utils/approvalModeBridge.ts +17 -0
  32. package/src/utils/commands/approvals.ts +48 -0
  33. package/src/utils/commands/image.ts +109 -0
  34. package/src/utils/commands/index.ts +5 -1
  35. package/src/utils/diffRendering.tsx +13 -14
  36. package/src/utils/history.ts +82 -40
  37. package/src/utils/imageBridge.ts +28 -0
  38. package/src/utils/images.ts +31 -0
  39. package/src/utils/models.ts +0 -7
  40. package/src/utils/notificationBridge.ts +23 -0
  41. package/src/utils/toolFormatting.ts +162 -43
  42. package/src/web/app.tsx +94 -34
  43. package/src/web/assets/css/ChatPage.css +102 -30
  44. package/src/web/assets/css/MessageItem.css +26 -29
  45. package/src/web/assets/css/ThinkingIndicator.css +44 -6
  46. package/src/web/assets/css/ToolMessage.css +36 -14
  47. package/src/web/components/ChatPage.tsx +228 -105
  48. package/src/web/components/HomePage.tsx +6 -6
  49. package/src/web/components/MessageItem.tsx +88 -89
  50. package/src/web/components/Setup.tsx +1 -1
  51. package/src/web/components/Sidebar.tsx +1 -1
  52. package/src/web/components/ThinkingIndicator.tsx +40 -21
  53. package/src/web/router.ts +1 -1
  54. package/src/web/server.tsx +187 -39
  55. package/src/web/storage.ts +23 -1
  56. 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, getInputAreaTotalLines, formatElapsedTime } from "./ThinkingIndicator";
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
- const match = content.match(/^(.+?)\s*\((.+)\)$/);
20
- if (match) {
21
- const [, toolName, toolInfo] = match;
22
- return (
23
- <>
24
- <text fg="white">{toolName}</text>
25
- <text fg="white" attributes={TextAttributes.DIM}> ({toolInfo})</text>
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
- let visibleLines: RenderItem[] = [];
344
-
345
- if (allItems.length === 0) {
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
- <box flexGrow={1} flexDirection="column" width="100%" paddingLeft={1} paddingRight={1} paddingTop={1} paddingBottom={bottomReservedLines}>
380
- {visibleLines.map((item) => {
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.role === "tool" ? (
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
- </box>
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={getInputBarBaseLines()}
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={!isProcessing && !shortcutsOpen && !questionRequest && !approvalRequest}
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>—</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={getInputBarBaseLines() + 1} left={0} right={0} flexDirection="column" paddingLeft={1} paddingRight={1}>
512
- <ThinkingIndicatorBlock isProcessing={isProcessing} hasQuestion={Boolean(questionRequest) || Boolean(approvalRequest)} startTime={processingStartTime} tokens={currentTokens} />
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
- return getInputBarBaseLines() + (shouldShowThinkingIndicator(props) ? 2 : 0) + 2;
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
- toolName?: string;
36
- toolArgs?: Record<string, unknown>;
37
- toolResult?: unknown;
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
- parsed.initialMessage = args[i + 1];
58
- i += 2;
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
+ }