@kirosnn/mosaic 0.0.91 → 0.73.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 (99) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -6
  3. package/package.json +55 -48
  4. package/src/agent/Agent.ts +353 -131
  5. package/src/agent/context.ts +4 -4
  6. package/src/agent/prompts/systemPrompt.ts +209 -70
  7. package/src/agent/prompts/toolsPrompt.ts +285 -138
  8. package/src/agent/provider/anthropic.ts +109 -105
  9. package/src/agent/provider/google.ts +111 -107
  10. package/src/agent/provider/mistral.ts +95 -95
  11. package/src/agent/provider/ollama.ts +73 -17
  12. package/src/agent/provider/openai.ts +146 -102
  13. package/src/agent/provider/rateLimit.ts +178 -0
  14. package/src/agent/provider/reasoning.ts +29 -0
  15. package/src/agent/provider/xai.ts +108 -104
  16. package/src/agent/tools/definitions.ts +15 -1
  17. package/src/agent/tools/executor.ts +717 -98
  18. package/src/agent/tools/exploreExecutor.ts +20 -22
  19. package/src/agent/tools/fetch.ts +58 -0
  20. package/src/agent/tools/glob.ts +20 -4
  21. package/src/agent/tools/grep.ts +64 -9
  22. package/src/agent/tools/plan.ts +27 -0
  23. package/src/agent/tools/question.ts +7 -1
  24. package/src/agent/tools/read.ts +2 -0
  25. package/src/agent/types.ts +15 -14
  26. package/src/components/App.tsx +50 -8
  27. package/src/components/CustomInput.tsx +461 -77
  28. package/src/components/Main.tsx +1459 -1112
  29. package/src/components/Setup.tsx +1 -1
  30. package/src/components/ShortcutsModal.tsx +11 -8
  31. package/src/components/Welcome.tsx +1 -1
  32. package/src/components/main/ApprovalPanel.tsx +4 -3
  33. package/src/components/main/ChatPage.tsx +858 -516
  34. package/src/components/main/HomePage.tsx +58 -39
  35. package/src/components/main/QuestionPanel.tsx +52 -7
  36. package/src/components/main/ThinkingIndicator.tsx +13 -2
  37. package/src/components/main/types.ts +11 -10
  38. package/src/index.tsx +53 -25
  39. package/src/mcp/approvalPolicy.ts +148 -0
  40. package/src/mcp/cli/add.ts +185 -0
  41. package/src/mcp/cli/doctor.ts +77 -0
  42. package/src/mcp/cli/index.ts +85 -0
  43. package/src/mcp/cli/list.ts +50 -0
  44. package/src/mcp/cli/logs.ts +24 -0
  45. package/src/mcp/cli/manage.ts +99 -0
  46. package/src/mcp/cli/show.ts +53 -0
  47. package/src/mcp/cli/tools.ts +77 -0
  48. package/src/mcp/config.ts +223 -0
  49. package/src/mcp/index.ts +80 -0
  50. package/src/mcp/processManager.ts +299 -0
  51. package/src/mcp/rateLimiter.ts +50 -0
  52. package/src/mcp/registry.ts +151 -0
  53. package/src/mcp/schemaConverter.ts +100 -0
  54. package/src/mcp/servers/navigation.ts +854 -0
  55. package/src/mcp/toolCatalog.ts +169 -0
  56. package/src/mcp/types.ts +95 -0
  57. package/src/utils/approvalBridge.ts +45 -12
  58. package/src/utils/approvalModeBridge.ts +17 -0
  59. package/src/utils/commands/approvals.ts +48 -0
  60. package/src/utils/commands/compact.ts +30 -0
  61. package/src/utils/commands/echo.ts +1 -1
  62. package/src/utils/commands/image.ts +109 -0
  63. package/src/utils/commands/index.ts +9 -7
  64. package/src/utils/commands/new.ts +15 -0
  65. package/src/utils/commands/types.ts +3 -0
  66. package/src/utils/config.ts +3 -1
  67. package/src/utils/diffRendering.tsx +13 -16
  68. package/src/utils/exploreBridge.ts +10 -0
  69. package/src/utils/history.ts +82 -40
  70. package/src/utils/imageBridge.ts +28 -0
  71. package/src/utils/images.ts +31 -0
  72. package/src/utils/markdown.tsx +163 -99
  73. package/src/utils/models.ts +31 -16
  74. package/src/utils/notificationBridge.ts +23 -0
  75. package/src/utils/questionBridge.ts +36 -1
  76. package/src/utils/tokenEstimator.ts +32 -0
  77. package/src/utils/toolFormatting.ts +428 -48
  78. package/src/web/app.tsx +65 -5
  79. package/src/web/assets/css/ChatPage.css +102 -30
  80. package/src/web/assets/css/MessageItem.css +26 -29
  81. package/src/web/assets/css/ThinkingIndicator.css +44 -6
  82. package/src/web/assets/css/ToolMessage.css +36 -14
  83. package/src/web/components/ChatPage.tsx +228 -105
  84. package/src/web/components/HomePage.tsx +3 -3
  85. package/src/web/components/MessageItem.tsx +80 -81
  86. package/src/web/components/QuestionPanel.tsx +72 -12
  87. package/src/web/components/Setup.tsx +1 -1
  88. package/src/web/components/Sidebar.tsx +1 -3
  89. package/src/web/components/ThinkingIndicator.tsx +41 -21
  90. package/src/web/router.ts +1 -1
  91. package/src/web/server.tsx +894 -662
  92. package/src/web/storage.ts +23 -1
  93. package/src/web/types.ts +7 -6
  94. package/src/utils/commands/redo.ts +0 -74
  95. package/src/utils/commands/sessions.ts +0 -129
  96. package/src/utils/commands/undo.ts +0 -75
  97. package/src/utils/undoRedo.ts +0 -429
  98. package/src/utils/undoRedoBridge.ts +0 -45
  99. package/src/utils/undoRedoDb.ts +0 -338
@@ -0,0 +1,169 @@
1
+ import { tool, type CoreTool } from 'ai';
2
+ import type { McpToolInfo, McpServerConfig } from './types';
3
+ import { parseSafeId } from './types';
4
+ import { McpProcessManager } from './processManager';
5
+ import { McpApprovalPolicy } from './approvalPolicy';
6
+ import { jsonSchemaObjectToZodObject } from './schemaConverter';
7
+
8
+ function matchGlobList(name: string, patterns: string[]): boolean {
9
+ for (const pattern of patterns) {
10
+ if (pattern === '*') return true;
11
+ if (pattern === name) return true;
12
+
13
+ const regex = new RegExp(
14
+ '^' + pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$'
15
+ );
16
+ if (regex.test(name)) return true;
17
+ }
18
+ return false;
19
+ }
20
+
21
+ export class McpToolCatalog {
22
+ private processManager: McpProcessManager;
23
+ private approvalPolicy: McpApprovalPolicy;
24
+ private configs: McpServerConfig[];
25
+ private exposedTools = new Map<string, CoreTool>();
26
+ private toolInfoMap = new Map<string, McpToolInfo>();
27
+ private safeToCanonical = new Map<string, string>();
28
+ private canonicalToSafe = new Map<string, string>();
29
+
30
+ constructor(processManager: McpProcessManager, approvalPolicy: McpApprovalPolicy, configs: McpServerConfig[]) {
31
+ this.processManager = processManager;
32
+ this.approvalPolicy = approvalPolicy;
33
+ this.configs = configs;
34
+ }
35
+
36
+ refreshTools(serverId?: string): void {
37
+ if (serverId) {
38
+ this.refreshServerTools(serverId);
39
+ } else {
40
+ for (const config of this.configs) {
41
+ if (config.enabled) {
42
+ this.refreshServerTools(config.id);
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ private refreshServerTools(serverId: string): void {
49
+ for (const [safeId, info] of this.toolInfoMap) {
50
+ if (info.serverId === serverId) {
51
+ this.exposedTools.delete(safeId);
52
+ this.toolInfoMap.delete(safeId);
53
+ this.safeToCanonical.delete(safeId);
54
+ this.canonicalToSafe.delete(info.canonicalId);
55
+ }
56
+ }
57
+
58
+ const config = this.configs.find(c => c.id === serverId);
59
+ if (!config || !config.enabled) return;
60
+
61
+ const rawTools = this.processManager.listTools(serverId);
62
+
63
+ for (const toolInfo of rawTools) {
64
+ if (config.tools.deny && config.tools.deny.length > 0) {
65
+ if (matchGlobList(toolInfo.name, config.tools.deny)) continue;
66
+ }
67
+
68
+ if (config.tools.allow && config.tools.allow.length > 0) {
69
+ if (!matchGlobList(toolInfo.name, config.tools.allow)) continue;
70
+ }
71
+
72
+ const coreTool = this.convertToCoreToolDef(toolInfo, config);
73
+ this.exposedTools.set(toolInfo.safeId, coreTool);
74
+ this.toolInfoMap.set(toolInfo.safeId, toolInfo);
75
+ this.safeToCanonical.set(toolInfo.safeId, toolInfo.canonicalId);
76
+ this.canonicalToSafe.set(toolInfo.canonicalId, toolInfo.safeId);
77
+ }
78
+ }
79
+
80
+ private convertToCoreToolDef(toolInfo: McpToolInfo, config: McpServerConfig): CoreTool {
81
+ const schema = toolInfo.inputSchema || { type: 'object', properties: {} };
82
+ const zodParams = jsonSchemaObjectToZodObject(schema as Record<string, unknown>);
83
+
84
+ const pm = this.processManager;
85
+ const ap = this.approvalPolicy;
86
+ const serverConfig = config;
87
+
88
+ return tool({
89
+ description: toolInfo.description || `MCP tool: ${toolInfo.name} (${config.name})`,
90
+ parameters: zodParams,
91
+ execute: async (args: Record<string, unknown>) => {
92
+ try {
93
+ const approvalResult = await ap.requestMcpApproval({
94
+ serverId: serverConfig.id,
95
+ serverName: serverConfig.name,
96
+ toolName: toolInfo.name,
97
+ canonicalId: toolInfo.canonicalId,
98
+ args,
99
+ approvalMode: serverConfig.approval,
100
+ });
101
+
102
+ if (!approvalResult.approved) {
103
+ if (approvalResult.customResponse) {
104
+ return {
105
+ error: `OPERATION REJECTED BY USER with custom instructions: "${approvalResult.customResponse}"`,
106
+ userMessage: 'Operation cancelled by user',
107
+ };
108
+ }
109
+ return {
110
+ error: `OPERATION REJECTED BY USER: calling MCP tool ${toolInfo.canonicalId}`,
111
+ userMessage: 'Operation cancelled by user',
112
+ };
113
+ }
114
+
115
+ const result = await pm.callTool(serverConfig.id, toolInfo.name, args);
116
+
117
+ if (result.isError) {
118
+ return { error: result.content };
119
+ }
120
+
121
+ return result.content;
122
+ } catch (error) {
123
+ const message = error instanceof Error ? error.message : String(error);
124
+ return { error: `MCP tool call failed: ${message}` };
125
+ }
126
+ },
127
+ });
128
+ }
129
+
130
+ getExposedTools(): Record<string, CoreTool> {
131
+ const result: Record<string, CoreTool> = {};
132
+ for (const [safeId, coreTool] of this.exposedTools) {
133
+ result[safeId] = coreTool;
134
+ }
135
+ return result;
136
+ }
137
+
138
+ getMcpToolInfos(): McpToolInfo[] {
139
+ return Array.from(this.toolInfoMap.values());
140
+ }
141
+
142
+ getToolInfo(safeId: string): McpToolInfo | null {
143
+ return this.toolInfoMap.get(safeId) || null;
144
+ }
145
+
146
+ getSafeIdFromCanonical(canonicalId: string): string | null {
147
+ return this.canonicalToSafe.get(canonicalId) || null;
148
+ }
149
+
150
+ getCanonicalFromSafeId(safeId: string): string | null {
151
+ return this.safeToCanonical.get(safeId) || null;
152
+ }
153
+
154
+ isMcpTool(toolName: string): boolean {
155
+ return toolName.startsWith('mcp__');
156
+ }
157
+
158
+ parseMcpToolName(safeId: string): { serverId: string; toolName: string } | null {
159
+ return parseSafeId(safeId);
160
+ }
161
+
162
+ updateConfigs(configs: McpServerConfig[]): void {
163
+ this.configs = configs;
164
+ }
165
+
166
+ static mergeTools(internal: Record<string, CoreTool>, mcp: Record<string, CoreTool>): Record<string, CoreTool> {
167
+ return { ...internal, ...mcp };
168
+ }
169
+ }
@@ -0,0 +1,95 @@
1
+ export interface McpTransportConfig {
2
+ type: 'stdio';
3
+ }
4
+
5
+ export interface McpServerConfig {
6
+ id: string;
7
+ name: string;
8
+ enabled: boolean;
9
+ transport: McpTransportConfig;
10
+ command: string;
11
+ args: string[];
12
+ cwd?: string;
13
+ env?: Record<string, string>;
14
+ autostart: 'startup' | 'on-demand' | 'never';
15
+ timeouts: {
16
+ initialize: number;
17
+ call: number;
18
+ };
19
+ limits: {
20
+ maxCallsPerMinute: number;
21
+ maxPayloadBytes: number;
22
+ };
23
+ logs: {
24
+ persist: boolean;
25
+ path?: string;
26
+ bufferSize: number;
27
+ };
28
+ tools: {
29
+ allow?: string[];
30
+ deny?: string[];
31
+ };
32
+ approval: McpApprovalMode;
33
+ }
34
+
35
+ export type McpApprovalMode = 'always' | 'once-per-tool' | 'once-per-server' | 'never';
36
+
37
+ export type McpServerStatus = 'stopped' | 'starting' | 'running' | 'error';
38
+
39
+ export interface McpServerState {
40
+ status: McpServerStatus;
41
+ pid?: number;
42
+ initLatencyMs?: number;
43
+ toolCount: number;
44
+ lastError?: string;
45
+ lastCallAt?: number;
46
+ }
47
+
48
+ export interface McpToolInfo {
49
+ serverId: string;
50
+ name: string;
51
+ description: string;
52
+ inputSchema: Record<string, unknown>;
53
+ canonicalId: string;
54
+ safeId: string;
55
+ }
56
+
57
+ export type McpRiskHint = 'read' | 'write' | 'execute' | 'network' | 'unknown';
58
+
59
+ export type McpApprovalScope = 'toolArgs' | 'tool' | 'server';
60
+
61
+ export interface McpApprovalCacheEntry {
62
+ scope: McpApprovalScope;
63
+ key: string;
64
+ expiresAt: number;
65
+ }
66
+
67
+ export interface McpGlobalConfig {
68
+ servers: McpServerConfig[];
69
+ }
70
+
71
+ export function toCanonicalId(serverId: string, toolName: string): string {
72
+ return `mcp:${serverId}:${toolName}`;
73
+ }
74
+
75
+ export function toSafeId(serverId: string, toolName: string): string {
76
+ return `mcp__${serverId}__${toolName}`;
77
+ }
78
+
79
+ export function parseSafeId(safeId: string): { serverId: string; toolName: string } | null {
80
+ if (!safeId.startsWith('mcp__')) return null;
81
+ const parts = safeId.slice(5).split('__');
82
+ if (parts.length < 2) return null;
83
+ const toolName = parts.pop()!;
84
+ const serverId = parts.join('__');
85
+ return { serverId, toolName };
86
+ }
87
+
88
+ export function parseCanonicalId(canonicalId: string): { serverId: string; toolName: string } | null {
89
+ if (!canonicalId.startsWith('mcp:')) return null;
90
+ const parts = canonicalId.slice(4).split(':');
91
+ if (parts.length < 2) return null;
92
+ const toolName = parts.pop()!;
93
+ const serverId = parts.join(':');
94
+ return { serverId, toolName };
95
+ }
@@ -1,12 +1,19 @@
1
1
  export interface ApprovalRequest {
2
2
  id: string;
3
- toolName: 'write' | 'edit' | 'bash';
3
+ toolName: string;
4
4
  preview: {
5
5
  title: string;
6
6
  content: string;
7
7
  details?: string[];
8
8
  };
9
9
  args: Record<string, unknown>;
10
+ mcpMeta?: {
11
+ serverId: string;
12
+ serverName: string;
13
+ canonicalId: string;
14
+ riskHint: string;
15
+ payloadSize: number;
16
+ };
10
17
  }
11
18
 
12
19
  export interface ApprovalResponse {
@@ -16,7 +23,7 @@ export interface ApprovalResponse {
16
23
  }
17
24
 
18
25
  export interface ApprovalAccepted {
19
- toolName: 'write' | 'edit' | 'bash';
26
+ toolName: string;
20
27
  args: Record<string, unknown>;
21
28
  }
22
29
 
@@ -28,6 +35,11 @@ let listeners = new Set<ApprovalListener>();
28
35
  let acceptedListeners = new Set<ApprovalAcceptedListener>();
29
36
  let pendingResolve: ((response: ApprovalResponse) => void) | null = null;
30
37
  let pendingReject: ((reason?: any) => void) | null = null;
38
+ let queuedRequests: {
39
+ request: ApprovalRequest;
40
+ resolve: (response: ApprovalResponse) => void;
41
+ reject: (reason?: any) => void;
42
+ }[] = [];
31
43
 
32
44
  function notify(): void {
33
45
  for (const listener of listeners) {
@@ -54,7 +66,7 @@ export function subscribeApprovalAccepted(listener: ApprovalAcceptedListener): (
54
66
  };
55
67
  }
56
68
 
57
- function notifyApprovalAccepted(toolName: 'write' | 'edit' | 'bash', args: Record<string, unknown>): void {
69
+ function notifyApprovalAccepted(toolName: string, args: Record<string, unknown>): void {
58
70
  for (const listener of acceptedListeners) {
59
71
  listener({ toolName, args });
60
72
  }
@@ -65,27 +77,32 @@ export function getCurrentApproval(): ApprovalRequest | null {
65
77
  }
66
78
 
67
79
  export async function requestApproval(
68
- toolName: 'write' | 'edit' | 'bash',
80
+ toolName: string,
69
81
  args: Record<string, unknown>,
70
82
  preview: { title: string; content: string; details?: string[] }
71
83
  ): Promise<{ approved: boolean; customResponse?: string }> {
72
- if (pendingResolve) {
73
- throw new Error('An approval request is already pending');
74
- }
84
+ const mcpMeta = (args as any).__mcpMeta;
85
+ const cleanArgs = { ...args };
86
+ delete (cleanArgs as any).__mcpMeta;
75
87
 
76
88
  const request: ApprovalRequest = {
77
89
  id: createId(),
78
90
  toolName,
79
91
  preview,
80
- args,
92
+ args: cleanArgs,
93
+ ...(mcpMeta && { mcpMeta }),
81
94
  };
82
95
 
83
- currentRequest = request;
84
- notify();
85
-
86
96
  const response = await new Promise<ApprovalResponse>((resolve, reject) => {
97
+ if (pendingResolve) {
98
+ queuedRequests.push({ request, resolve, reject });
99
+ return;
100
+ }
101
+
102
+ currentRequest = request;
87
103
  pendingResolve = resolve;
88
104
  pendingReject = reject;
105
+ notify();
89
106
  });
90
107
 
91
108
  return { approved: response.approved, customResponse: response.customResponse };
@@ -114,6 +131,14 @@ export function respondApproval(approved: boolean, customResponse?: string): voi
114
131
  }
115
132
 
116
133
  resolve(response);
134
+
135
+ const next = queuedRequests.shift();
136
+ if (next) {
137
+ currentRequest = next.request;
138
+ pendingResolve = next.resolve;
139
+ pendingReject = next.reject;
140
+ notify();
141
+ }
117
142
  }
118
143
 
119
144
  export function cancelApproval(): void {
@@ -126,4 +151,12 @@ export function cancelApproval(): void {
126
151
  notify();
127
152
 
128
153
  reject(new Error('Interrupted by user'));
129
- }
154
+
155
+ const next = queuedRequests.shift();
156
+ if (next) {
157
+ currentRequest = next.request;
158
+ pendingResolve = next.resolve;
159
+ pendingReject = next.reject;
160
+ notify();
161
+ }
162
+ }
@@ -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
+ }
@@ -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,30 @@
1
+ import type { Command, CommandResult } from './types';
2
+
3
+ export const compactCommand: Command = {
4
+ name: 'compact',
5
+ description: 'Compact the current conversation context',
6
+ usage: '/compact [maxTokens]',
7
+ execute: (args: string[]): CommandResult => {
8
+ let maxTokens: number | undefined;
9
+ if (args[0]) {
10
+ const parsed = Number(args[0]);
11
+ if (!Number.isFinite(parsed) || parsed <= 0) {
12
+ return {
13
+ success: false,
14
+ content: 'Invalid maxTokens. Usage: /compact [maxTokens]',
15
+ shouldAddToHistory: false
16
+ };
17
+ }
18
+ maxTokens = Math.floor(parsed);
19
+ }
20
+
21
+ return {
22
+ success: true,
23
+ content: 'Conversation compacted.',
24
+ shouldAddToHistory: false,
25
+ shouldCompactMessages: true,
26
+ compactMaxTokens: maxTokens,
27
+ shouldClearMessages: true
28
+ };
29
+ }
30
+ };
@@ -5,7 +5,7 @@ export const echoCommand: Command = {
5
5
  description: 'Echo the provided text back to the user',
6
6
  usage: '/echo <text>',
7
7
  aliases: ['e'],
8
- execute: (args: string[], fullCommand: string): { success: boolean; content: string } => {
8
+ execute: (args: string[], _fullCommand: string): { success: boolean; content: string } => {
9
9
  if (args.length === 0) {
10
10
  return {
11
11
  success: false,
@@ -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
+ };
@@ -3,10 +3,11 @@ import { commandRegistry } from './registry';
3
3
  import { echoCommand } from './echo';
4
4
  import { helpCommand } from './help';
5
5
  import { initCommand } from './init';
6
- import { undoCommand } from './undo';
7
- import { redoCommand } from './redo';
8
- import { sessionsCommand } from './sessions';
9
6
  import { webCommand } from './web';
7
+ import { imageCommand } from './image';
8
+ import { approvalsCommand } from './approvals';
9
+ import { newCommand } from './new';
10
+ import { compactCommand } from './compact';
10
11
 
11
12
  export { commandRegistry } from './registry';
12
13
  export type { Command, CommandResult, CommandRegistry } from './types';
@@ -61,8 +62,9 @@ export function initializeCommands(): void {
61
62
  commandRegistry.register(echoCommand);
62
63
  commandRegistry.register(helpCommand);
63
64
  commandRegistry.register(initCommand);
64
- commandRegistry.register(undoCommand);
65
- commandRegistry.register(redoCommand);
66
- commandRegistry.register(sessionsCommand);
67
65
  commandRegistry.register(webCommand);
68
- }
66
+ commandRegistry.register(imageCommand);
67
+ commandRegistry.register(approvalsCommand);
68
+ commandRegistry.register(newCommand);
69
+ commandRegistry.register(compactCommand);
70
+ }
@@ -0,0 +1,15 @@
1
+ import type { Command, CommandResult } from './types';
2
+
3
+ export const newCommand: Command = {
4
+ name: 'new',
5
+ description: 'Start a new chat',
6
+ usage: '/new',
7
+ aliases: ['clear'],
8
+ execute: (_args: string[], _fullCommand: string): CommandResult => {
9
+ return {
10
+ success: true,
11
+ content: 'Starting a new chat...',
12
+ shouldClearMessages: true
13
+ };
14
+ }
15
+ };
@@ -2,6 +2,9 @@ export interface CommandResult {
2
2
  success: boolean;
3
3
  content: string;
4
4
  shouldAddToHistory?: boolean;
5
+ shouldClearMessages?: boolean;
6
+ shouldCompactMessages?: boolean;
7
+ compactMaxTokens?: number;
5
8
  }
6
9
 
7
10
  export interface Command {
@@ -38,6 +38,8 @@ export interface MosaicConfig {
38
38
  model?: string;
39
39
  apiKey?: string;
40
40
  systemPrompt?: string;
41
+ maxSteps?: number;
42
+ maxContextTokens?: number;
41
43
  customProviders?: CustomProvider[];
42
44
  customModels?: { [providerId: string]: AIModel[] };
43
45
  requireApprovals?: boolean;
@@ -354,4 +356,4 @@ export function clearRecentProjects(): void {
354
356
  const config = readConfig();
355
357
  config.recentProjects = [];
356
358
  writeConfig(config);
357
- }
359
+ }