@kirosnn/mosaic 0.0.7

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 (154) hide show
  1. package/.mosaic/mosaic.local.jsonc +0 -0
  2. package/MOSAIC.md +188 -0
  3. package/README.md +127 -0
  4. package/docs/mosaic.png +0 -0
  5. package/package.json +42 -0
  6. package/src/agent/Agent.ts +131 -0
  7. package/src/agent/context.ts +96 -0
  8. package/src/agent/index.ts +2 -0
  9. package/src/agent/prompts/systemPrompt.ts +138 -0
  10. package/src/agent/prompts/toolsPrompt.ts +139 -0
  11. package/src/agent/provider/anthropic.ts +122 -0
  12. package/src/agent/provider/google.ts +124 -0
  13. package/src/agent/provider/mistral.ts +117 -0
  14. package/src/agent/provider/ollama.ts +531 -0
  15. package/src/agent/provider/openai.ts +220 -0
  16. package/src/agent/provider/xai.ts +122 -0
  17. package/src/agent/tools/bash.ts +20 -0
  18. package/src/agent/tools/definitions.ts +27 -0
  19. package/src/agent/tools/edit.ts +23 -0
  20. package/src/agent/tools/executor.ts +751 -0
  21. package/src/agent/tools/explore.ts +18 -0
  22. package/src/agent/tools/exploreExecutor.ts +320 -0
  23. package/src/agent/tools/glob.ts +16 -0
  24. package/src/agent/tools/grep.ts +19 -0
  25. package/src/agent/tools/index.ts +4 -0
  26. package/src/agent/tools/list.ts +20 -0
  27. package/src/agent/tools/question.ts +20 -0
  28. package/src/agent/tools/read.ts +15 -0
  29. package/src/agent/tools/write.ts +21 -0
  30. package/src/agent/types.ts +155 -0
  31. package/src/components/App.tsx +174 -0
  32. package/src/components/CommandsModal.tsx +77 -0
  33. package/src/components/CustomInput.tsx +328 -0
  34. package/src/components/Main.tsx +1112 -0
  35. package/src/components/Notification.tsx +91 -0
  36. package/src/components/SelectList.tsx +47 -0
  37. package/src/components/Setup.tsx +528 -0
  38. package/src/components/ShortcutsModal.tsx +67 -0
  39. package/src/components/Welcome.tsx +39 -0
  40. package/src/components/main/ApprovalPanel.tsx +134 -0
  41. package/src/components/main/ChatPage.tsx +516 -0
  42. package/src/components/main/HomePage.tsx +111 -0
  43. package/src/components/main/QuestionPanel.tsx +85 -0
  44. package/src/components/main/ThinkingIndicator.tsx +101 -0
  45. package/src/components/main/types.ts +55 -0
  46. package/src/components/main/wrapText.ts +41 -0
  47. package/src/index.tsx +212 -0
  48. package/src/utils/approvalBridge.ts +129 -0
  49. package/src/utils/commands/echo.ts +22 -0
  50. package/src/utils/commands/help.ts +25 -0
  51. package/src/utils/commands/index.ts +68 -0
  52. package/src/utils/commands/init.ts +68 -0
  53. package/src/utils/commands/redo.ts +74 -0
  54. package/src/utils/commands/registry.ts +29 -0
  55. package/src/utils/commands/sessions.ts +129 -0
  56. package/src/utils/commands/types.ts +20 -0
  57. package/src/utils/commands/undo.ts +75 -0
  58. package/src/utils/commands/web.ts +77 -0
  59. package/src/utils/config.ts +357 -0
  60. package/src/utils/diff.ts +201 -0
  61. package/src/utils/diffRendering.tsx +62 -0
  62. package/src/utils/exploreBridge.ts +87 -0
  63. package/src/utils/fileChangeTracker.ts +98 -0
  64. package/src/utils/fileChangesBridge.ts +18 -0
  65. package/src/utils/history.ts +106 -0
  66. package/src/utils/markdown.tsx +232 -0
  67. package/src/utils/models.ts +304 -0
  68. package/src/utils/questionBridge.ts +122 -0
  69. package/src/utils/terminalUtils.ts +25 -0
  70. package/src/utils/toolFormatting.ts +384 -0
  71. package/src/utils/undoRedo.ts +429 -0
  72. package/src/utils/undoRedoBridge.ts +45 -0
  73. package/src/utils/undoRedoDb.ts +338 -0
  74. package/src/utils/uninstall.ts +45 -0
  75. package/src/utils/version.ts +3 -0
  76. package/src/web/app.tsx +606 -0
  77. package/src/web/assets/css/ChatPage.css +212 -0
  78. package/src/web/assets/css/FileExplorer.css +202 -0
  79. package/src/web/assets/css/HomePage.css +119 -0
  80. package/src/web/assets/css/Markdown.css +178 -0
  81. package/src/web/assets/css/MessageItem.css +160 -0
  82. package/src/web/assets/css/Sidebar.css +208 -0
  83. package/src/web/assets/css/SidebarModal.css +137 -0
  84. package/src/web/assets/css/ThinkingIndicator.css +47 -0
  85. package/src/web/assets/css/ToolMessage.css +148 -0
  86. package/src/web/assets/css/global.css +226 -0
  87. package/src/web/assets/fonts/Geist-Black.woff2 +0 -0
  88. package/src/web/assets/fonts/Geist-BlackItalic.woff2 +0 -0
  89. package/src/web/assets/fonts/Geist-Bold.woff2 +0 -0
  90. package/src/web/assets/fonts/Geist-BoldItalic.woff2 +0 -0
  91. package/src/web/assets/fonts/Geist-ExtraBold.woff2 +0 -0
  92. package/src/web/assets/fonts/Geist-ExtraBoldItalic.woff2 +0 -0
  93. package/src/web/assets/fonts/Geist-ExtraLight.woff2 +0 -0
  94. package/src/web/assets/fonts/Geist-ExtraLightItalic.woff2 +0 -0
  95. package/src/web/assets/fonts/Geist-Italic[wght].woff2 +0 -0
  96. package/src/web/assets/fonts/Geist-Light.woff2 +0 -0
  97. package/src/web/assets/fonts/Geist-LightItalic.woff2 +0 -0
  98. package/src/web/assets/fonts/Geist-Medium.woff2 +0 -0
  99. package/src/web/assets/fonts/Geist-MediumItalic.woff2 +0 -0
  100. package/src/web/assets/fonts/Geist-Regular.woff2 +0 -0
  101. package/src/web/assets/fonts/Geist-RegularItalic.woff2 +0 -0
  102. package/src/web/assets/fonts/Geist-SemiBold.woff2 +0 -0
  103. package/src/web/assets/fonts/Geist-SemiBoldItalic.woff2 +0 -0
  104. package/src/web/assets/fonts/Geist-Thin.woff2 +0 -0
  105. package/src/web/assets/fonts/Geist-ThinItalic.woff2 +0 -0
  106. package/src/web/assets/fonts/GeistMono-Black.woff2 +0 -0
  107. package/src/web/assets/fonts/GeistMono-BlackItalic.woff2 +0 -0
  108. package/src/web/assets/fonts/GeistMono-Bold.woff2 +0 -0
  109. package/src/web/assets/fonts/GeistMono-BoldItalic.woff2 +0 -0
  110. package/src/web/assets/fonts/GeistMono-ExtraBold.woff2 +0 -0
  111. package/src/web/assets/fonts/GeistMono-ExtraBoldItalic.woff2 +0 -0
  112. package/src/web/assets/fonts/GeistMono-ExtraLight.woff2 +0 -0
  113. package/src/web/assets/fonts/GeistMono-ExtraLightItalic.woff2 +0 -0
  114. package/src/web/assets/fonts/GeistMono-Italic.woff2 +0 -0
  115. package/src/web/assets/fonts/GeistMono-Italic[wght].woff2 +0 -0
  116. package/src/web/assets/fonts/GeistMono-Light.woff2 +0 -0
  117. package/src/web/assets/fonts/GeistMono-LightItalic.woff2 +0 -0
  118. package/src/web/assets/fonts/GeistMono-Medium.woff2 +0 -0
  119. package/src/web/assets/fonts/GeistMono-MediumItalic.woff2 +0 -0
  120. package/src/web/assets/fonts/GeistMono-Regular.woff2 +0 -0
  121. package/src/web/assets/fonts/GeistMono-SemiBold.woff2 +0 -0
  122. package/src/web/assets/fonts/GeistMono-SemiBoldItalic.woff2 +0 -0
  123. package/src/web/assets/fonts/GeistMono-Thin.woff2 +0 -0
  124. package/src/web/assets/fonts/GeistMono-ThinItalic.woff2 +0 -0
  125. package/src/web/assets/fonts/GeistMono[wght].woff2 +0 -0
  126. package/src/web/assets/fonts/Geist[wght].woff2 +0 -0
  127. package/src/web/assets/fonts/blauer-nue-regular.woff2 +0 -0
  128. package/src/web/assets/fonts/neue-montreal-regular.woff2 +0 -0
  129. package/src/web/assets/images/favicon-v2.svg +6 -0
  130. package/src/web/assets/images/favicon.png +0 -0
  131. package/src/web/assets/images/foruse.svg +5 -0
  132. package/src/web/assets/images/logo_black.svg +5 -0
  133. package/src/web/assets/images/logo_white.svg +5 -0
  134. package/src/web/assets/images/logoblack.png +0 -0
  135. package/src/web/assets/images/logowhite.png +0 -0
  136. package/src/web/build.ts +23 -0
  137. package/src/web/components/ApprovalPanel.tsx +191 -0
  138. package/src/web/components/ChatPage.tsx +273 -0
  139. package/src/web/components/FileExplorer.tsx +162 -0
  140. package/src/web/components/HomePage.tsx +121 -0
  141. package/src/web/components/MessageItem.tsx +178 -0
  142. package/src/web/components/Modal.tsx +30 -0
  143. package/src/web/components/QuestionPanel.tsx +149 -0
  144. package/src/web/components/Setup.tsx +211 -0
  145. package/src/web/components/Sidebar.tsx +292 -0
  146. package/src/web/components/ThinkingIndicator.tsx +85 -0
  147. package/src/web/logo_black.svg +5 -0
  148. package/src/web/logo_white.svg +5 -0
  149. package/src/web/router.ts +46 -0
  150. package/src/web/server.tsx +662 -0
  151. package/src/web/storage.ts +92 -0
  152. package/src/web/types.ts +17 -0
  153. package/src/web/utils.ts +61 -0
  154. package/tsconfig.json +33 -0
@@ -0,0 +1,357 @@
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { VERSION } from './version';
5
+
6
+ const CONFIG_DIR = join(homedir(), '.mosaic');
7
+ const CONFIG_FILE = join(CONFIG_DIR, 'mosaic.jsonc');
8
+
9
+ export interface AIProvider {
10
+ id: string;
11
+ name: string;
12
+ description: string;
13
+ models: AIModel[];
14
+ requiresApiKey: boolean;
15
+ }
16
+
17
+ export interface AIModel {
18
+ id: string;
19
+ name: string;
20
+ description: string;
21
+ requiresApiKey?: boolean;
22
+ }
23
+
24
+ export interface CustomProvider extends AIProvider {
25
+ baseUrl?: string;
26
+ isCustom: true;
27
+ }
28
+
29
+ export interface RecentProject {
30
+ path: string;
31
+ lastOpened: number;
32
+ }
33
+
34
+ export interface MosaicConfig {
35
+ firstRun: boolean;
36
+ version: string;
37
+ provider?: string;
38
+ model?: string;
39
+ apiKey?: string;
40
+ systemPrompt?: string;
41
+ customProviders?: CustomProvider[];
42
+ customModels?: { [providerId: string]: AIModel[] };
43
+ requireApprovals?: boolean;
44
+ recentProjects?: RecentProject[];
45
+ }
46
+
47
+ export const AI_PROVIDERS: AIProvider[] = [
48
+ {
49
+ id: 'openai',
50
+ name: 'OpenAI',
51
+ description: 'GPT models from OpenAI',
52
+ requiresApiKey: true,
53
+ models: [
54
+ { id: 'gpt-5.2-2025-12-11', name: 'GPT-5.2', description: 'The best model for coding and agentic tasks across industries' },
55
+ { id: 'gpt-5.1-2025-11-13', name: 'GPT-5.1', description: 'The best model for coding and agentic tasks with configurable reasoning effort' },
56
+ { id: 'gpt-5-2025-08-07', name: 'GPT-5', description: 'The first model GPT 5 series from OpenAI' },
57
+ { id: 'gpt-4.1-2025-04-14', name: 'GPT-4.1', description: 'Smartest non-reasoning model from OpenAI' },
58
+ ]
59
+ },
60
+ {
61
+ id: 'anthropic',
62
+ name: 'Anthropic',
63
+ description: 'Claude models with extended context windows',
64
+ requiresApiKey: true,
65
+ models: [
66
+ { id: 'claude-opus-4-5', name: 'Claude Opus 4.5', description: 'Most capable Claude model' },
67
+ { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5', description: 'Balanced performance and speed' },
68
+ { id: 'claude-sonnet-4', name: 'Claude Sonnet 4', description: 'Previous Sonnet model' },
69
+ { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5', description: 'Fastest Claude model' },
70
+ ]
71
+ },
72
+ {
73
+ id: 'mistral',
74
+ name: 'Mistral',
75
+ description: 'Mistral AI is a French company that develops open and efficient artificial intelligence models for various applications',
76
+ requiresApiKey: true,
77
+ models: [
78
+ { id: 'mistral-large-latest', name: 'Mistral Large 3', description: 'Mistral Large 3, is a state-of-the-art, open-weight, general-purpose multimodal model' },
79
+ { id: 'devstral-medium-latest', name: 'Devstral 2', description: 'Frontier code agents model for solving software engineering tasks' },
80
+ { id: 'mistral-medium-latest', name: 'Mistral Medium 3.1', description: 'Frontier-class multimodal model' },
81
+ ]
82
+ },
83
+ {
84
+ id: 'xai',
85
+ name: 'xAI',
86
+ description: 'xAI is an AI company focused on creating AI for understanding the universe',
87
+ requiresApiKey: true,
88
+ models: [
89
+ { id: 'grok-4-1-fast-reasoning', name: 'Grok 4.1 Fast Reasoning', description: 'A frontier multimodal model optimized specifically for high-performance agentic tool calling' },
90
+ { id: 'grok-4-fast-reasoning', name: 'Grok 4 Fast Reasoning', description: 'Advancement in cost-efficient reasoning models.' },
91
+ { id: 'grok-code-fast-1', name: 'Grok Code Fast 1', description: 'Optimized model for coding, programming, and software development tasks' },
92
+ ]
93
+ },
94
+ {
95
+ id: 'google',
96
+ name: 'Google',
97
+ description: 'Introducing Google would almost be insulting',
98
+ requiresApiKey: true,
99
+ models: [
100
+ { id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro Preview', description: 'The first model in the new series, is ideal for complex tasks that require extensive world knowledge and advanced reasoning in multiple modalities' },
101
+ { id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash Preview', description: 'Latest model in the 3 series. It offers Pro-level intelligence at the speed and price of Flash' },
102
+ { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', description: 'A versatile, cutting-edge model that excels in complex coding and reasoning tasks' },
103
+ ]
104
+ },
105
+ {
106
+ id: 'ollama',
107
+ name: 'Ollama',
108
+ description: 'Run models locally on your machine',
109
+ requiresApiKey: false,
110
+ models: [
111
+ { id: 'gpt-oss:120b', name: 'GPT OSS 120b', description: 'Best OSS reasoning model (and only one) OpenAI created' },
112
+ { id: 'glm-4.7:cloud', name: 'GLM 4.7 Cloud', description: 'Advancing the coding capability, from zAI', requiresApiKey: true },
113
+ { id: 'devstral-2:123b-cloud', name: 'Devstral 2 Cloud', description: 'Devstral is an agentic LLM for software engineering tasks, from Mistral', requiresApiKey: true },
114
+ ]
115
+ },
116
+ {
117
+ id: 'openrouter',
118
+ name: 'OpenRouter',
119
+ description: 'Access to various AI models through a unified API',
120
+ requiresApiKey: true,
121
+ models: [
122
+ { id: 'anthropic/claude-sonnet-4.5', name: 'Claude Sonnet 4.5', description: 'Most capable Claude model' },
123
+ { id: 'x-ai/grok-code-fast-1', name: 'Grok Code Fast 1', description: 'Grok Code Fast 1 is a speedy and economical reasoning model that excels at agentic coding.' },
124
+ { id: 'anthropic/claude-opus-4.5', name: 'Claude 3', description: 'Most capable Claude model' },
125
+ ]
126
+ }
127
+ ];
128
+
129
+ export function ensureConfigDir(): void {
130
+ if (!existsSync(CONFIG_DIR)) {
131
+ mkdirSync(CONFIG_DIR, { recursive: true });
132
+ }
133
+ }
134
+
135
+ export function isFirstRun(): boolean {
136
+ ensureConfigDir();
137
+
138
+ if (!existsSync(CONFIG_FILE)) {
139
+ return true;
140
+ }
141
+
142
+ try {
143
+ const config = readConfig();
144
+ return config.firstRun !== false;
145
+ } catch {
146
+ return true;
147
+ }
148
+ }
149
+
150
+ export function readConfig(): MosaicConfig {
151
+ if (!existsSync(CONFIG_FILE)) {
152
+ return {
153
+ firstRun: true,
154
+ version: VERSION,
155
+ requireApprovals: true
156
+ };
157
+ }
158
+
159
+ try {
160
+ const content = readFileSync(CONFIG_FILE, 'utf-8');
161
+ const config = JSON.parse(content);
162
+
163
+ if (config.requireApprovals === undefined) {
164
+ config.requireApprovals = true;
165
+ }
166
+
167
+ return config;
168
+ } catch (error) {
169
+ if (error instanceof SyntaxError) {
170
+ console.warn('Config file is corrupted. Resetting to default.');
171
+ try {
172
+ const { renameSync } = require('fs');
173
+ renameSync(CONFIG_FILE, `${CONFIG_FILE}.bak`);
174
+ } catch { }
175
+ }
176
+ return {
177
+ firstRun: true,
178
+ version: VERSION,
179
+ requireApprovals: true
180
+ };
181
+ }
182
+ }
183
+
184
+ export function writeConfig(config: MosaicConfig): void {
185
+ ensureConfigDir();
186
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
187
+ }
188
+
189
+ export function markFirstRunComplete(provider: string, model: string, apiKey?: string): void {
190
+ const config = readConfig();
191
+ config.firstRun = false;
192
+ config.provider = provider;
193
+ config.model = model;
194
+ config.apiKey = apiKey;
195
+ writeConfig(config);
196
+ }
197
+
198
+ export function getConfigDir(): string {
199
+ return CONFIG_DIR;
200
+ }
201
+
202
+ export function getAllProviders(): AIProvider[] {
203
+ const config = readConfig();
204
+ const customProviders = config.customProviders || [];
205
+ const customModels = config.customModels || {};
206
+
207
+ const providersWithCustomModels = AI_PROVIDERS.map(provider => {
208
+ const customModelsForProvider = customModels[provider.id] || [];
209
+ if (customModelsForProvider.length > 0) {
210
+ return {
211
+ ...provider,
212
+ models: [...provider.models, ...customModelsForProvider]
213
+ };
214
+ }
215
+ return provider;
216
+ });
217
+
218
+ return [...providersWithCustomModels, ...customProviders];
219
+ }
220
+
221
+ export function getProviderById(id: string): AIProvider | undefined {
222
+ return getAllProviders().find(p => p.id === id);
223
+ }
224
+
225
+ export function getModelById(providerId: string, modelId: string): AIModel | undefined {
226
+ const provider = getProviderById(providerId);
227
+ return provider?.models.find(m => m.id === modelId);
228
+ }
229
+
230
+ export function modelRequiresApiKey(providerId: string, modelId: string): boolean {
231
+ const provider = getProviderById(providerId);
232
+ const model = getModelById(providerId, modelId);
233
+
234
+ if (model?.requiresApiKey !== undefined) {
235
+ return model.requiresApiKey === true;
236
+ }
237
+
238
+ return provider?.requiresApiKey === true;
239
+ }
240
+
241
+ export function addCustomProvider(provider: CustomProvider): void {
242
+ const config = readConfig();
243
+ if (!config.customProviders) {
244
+ config.customProviders = [];
245
+ }
246
+ config.customProviders.push(provider);
247
+ writeConfig(config);
248
+ }
249
+
250
+ export function removeCustomProvider(id: string): void {
251
+ const config = readConfig();
252
+ if (config.customProviders) {
253
+ config.customProviders = config.customProviders.filter(p => p.id !== id);
254
+ writeConfig(config);
255
+ }
256
+ }
257
+
258
+ export function updateCustomProvider(id: string, updates: Partial<CustomProvider>): void {
259
+ const config = readConfig();
260
+ if (config.customProviders) {
261
+ const index = config.customProviders.findIndex(p => p.id === id);
262
+ if (index !== -1) {
263
+ config.customProviders[index] = { ...config.customProviders[index]!, ...updates };
264
+ writeConfig(config);
265
+ }
266
+ }
267
+ }
268
+
269
+ export function addCustomModel(providerId: string, model: AIModel): void {
270
+ const config = readConfig();
271
+ if (!config.customModels) {
272
+ config.customModels = {};
273
+ }
274
+ if (!config.customModels[providerId]) {
275
+ config.customModels[providerId] = [];
276
+ }
277
+ config.customModels[providerId].push(model);
278
+ writeConfig(config);
279
+ }
280
+
281
+ export function removeCustomModel(providerId: string, modelId: string): void {
282
+ const config = readConfig();
283
+ if (config.customModels && config.customModels[providerId]) {
284
+ config.customModels[providerId] = config.customModels[providerId].filter(m => m.id !== modelId);
285
+ writeConfig(config);
286
+ }
287
+ }
288
+
289
+ export function getCustomModels(providerId: string): AIModel[] {
290
+ const config = readConfig();
291
+ return config.customModels?.[providerId] || [];
292
+ }
293
+
294
+ export function updateSystemPrompt(systemPrompt: string): void {
295
+ const config = readConfig();
296
+ config.systemPrompt = systemPrompt;
297
+ writeConfig(config);
298
+ }
299
+
300
+ export function getSystemPrompt(): string | undefined {
301
+ const config = readConfig();
302
+ return config.systemPrompt;
303
+ }
304
+
305
+ export function shouldRequireApprovals(): boolean {
306
+ const config = readConfig();
307
+ return config.requireApprovals !== false;
308
+ }
309
+
310
+ export function setRequireApprovals(require: boolean): void {
311
+ const config = readConfig();
312
+ config.requireApprovals = require;
313
+ writeConfig(config);
314
+ }
315
+
316
+ const MAX_RECENT_PROJECTS = 10;
317
+
318
+ export function getRecentProjects(): RecentProject[] {
319
+ const config = readConfig();
320
+ return config.recentProjects || [];
321
+ }
322
+
323
+ export function addRecentProject(projectPath: string): void {
324
+ const config = readConfig();
325
+ const recentProjects = config.recentProjects || [];
326
+
327
+ const existingIndex = recentProjects.findIndex(p => p.path === projectPath);
328
+ if (existingIndex !== -1) {
329
+ recentProjects.splice(existingIndex, 1);
330
+ }
331
+
332
+ recentProjects.unshift({
333
+ path: projectPath,
334
+ lastOpened: Date.now()
335
+ });
336
+
337
+ if (recentProjects.length > MAX_RECENT_PROJECTS) {
338
+ recentProjects.pop();
339
+ }
340
+
341
+ config.recentProjects = recentProjects;
342
+ writeConfig(config);
343
+ }
344
+
345
+ export function removeRecentProject(projectPath: string): void {
346
+ const config = readConfig();
347
+ if (config.recentProjects) {
348
+ config.recentProjects = config.recentProjects.filter(p => p.path !== projectPath);
349
+ writeConfig(config);
350
+ }
351
+ }
352
+
353
+ export function clearRecentProjects(): void {
354
+ const config = readConfig();
355
+ config.recentProjects = [];
356
+ writeConfig(config);
357
+ }
@@ -0,0 +1,201 @@
1
+ export interface DiffLine {
2
+ type: 'added' | 'removed' | 'unchanged';
3
+ lineNumber: number | null;
4
+ content: string;
5
+ }
6
+
7
+ export interface DiffResult {
8
+ lines: DiffLine[];
9
+ hasChanges: boolean;
10
+ }
11
+
12
+ function computeLCS(a: string[], b: string[]): number[][] {
13
+ const m = a.length;
14
+ const n = b.length;
15
+ const dp: number[][] = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0));
16
+
17
+ for (let i = 1; i <= m; i++) {
18
+ for (let j = 1; j <= n; j++) {
19
+ if (a[i - 1] === b[j - 1]) {
20
+ dp[i]![j] = (dp[i - 1]?.[j - 1] ?? 0) + 1;
21
+ } else {
22
+ dp[i]![j] = Math.max(dp[i - 1]?.[j] ?? 0, dp[i]?.[j - 1] ?? 0);
23
+ }
24
+ }
25
+ }
26
+
27
+ return dp;
28
+ }
29
+
30
+ function backtrackLCS(
31
+ a: string[],
32
+ b: string[],
33
+ dp: number[][],
34
+ i: number,
35
+ j: number,
36
+ result: DiffLine[]
37
+ ): void {
38
+ if (i === 0 && j === 0) return;
39
+
40
+ if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
41
+ backtrackLCS(a, b, dp, i - 1, j - 1, result);
42
+ result.push({
43
+ type: 'unchanged',
44
+ lineNumber: i,
45
+ content: a[i - 1] ?? '',
46
+ });
47
+ } else if (j > 0 && (i === 0 || (dp[i]?.[j - 1] ?? 0) >= (dp[i - 1]?.[j] ?? 0))) {
48
+ backtrackLCS(a, b, dp, i, j - 1, result);
49
+ result.push({
50
+ type: 'added',
51
+ lineNumber: null,
52
+ content: b[j - 1] ?? '',
53
+ });
54
+ } else if (i > 0) {
55
+ backtrackLCS(a, b, dp, i - 1, j, result);
56
+ result.push({
57
+ type: 'removed',
58
+ lineNumber: i,
59
+ content: a[i - 1] ?? '',
60
+ });
61
+ }
62
+ }
63
+
64
+ export function generateDiff(oldContent: string, newContent: string): DiffResult {
65
+ const oldLines = oldContent === '' ? [] : oldContent.split('\n');
66
+ const newLines = newContent === '' ? [] : newContent.split('\n');
67
+
68
+ const dp = computeLCS(oldLines, newLines);
69
+ const diffLines: DiffLine[] = [];
70
+
71
+ backtrackLCS(oldLines, newLines, dp, oldLines.length, newLines.length, diffLines);
72
+
73
+ const hasChanges = diffLines.some(line => line.type !== 'unchanged');
74
+
75
+ return {
76
+ lines: diffLines,
77
+ hasChanges,
78
+ };
79
+ }
80
+
81
+ export function formatDiffForDisplay(diff: DiffResult, maxLines = 0): string[] {
82
+ const result: string[] = [];
83
+ let addedLineNumber = 1;
84
+ let removedLineNumber = 1;
85
+
86
+ for (const line of diff.lines) {
87
+ if (maxLines > 0 && result.length >= maxLines) {
88
+ const remaining = diff.lines.length - result.length;
89
+ if (remaining > 0) {
90
+ result.push(`... (${remaining} more lines)`);
91
+ }
92
+ break;
93
+ }
94
+
95
+ switch (line.type) {
96
+ case 'added':
97
+ result.push(`+${String(addedLineNumber).padStart(4)} | ${line.content}`);
98
+ addedLineNumber++;
99
+ break;
100
+ case 'removed':
101
+ result.push(`-${String(removedLineNumber).padStart(4)} | ${line.content}`);
102
+ removedLineNumber++;
103
+ break;
104
+ case 'unchanged':
105
+ addedLineNumber++;
106
+ removedLineNumber++;
107
+ break;
108
+ }
109
+ }
110
+
111
+ return result;
112
+ }
113
+
114
+ export function formatWriteToolResult(result: unknown, isAppend: boolean): string[] {
115
+ if (isAppend) return ['Appended'];
116
+
117
+ if (result && typeof result === 'object') {
118
+ const obj = result as Record<string, unknown>;
119
+ const diff = obj.diff;
120
+ if (Array.isArray(diff)) {
121
+ if (diff.length === 0) return ['No changes'];
122
+ const maxLines = 10;
123
+ if (diff.length > maxLines) {
124
+ const visibleDiff = diff.slice(0, maxLines);
125
+ const remaining = diff.length - maxLines;
126
+ return [...visibleDiff, `(${remaining} more lines)`];
127
+ }
128
+ return diff as string[];
129
+ }
130
+ }
131
+
132
+ const resultStr = typeof result === 'string' ? result : '';
133
+ const lineCount = resultStr ? resultStr.split('\n').length : 0;
134
+ return lineCount > 0 ? [`Wrote ${lineCount} lines`] : ['Done'];
135
+ }
136
+
137
+ export function formatEditToolResult(result: unknown): string[] {
138
+ if (result && typeof result === 'object') {
139
+ const obj = result as Record<string, unknown>;
140
+ const diff = obj.diff;
141
+ if (Array.isArray(diff)) {
142
+ if (diff.length === 0) return ['No changes'];
143
+ return diff as string[];
144
+ }
145
+ }
146
+
147
+ const resultStr = typeof result === 'string' ? result : '';
148
+ const lineCount = resultStr ? resultStr.split('\n').length : 0;
149
+ return lineCount > 0 ? [`Edited ${lineCount} lines`] : ['Edited'];
150
+ }
151
+
152
+ export interface ParsedDiffLine {
153
+ isDiffLine: boolean;
154
+ prefix?: '+' | '-';
155
+ lineNumber?: string;
156
+ content?: string;
157
+ isAdded: boolean;
158
+ isRemoved: boolean;
159
+ }
160
+
161
+ export function parseDiffLine(line: string): ParsedDiffLine {
162
+ const match = line.match(/^([+-])\s*(\d+)\s*\|?\s*(.*)$/);
163
+
164
+ if (!match) {
165
+ return {
166
+ isDiffLine: false,
167
+ isAdded: false,
168
+ isRemoved: false,
169
+ };
170
+ }
171
+
172
+ const [, prefix, lineNum, content] = match;
173
+ const isAdded = prefix === '+';
174
+ const isRemoved = prefix === '-';
175
+
176
+ return {
177
+ isDiffLine: true,
178
+ prefix: prefix as '+' | '-',
179
+ lineNumber: lineNum,
180
+ content,
181
+ isAdded,
182
+ isRemoved,
183
+ };
184
+ }
185
+
186
+ export function getDiffLineColors(parsed: ParsedDiffLine): {
187
+ labelBackground: string;
188
+ contentBackground: string;
189
+ } {
190
+ if (!parsed.isDiffLine) {
191
+ return {
192
+ labelBackground: 'transparent',
193
+ contentBackground: 'transparent',
194
+ };
195
+ }
196
+
197
+ return {
198
+ labelBackground: parsed.isAdded ? '#0d2b0d' : parsed.isRemoved ? '#2b0d0d' : 'transparent',
199
+ contentBackground: parsed.isAdded ? '#1a3a1a' : parsed.isRemoved ? '#3a1a1a' : 'transparent',
200
+ };
201
+ }
@@ -0,0 +1,62 @@
1
+ import { TextAttributes } from '@opentui/core';
2
+ import { parseDiffLine, getDiffLineColors } from './diff';
3
+
4
+ export function renderDiffLine(line: string, key: string) {
5
+ const parsed = parseDiffLine(line);
6
+
7
+ if (parsed.isDiffLine) {
8
+ const colors = getDiffLineColors(parsed);
9
+
10
+ return (
11
+ <box key={key} flexDirection="row">
12
+ <box backgroundColor={colors.labelBackground}>
13
+ <text fg="#ffffff">
14
+ {" "}{parsed.prefix}{parsed.lineNumber?.padStart(5) || ''}{' '}
15
+ </text>
16
+ </box>
17
+ <box flexGrow={1} backgroundColor={colors.contentBackground}>
18
+ <text fg="#ffffff">
19
+ {" "}{parsed.content || ''}
20
+ </text>
21
+ </box>
22
+ </box>
23
+ );
24
+ }
25
+
26
+ return (
27
+ <text key={key} fg="#ffffff">
28
+ {line || ' '}
29
+ </text>
30
+ );
31
+ }
32
+
33
+ export function renderInlineDiffLine(content: string) {
34
+ const parsed = parseDiffLine(content);
35
+
36
+ if (parsed.isDiffLine) {
37
+ const colors = getDiffLineColors(parsed);
38
+
39
+ return (
40
+ <>
41
+ <box>
42
+ <text fg="#ffffff">
43
+ {parsed.prefix}{parsed.lineNumber?.padStart(5) || ''}{' '}
44
+ </text>
45
+ </box>
46
+ <box backgroundColor={colors.contentBackground}>
47
+ <text fg="#ffffff">
48
+ {" "}{parsed.content || ''}
49
+ </text>
50
+ </box>
51
+ </>
52
+ );
53
+ }
54
+
55
+ return null;
56
+ }
57
+
58
+ 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;
62
+ }
@@ -0,0 +1,87 @@
1
+ type ExploreToolCallback = (toolName: string, args: Record<string, unknown>, result: { success: boolean; preview: string }, tokenEstimate: number) => void;
2
+
3
+ export interface ExploreToolEvent {
4
+ toolName: string;
5
+ args: Record<string, unknown>;
6
+ success: boolean;
7
+ preview: string;
8
+ tokenEstimate: number;
9
+ }
10
+
11
+ type ExploreToolSubscriber = (event: ExploreToolEvent) => void;
12
+
13
+ interface ExploreBridgeGlobal {
14
+ currentAbortController: AbortController | null;
15
+ toolCallback: ExploreToolCallback | null;
16
+ totalExploreTokens: number;
17
+ subscribers: Set<ExploreToolSubscriber>;
18
+ }
19
+
20
+ const globalKey = '__mosaic_explore_bridge__';
21
+ const g = globalThis as any;
22
+
23
+ if (!g[globalKey]) {
24
+ g[globalKey] = {
25
+ currentAbortController: null,
26
+ toolCallback: null,
27
+ totalExploreTokens: 0,
28
+ subscribers: new Set<ExploreToolSubscriber>(),
29
+ };
30
+ }
31
+
32
+ const state: ExploreBridgeGlobal = g[globalKey];
33
+
34
+ export function setExploreAbortController(controller: AbortController | null): void {
35
+ state.currentAbortController = controller;
36
+ if (controller) {
37
+ state.totalExploreTokens = 0;
38
+ }
39
+ }
40
+
41
+ export function getExploreAbortSignal(): AbortSignal | undefined {
42
+ return state.currentAbortController?.signal;
43
+ }
44
+
45
+ export function abortExplore(): void {
46
+ state.currentAbortController?.abort();
47
+ }
48
+
49
+ export function isExploreAborted(): boolean {
50
+ return state.currentAbortController?.signal.aborted ?? false;
51
+ }
52
+
53
+ export function setExploreToolCallback(callback: ExploreToolCallback | null): void {
54
+ state.toolCallback = callback;
55
+ }
56
+
57
+ export function notifyExploreTool(toolName: string, args: Record<string, unknown>, result: { success: boolean; preview: string }, resultLength: number): void {
58
+ const tokenEstimate = Math.ceil(resultLength / 4);
59
+ state.totalExploreTokens += tokenEstimate;
60
+ state.toolCallback?.(toolName, args, result, state.totalExploreTokens);
61
+
62
+ const event: ExploreToolEvent = {
63
+ toolName,
64
+ args,
65
+ success: result.success,
66
+ preview: result.preview,
67
+ tokenEstimate: state.totalExploreTokens,
68
+ };
69
+ console.log(`[EXPLORE BRIDGE] notify: ${toolName}, subs=${state.subscribers.size}`);
70
+ state.subscribers.forEach(sub => {
71
+ console.log(`[EXPLORE BRIDGE] calling subscriber`);
72
+ sub(event);
73
+ });
74
+ }
75
+
76
+ export function subscribeExploreTool(callback: ExploreToolSubscriber): () => void {
77
+ state.subscribers.add(callback);
78
+ console.log(`[EXPLORE BRIDGE] subscribe: now ${state.subscribers.size} subscribers`);
79
+ return () => {
80
+ state.subscribers.delete(callback);
81
+ console.log(`[EXPLORE BRIDGE] unsubscribe: now ${state.subscribers.size} subscribers`);
82
+ };
83
+ }
84
+
85
+ export function getExploreTokens(): number {
86
+ return state.totalExploreTokens;
87
+ }