@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
@@ -0,0 +1,48 @@
1
+ import type { Command } from './types'
2
+ import { getCurrentApproval, respondApproval } from '../approvalBridge'
3
+ import { shouldRequireApprovals, setRequireApprovals } from '../config'
4
+ import { notifyNotification } from '../notificationBridge'
5
+ import { emitApprovalMode } from '../approvalModeBridge'
6
+
7
+ export const approvalsCommand: Command = {
8
+ name: 'approvals',
9
+ description: 'Toggle approval prompts for agent changes',
10
+ usage: '/approvals on|off|toggle|status',
11
+ aliases: ['approval', 'autoapprove', 'auto-approve'],
12
+ execute: (args: string[]) => {
13
+ const raw = args[0]?.toLowerCase()
14
+ const current = shouldRequireApprovals()
15
+ let next = current
16
+
17
+ if (!raw || raw === 'toggle') {
18
+ next = !current
19
+ } else if (raw === 'on' || raw === 'true' || raw === 'yes') {
20
+ next = true
21
+ } else if (raw === 'off' || raw === 'false' || raw === 'no') {
22
+ next = false
23
+ } else if (raw === 'status') {
24
+ return {
25
+ success: true,
26
+ content: current ? 'Approvals are enabled.' : 'Auto-approve is enabled.'
27
+ }
28
+ } else {
29
+ return {
30
+ success: false,
31
+ content: 'Usage: /approvals on|off|toggle|status'
32
+ }
33
+ }
34
+
35
+ setRequireApprovals(next)
36
+ if (!next && getCurrentApproval()) {
37
+ respondApproval(true)
38
+ }
39
+ emitApprovalMode(next)
40
+
41
+ notifyNotification(next ? 'Approvals enabled.' : 'Auto-approve enabled.', 'info', 2500)
42
+
43
+ return {
44
+ success: true,
45
+ content: next ? 'Approvals enabled.' : 'Auto-approve enabled.'
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,109 @@
1
+ import { existsSync, readFileSync, statSync } from "fs";
2
+ import { basename } from "path";
3
+ import type { Command } from "./types";
4
+ import { guessImageMimeType } from "../images";
5
+ import { emitImageCommand, canUseImages } from "../imageBridge";
6
+
7
+ const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
8
+
9
+ function parseImagePath(fullCommand: string): string {
10
+ const trimmed = fullCommand.trim();
11
+ const without = trimmed.replace(/^\/(image|img)\s+/i, "");
12
+ return without.trim();
13
+ }
14
+
15
+ export const imageCommand: Command = {
16
+ name: "image",
17
+ description: "Attach an image for the next message",
18
+ usage: "/image <path> | /image clear",
19
+ aliases: ["img"],
20
+ execute: (args, fullCommand) => {
21
+ const first = args[0]?.toLowerCase();
22
+ if (!first) {
23
+ return {
24
+ success: false,
25
+ content: "Usage: /image <path> | /image clear",
26
+ shouldAddToHistory: false
27
+ };
28
+ }
29
+
30
+ if (first === "clear") {
31
+ emitImageCommand({ type: "clear" });
32
+ return {
33
+ success: true,
34
+ content: "Image list cleared.",
35
+ shouldAddToHistory: false
36
+ };
37
+ }
38
+
39
+ if (!canUseImages()) {
40
+ return {
41
+ success: false,
42
+ content: "Images are not supported by the current model.",
43
+ shouldAddToHistory: false
44
+ };
45
+ }
46
+
47
+ const path = parseImagePath(fullCommand);
48
+ if (!path) {
49
+ return {
50
+ success: false,
51
+ content: "Missing image path.",
52
+ shouldAddToHistory: false
53
+ };
54
+ }
55
+
56
+ if (!existsSync(path)) {
57
+ return {
58
+ success: false,
59
+ content: "File not found.",
60
+ shouldAddToHistory: false
61
+ };
62
+ }
63
+
64
+ const stat = statSync(path);
65
+ if (!stat.isFile()) {
66
+ return {
67
+ success: false,
68
+ content: "Not a file.",
69
+ shouldAddToHistory: false
70
+ };
71
+ }
72
+
73
+ if (stat.size > MAX_IMAGE_BYTES) {
74
+ return {
75
+ success: false,
76
+ content: "Image too large (max 10 MB).",
77
+ shouldAddToHistory: false
78
+ };
79
+ }
80
+
81
+ const name = basename(path);
82
+ const mimeType = guessImageMimeType(name);
83
+ if (!mimeType.startsWith("image/")) {
84
+ return {
85
+ success: false,
86
+ content: "Unsupported image type.",
87
+ shouldAddToHistory: false
88
+ };
89
+ }
90
+
91
+ const data = readFileSync(path).toString("base64");
92
+ emitImageCommand({
93
+ type: "add",
94
+ image: {
95
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
96
+ name,
97
+ mimeType,
98
+ data,
99
+ size: stat.size
100
+ }
101
+ });
102
+
103
+ return {
104
+ success: true,
105
+ content: `Image attached: ${name}`,
106
+ shouldAddToHistory: false
107
+ };
108
+ }
109
+ };
@@ -7,6 +7,8 @@ import { undoCommand } from './undo';
7
7
  import { redoCommand } from './redo';
8
8
  import { sessionsCommand } from './sessions';
9
9
  import { webCommand } from './web';
10
+ import { imageCommand } from './image';
11
+ import { approvalsCommand } from './approvals';
10
12
 
11
13
  export { commandRegistry } from './registry';
12
14
  export type { Command, CommandResult, CommandRegistry } from './types';
@@ -65,4 +67,6 @@ export function initializeCommands(): void {
65
67
  commandRegistry.register(redoCommand);
66
68
  commandRegistry.register(sessionsCommand);
67
69
  commandRegistry.register(webCommand);
68
- }
70
+ commandRegistry.register(imageCommand);
71
+ commandRegistry.register(approvalsCommand);
72
+ }
@@ -8,13 +8,13 @@ export function renderDiffLine(line: string, key: string) {
8
8
  const colors = getDiffLineColors(parsed);
9
9
 
10
10
  return (
11
- <box key={key} flexDirection="row">
12
- <box backgroundColor={colors.labelBackground}>
11
+ <box key={key} flexDirection="row" width="100%" alignItems="stretch">
12
+ <box backgroundColor={colors.labelBackground} flexShrink={0}>
13
13
  <text fg="#ffffff">
14
14
  {" "}{parsed.prefix}{parsed.lineNumber?.padStart(5) || ''}{' '}
15
15
  </text>
16
16
  </box>
17
- <box flexGrow={1} backgroundColor={colors.contentBackground}>
17
+ <box flexGrow={1} backgroundColor={colors.contentBackground} minWidth={0}>
18
18
  <text fg="#ffffff">
19
19
  {" "}{parsed.content || ''}
20
20
  </text>
@@ -24,27 +24,28 @@ export function renderDiffLine(line: string, key: string) {
24
24
  }
25
25
 
26
26
  return (
27
- <text key={key} fg="#ffffff">
28
- {line || ' '}
29
- </text>
27
+ <box key={key} width="100%">
28
+ <text fg="#ffffff">
29
+ {line || ' '}
30
+ </text>
31
+ </box>
30
32
  );
31
33
  }
32
34
 
33
35
  export function renderInlineDiffLine(content: string) {
34
36
  const parsed = parseDiffLine(content);
37
+ const colors = getDiffLineColors(parsed);
35
38
 
36
39
  if (parsed.isDiffLine) {
37
- const colors = getDiffLineColors(parsed);
38
-
39
40
  return (
40
41
  <>
41
42
  <box>
42
- <text fg="#ffffff">
43
+ <text fg="white" attributes={TextAttributes.DIM}>
43
44
  {parsed.prefix}{parsed.lineNumber?.padStart(5) || ''}{' '}
44
45
  </text>
45
46
  </box>
46
- <box backgroundColor={colors.contentBackground}>
47
- <text fg="#ffffff">
47
+ <box>
48
+ <text fg="white">
48
49
  {" "}{parsed.content || ''}
49
50
  </text>
50
51
  </box>
@@ -56,7 +57,5 @@ export function renderInlineDiffLine(content: string) {
56
57
  }
57
58
 
58
59
  export function getDiffLineBackground(content: string): string | null {
59
- const parsed = parseDiffLine(content);
60
- const colors = getDiffLineColors(parsed);
61
- return colors.contentBackground !== 'transparent' ? colors.contentBackground : null;
60
+ return null;
62
61
  }
@@ -1,26 +1,31 @@
1
- import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from 'fs';
1
+ import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync, unlinkSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { homedir } from 'os';
4
4
 
5
- export interface ConversationStep {
6
- type: 'user' | 'assistant' | 'tool';
7
- content: string;
8
- toolName?: string;
9
- toolArgs?: Record<string, unknown>;
10
- toolResult?: unknown;
11
- timestamp: number;
12
- }
13
-
14
- export interface ConversationHistory {
15
- id: string;
16
- timestamp: number;
17
- steps: ConversationStep[];
18
- totalSteps: number;
19
- totalTokens?: {
20
- prompt: number;
21
- completion: number;
22
- total: number;
23
- };
5
+ export interface ConversationStep {
6
+ type: 'user' | 'assistant' | 'tool';
7
+ content: string;
8
+ images?: import("./images").ImageAttachment[];
9
+ toolName?: string;
10
+ toolArgs?: Record<string, unknown>;
11
+ toolResult?: unknown;
12
+ timestamp: number;
13
+ responseDuration?: number;
14
+ blendWord?: string;
15
+ }
16
+
17
+ export interface ConversationHistory {
18
+ id: string;
19
+ timestamp: number;
20
+ steps: ConversationStep[];
21
+ totalSteps: number;
22
+ title?: string | null;
23
+ workspace?: string | null;
24
+ totalTokens?: {
25
+ prompt: number;
26
+ completion: number;
27
+ total: number;
28
+ };
24
29
  model?: string;
25
30
  provider?: string;
26
31
  }
@@ -36,14 +41,49 @@ export function getHistoryDir(): string {
36
41
  return historyDir;
37
42
  }
38
43
 
39
- export function saveConversation(conversation: ConversationHistory): void {
40
- const historyDir = getHistoryDir();
41
- const filename = `${conversation.id}.json`;
42
- const filepath = join(historyDir, filename);
43
-
44
- writeFileSync(filepath, JSON.stringify(conversation, null, 2), 'utf-8');
45
- }
46
-
44
+ export function saveConversation(conversation: ConversationHistory): void {
45
+ const historyDir = getHistoryDir();
46
+ const filename = `${conversation.id}.json`;
47
+ const filepath = join(historyDir, filename);
48
+
49
+ writeFileSync(filepath, JSON.stringify(conversation, null, 2), 'utf-8');
50
+ }
51
+
52
+ export function updateConversationTitle(id: string, title: string | null): boolean {
53
+ const historyDir = getHistoryDir();
54
+ const filepath = join(historyDir, `${id}.json`);
55
+
56
+ if (!existsSync(filepath)) {
57
+ return false;
58
+ }
59
+
60
+ try {
61
+ const content = readFileSync(filepath, 'utf-8');
62
+ const data = JSON.parse(content) as ConversationHistory;
63
+ data.title = title;
64
+ writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf-8');
65
+ return true;
66
+ } catch (error) {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ export function deleteConversation(id: string): boolean {
72
+ const historyDir = getHistoryDir();
73
+ const filepath = join(historyDir, `${id}.json`);
74
+
75
+ if (!existsSync(filepath)) {
76
+ return false;
77
+ }
78
+
79
+ try {
80
+ unlinkSync(filepath);
81
+ return true;
82
+ } catch (error) {
83
+ return false;
84
+ }
85
+ }
86
+
47
87
  export function loadConversations(): ConversationHistory[] {
48
88
  const historyDir = getHistoryDir();
49
89
 
@@ -51,17 +91,19 @@ export function loadConversations(): ConversationHistory[] {
51
91
  return [];
52
92
  }
53
93
 
54
- const files = readdirSync(historyDir).filter(f => f.endsWith('.json'));
55
- const conversations: ConversationHistory[] = [];
56
-
57
- for (const file of files) {
58
- try {
59
- const content = readFileSync(join(historyDir, file), 'utf-8');
60
- conversations.push(JSON.parse(content));
61
- } catch (error) {
62
- console.error(`Failed to load ${file}:`, error);
63
- }
64
- }
94
+ const files = readdirSync(historyDir).filter(f => f.endsWith('.json') && f !== 'inputs.json');
95
+ const conversations: ConversationHistory[] = [];
96
+
97
+ for (const file of files) {
98
+ try {
99
+ const content = readFileSync(join(historyDir, file), 'utf-8');
100
+ const parsed = JSON.parse(content) as ConversationHistory;
101
+ if (!parsed || !Array.isArray(parsed.steps)) continue;
102
+ conversations.push(parsed);
103
+ } catch (error) {
104
+ console.error(`Failed to load ${file}:`, error);
105
+ }
106
+ }
65
107
 
66
108
  return conversations.sort((a, b) => b.timestamp - a.timestamp);
67
109
  }
@@ -103,4 +145,4 @@ export function addInputToHistory(input: string): void {
103
145
 
104
146
  saveInputHistory(history);
105
147
  }
106
- }
148
+ }
@@ -0,0 +1,28 @@
1
+ import type { ImageAttachment } from "./images";
2
+
3
+ export type ImageCommandEvent =
4
+ | { type: "add"; image: ImageAttachment }
5
+ | { type: "clear" }
6
+ | { type: "remove"; id: string };
7
+
8
+ const listeners = new Set<(event: ImageCommandEvent) => void>();
9
+ let imageSupport = false;
10
+
11
+ export function subscribeImageCommand(listener: (event: ImageCommandEvent) => void): () => void {
12
+ listeners.add(listener);
13
+ return () => {
14
+ listeners.delete(listener);
15
+ };
16
+ }
17
+
18
+ export function emitImageCommand(event: ImageCommandEvent): void {
19
+ listeners.forEach((listener) => listener(event));
20
+ }
21
+
22
+ export function setImageSupport(enabled: boolean): void {
23
+ imageSupport = enabled;
24
+ }
25
+
26
+ export function canUseImages(): boolean {
27
+ return imageSupport;
28
+ }
@@ -0,0 +1,31 @@
1
+ export type ImageAttachment = {
2
+ id: string;
3
+ name: string;
4
+ mimeType: string;
5
+ data: string;
6
+ size: number;
7
+ };
8
+
9
+ const EXT_TO_MIME: Record<string, string> = {
10
+ png: "image/png",
11
+ jpg: "image/jpeg",
12
+ jpeg: "image/jpeg",
13
+ webp: "image/webp",
14
+ gif: "image/gif",
15
+ bmp: "image/bmp",
16
+ svg: "image/svg+xml",
17
+ tif: "image/tiff",
18
+ tiff: "image/tiff"
19
+ };
20
+
21
+ export function guessImageMimeType(filename: string): string {
22
+ const clean = filename.trim().toLowerCase();
23
+ const idx = clean.lastIndexOf(".");
24
+ if (idx === -1) return "application/octet-stream";
25
+ const ext = clean.slice(idx + 1);
26
+ return EXT_TO_MIME[ext] || "application/octet-stream";
27
+ }
28
+
29
+ export function toDataUrl(image: ImageAttachment): string {
30
+ return `data:${image.mimeType};base64,${image.data}`;
31
+ }
@@ -152,23 +152,16 @@ export class ModelsDevClient {
152
152
 
153
153
  async getModelById(modelId: ModelsDevModelId, options: { refresh?: boolean } = {}): Promise<ModelsDevSearchResult | null> {
154
154
  const data = await this.getAll(options);
155
- // Try exact match first
156
155
  for (const provider of Object.values(data)) {
157
156
  const model = provider.models?.[modelId];
158
157
  if (model) return { provider, model };
159
158
  }
160
-
161
- // Try semantic/partial match
162
- // e.g. gpt-5.2-2025-12-11 should match gpt-5.2 or vice versa
163
159
  const lowerSearch = modelId.toLowerCase();
164
160
 
165
161
  for (const provider of Object.values(data)) {
166
162
  const models = provider.models ?? {};
167
163
  for (const [id, model] of Object.entries(models)) {
168
164
  const lowerId = id.toLowerCase();
169
- // If the known model ID is a prefix of our search (e.g. search gpt-5.2-v1 matches model gpt-5.2)
170
- // OR if our search is a prefix of the known model ID (e.g. search gpt-5.2 matches model gpt-5.2-preview)
171
- // OR if one contains the other
172
165
  if (lowerSearch.includes(lowerId) || lowerId.includes(lowerSearch)) {
173
166
  return { provider, model };
174
167
  }
@@ -0,0 +1,23 @@
1
+ export type NotificationType = 'info' | 'success' | 'error' | 'warning'
2
+
3
+ export interface NotificationPayload {
4
+ message: string
5
+ type?: NotificationType
6
+ duration?: number
7
+ }
8
+
9
+ type NotificationListener = (payload: NotificationPayload) => void
10
+
11
+ const listeners = new Set<NotificationListener>()
12
+
13
+ export function subscribeNotifications(listener: NotificationListener): () => void {
14
+ listeners.add(listener)
15
+ return () => {
16
+ listeners.delete(listener)
17
+ }
18
+ }
19
+
20
+ export function notifyNotification(message: string, type: NotificationType = 'info', duration?: number): void {
21
+ const payload: NotificationPayload = { message, type, duration }
22
+ listeners.forEach((listener) => listener(payload))
23
+ }