@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
@@ -7,7 +7,7 @@ import { createXai } from '@ai-sdk/xai';
7
7
  import { z } from 'zod';
8
8
  import { readConfig } from '../../utils/config';
9
9
  import { executeTool } from './executor';
10
- import { getExploreAbortSignal, isExploreAborted, notifyExploreTool } from '../../utils/exploreBridge';
10
+ import { getExploreAbortSignal, isExploreAborted, notifyExploreTool, getExploreContext } from '../../utils/exploreBridge';
11
11
 
12
12
  interface ExploreLog {
13
13
  tool: string;
@@ -36,7 +36,7 @@ IMPORTANT RULES:
36
36
  5. Summarize findings clearly and include relevant file paths and code snippets
37
37
  6. You MUST call the "done" tool when finished - this is the only way to complete the exploration`;
38
38
 
39
- const MAX_STEPS = 50;
39
+ const MAX_STEPS = 100;
40
40
 
41
41
  interface ExploreResult {
42
42
  success: boolean;
@@ -78,13 +78,6 @@ function createModelProvider(config: { provider: string; model: string; apiKey?:
78
78
 
79
79
  let exploreDoneResult: string | null = null;
80
80
 
81
- function getResultPreview(result: string | undefined): string {
82
- if (!result) return '';
83
- const lines = result.split('\n');
84
- if (lines.length <= 3) return result.substring(0, 200);
85
- return lines.slice(0, 3).join('\n').substring(0, 200) + '...';
86
- }
87
-
88
81
  function createExploreTools() {
89
82
  return {
90
83
  read: createTool({
@@ -104,10 +97,10 @@ function createExploreTools() {
104
97
  },
105
98
  }),
106
99
  glob: createTool({
107
- description: 'Find files matching a glob pattern',
100
+ description: 'Find files matching a glob pattern. Do NOT use this to list directory contents (use "list" instead).',
108
101
  parameters: z.object({
109
102
  pattern: z.string().describe('Glob pattern to match files (e.g., "**/*.ts", "src/**/*.tsx")'),
110
- path: z.string().optional().describe('Directory to search in (default: workspace root)'),
103
+ path: z.string().describe('Directory to search in (use "." for workspace root)'),
111
104
  }),
112
105
  execute: async (args) => {
113
106
  if (isExploreAborted()) return { error: 'Exploration aborted' };
@@ -127,17 +120,17 @@ function createExploreTools() {
127
120
  },
128
121
  }),
129
122
  grep: createTool({
130
- description: 'Search for text content within files',
123
+ description: 'Search for text content within files using regular expressions',
131
124
  parameters: z.object({
132
125
  pattern: z.string().describe('Glob pattern to match files'),
133
- query: z.string().describe('Text to search for'),
134
- path: z.string().optional().describe('Directory to search in'),
135
- case_sensitive: z.boolean().optional().describe('Case-sensitive search'),
136
- max_results: z.number().optional().describe('Maximum results'),
126
+ query: z.string().describe('Regular expression pattern to search for'),
127
+ path: z.string().describe('Directory to search in (use "." for workspace root)'),
128
+ case_sensitive: z.boolean().describe('Case-sensitive search (pass false for default)'),
129
+ max_results: z.number().describe('Maximum results (pass 50 for default)'),
137
130
  }),
138
131
  execute: async (args) => {
139
132
  if (isExploreAborted()) return { error: 'Exploration aborted' };
140
- const result = await executeTool('grep', args);
133
+ const result = await executeTool('grep', { ...args, regex: true });
141
134
  const resultLen = result.result?.length || 0;
142
135
  let preview = result.error || 'error';
143
136
  if (result.success && result.result) {
@@ -156,9 +149,9 @@ function createExploreTools() {
156
149
  description: 'List files and directories',
157
150
  parameters: z.object({
158
151
  path: z.string().describe('Path to list'),
159
- recursive: z.boolean().optional().describe('List recursively'),
160
- filter: z.string().optional().describe('Filter pattern'),
161
- include_hidden: z.boolean().optional().describe('Include hidden files'),
152
+ recursive: z.boolean().describe('List recursively (pass false for default)'),
153
+ filter: z.string().describe('Filter pattern (pass empty string for no filter)'),
154
+ include_hidden: z.boolean().describe('Include hidden files (pass false for default)'),
162
155
  }),
163
156
  execute: async (args) => {
164
157
  if (isExploreAborted()) return { error: 'Exploration aborted' };
@@ -196,7 +189,7 @@ function formatExploreLogs(): string {
196
189
  const lines = ['Tools used:'];
197
190
  for (const log of exploreLogs) {
198
191
  const argStr = log.args.path || log.args.pattern || log.args.query || '';
199
- const status = log.success ? '+' : '-';
192
+ const status = log.success ? '' : '-';
200
193
  lines.push(` ${status} ${log.tool}(${argStr}) -> ${log.resultPreview || 'ok'}`);
201
194
  }
202
195
  return lines.join('\n');
@@ -240,6 +233,11 @@ export async function executeExploreTool(purpose: string): Promise<ExploreResult
240
233
 
241
234
  const tools = createExploreTools();
242
235
 
236
+ const parentContext = getExploreContext();
237
+ const systemPrompt = parentContext
238
+ ? `${EXPLORE_SYSTEM_PROMPT}\n\nCONTEXT FROM PARENT CONVERSATION:\n${parentContext}`
239
+ : EXPLORE_SYSTEM_PROMPT;
240
+
243
241
  const result = streamText({
244
242
  model,
245
243
  messages: [
@@ -248,7 +246,7 @@ export async function executeExploreTool(purpose: string): Promise<ExploreResult
248
246
  content: `Explore the codebase to: ${purpose}`,
249
247
  },
250
248
  ],
251
- system: EXPLORE_SYSTEM_PROMPT,
249
+ system: systemPrompt,
252
250
  tools,
253
251
  maxSteps: MAX_STEPS,
254
252
  abortSignal,
@@ -0,0 +1,58 @@
1
+ import { tool, type CoreTool } from 'ai';
2
+ import { z } from 'zod';
3
+ import { executeTool } from './executor';
4
+
5
+ export const fetch: CoreTool = tool({
6
+ description: `Fetches a URL from the internet and extracts its contents as markdown.
7
+
8
+ This tool allows you to retrieve web content and process it for analysis. HTML pages are automatically converted to clean markdown format for easier reading.
9
+
10
+ Features:
11
+ - Automatic HTML to Markdown conversion with Readability
12
+ - Pagination support for large pages (use start_index to continue reading)
13
+ - Raw HTML retrieval option
14
+ - Link and image URL resolution to absolute paths
15
+ - Configurable content length limits
16
+
17
+ Use cases:
18
+ - Reading documentation and articles
19
+ - Fetching API responses (JSON, XML, etc.)
20
+ - Researching information from the web
21
+ - Analyzing web page content`,
22
+ parameters: z.object({
23
+ url: z.string().describe('The URL to fetch (must be a valid HTTP/HTTPS URL)'),
24
+ max_length: z
25
+ .number()
26
+ .int()
27
+ .positive()
28
+ .max(100000)
29
+ .nullable()
30
+ .optional()
31
+ .describe('Maximum number of characters to return (default: 10000, max: 100000)'),
32
+ start_index: z
33
+ .number()
34
+ .int()
35
+ .nonnegative()
36
+ .nullable()
37
+ .optional()
38
+ .describe('Character index to start reading from. Use this to paginate through large pages when content is truncated.'),
39
+ raw: z
40
+ .boolean()
41
+ .nullable()
42
+ .optional()
43
+ .describe('If true, return raw HTML instead of converting to markdown'),
44
+ timeout: z
45
+ .number()
46
+ .int()
47
+ .positive()
48
+ .max(60000)
49
+ .nullable()
50
+ .optional()
51
+ .describe('Request timeout in milliseconds (default: 30000, max: 60000)'),
52
+ }),
53
+ execute: async (args) => {
54
+ const result = await executeTool('fetch', args);
55
+ if (!result.success) return { error: result.error || 'Unknown error occurred' };
56
+ return result.result;
57
+ },
58
+ });
@@ -3,13 +3,29 @@ import { z } from 'zod';
3
3
  import { executeTool } from './executor';
4
4
 
5
5
  export const glob: CoreTool = tool({
6
- description: 'Find files matching a glob pattern. Fast pattern-based file discovery.',
6
+ description: `Find files matching a glob pattern. Fast pattern-based file discovery.
7
+
8
+ IMPORTANT: Use "**/" prefix to search recursively in all subdirectories.
9
+ - "*.ts" only matches files in the current directory
10
+ - "**/*.ts" matches files in ALL subdirectories (usually what you want)
11
+
12
+ Note: Do not use this to simply list files in a directory; use the 'list' tool for that. This is for finding specific files by pattern.
13
+
14
+ Examples:
15
+ - glob(pattern="**/*.ts") - All TypeScript files
16
+ - glob(pattern="**/*.tsx") - All React components
17
+ - glob(pattern="**/package.json") - All package.json files
18
+ - glob(pattern="src/**/*.js") - All JS files in src/`,
7
19
  parameters: z.object({
8
- pattern: z.string().describe('Glob pattern to match files (e.g., "*.ts", "**/*.tsx", "src/**/*.js")'),
9
- path: z.string().nullable().optional().describe('Directory to search in (use null for workspace root)'),
20
+ pattern: z.string().describe('Glob pattern (use **/ for recursive search, e.g., "**/*.ts")'),
21
+ path: z.string().optional().describe('Directory to search in (defaults to workspace root)'),
10
22
  }),
11
23
  execute: async (args) => {
12
- const result = await executeTool('glob', args);
24
+ const cleanArgs = {
25
+ pattern: args.pattern,
26
+ path: args.path && args.path !== 'null' ? args.path : undefined,
27
+ };
28
+ const result = await executeTool('glob', cleanArgs);
13
29
  if (!result.success) return { error: result.error || 'Unknown error occurred' };
14
30
  return result.result;
15
31
  },
@@ -2,18 +2,73 @@ import { tool, type CoreTool } from 'ai';
2
2
  import { z } from 'zod';
3
3
  import { executeTool } from './executor';
4
4
 
5
+ const FILE_TYPE_EXTENSIONS: Record<string, string[]> = {
6
+ ts: ['.ts', '.tsx', '.mts', '.cts'],
7
+ js: ['.js', '.jsx', '.mjs', '.cjs'],
8
+ py: ['.py', '.pyw', '.pyi'],
9
+ java: ['.java'],
10
+ go: ['.go'],
11
+ rust: ['.rs'],
12
+ c: ['.c', '.h'],
13
+ cpp: ['.cpp', '.cc', '.cxx', '.hpp', '.hh', '.hxx'],
14
+ cs: ['.cs'],
15
+ rb: ['.rb', '.rake', '.gemspec'],
16
+ php: ['.php', '.phtml'],
17
+ swift: ['.swift'],
18
+ kt: ['.kt', '.kts'],
19
+ scala: ['.scala'],
20
+ html: ['.html', '.htm', '.xhtml'],
21
+ css: ['.css', '.scss', '.sass', '.less'],
22
+ json: ['.json', '.jsonc'],
23
+ yaml: ['.yaml', '.yml'],
24
+ md: ['.md', '.markdown'],
25
+ txt: ['.txt'],
26
+ sh: ['.sh', '.bash', '.zsh'],
27
+ sql: ['.sql'],
28
+ vue: ['.vue'],
29
+ svelte: ['.svelte'],
30
+ };
31
+
32
+ export { FILE_TYPE_EXTENSIONS };
33
+
5
34
  export const grep: CoreTool = tool({
6
- description: 'Search for text content within files matching a glob pattern. Combines pattern matching with content search.',
7
- parameters: z.object({
8
- pattern: z.string().describe('Glob pattern to match files (e.g., "*.ts", "**/*.tsx", "src/**/*.js")'),
9
- query: z.string().describe('Text content to search for within the matched files'),
10
- path: z.string().nullable().optional().describe('Directory to search in (use null for workspace root)'),
11
- case_sensitive: z.boolean().nullable().optional().describe('Whether text search should be case-sensitive (use null for false)'),
12
- max_results: z.number().nullable().optional().describe('Maximum number of results to return (use null for 100)'),
35
+ description: `Search for text within files using regular expressions.
36
+
37
+ RECOMMENDED: Use file_type for best results (automatically searches all subdirectories).
38
+
39
+ Examples:
40
+ - grep(query="interface User", file_type="ts") - Search in TypeScript files
41
+ - grep(query="export function", file_type="tsx") - Search in React files
42
+ - grep(query="TODO") - Search in all files
43
+ - grep(query="class.*Component", file_type="ts") - Reuse regex search
44
+ - grep(query="handleClick", output_mode="files") - Just list matching files`,
45
+ parameters: z.object({
46
+ query: z.string().describe('Regular expression pattern to search for'),
47
+ file_type: z.string().optional().describe('File type or extension (e.g. ts, tsx, js, txt, .env). Unknown types are treated as extensions.'),
48
+ pattern: z.string().optional().describe('Glob pattern for files (e.g., "**/*.config.ts"). Usually file_type is easier.'),
49
+ path: z.string().optional().describe('Directory to search (defaults to workspace root)'),
50
+ case_sensitive: z.boolean().optional().describe('Case-sensitive (default: false)'),
51
+ whole_word: z.boolean().optional().describe('Match whole words only (default: false)'),
52
+ context: z.number().optional().describe('Lines of context around matches (default: 0)'),
53
+ max_results: z.number().optional().describe('Max results (default: 500)'),
54
+ output_mode: z.enum(['matches', 'files', 'count']).optional().describe('"matches" (default), "files", or "count"'),
55
+ exclude_pattern: z.string().optional().describe('Glob pattern to exclude'),
13
56
  }),
14
57
  execute: async (args) => {
15
- const result = await executeTool('grep', args);
58
+ const cleanArgs: Record<string, unknown> = { query: args.query, regex: true };
59
+
60
+ if (args.file_type && args.file_type !== 'null') cleanArgs.file_type = args.file_type;
61
+ if (args.pattern && args.pattern !== 'null') cleanArgs.pattern = args.pattern;
62
+ if (args.path && args.path !== 'null') cleanArgs.path = args.path;
63
+ if (args.case_sensitive !== undefined && args.case_sensitive !== null) cleanArgs.case_sensitive = args.case_sensitive;
64
+ if (args.whole_word !== undefined && args.whole_word !== null) cleanArgs.whole_word = args.whole_word;
65
+ if (args.context !== undefined && args.context !== null) cleanArgs.context = args.context;
66
+ if (args.max_results !== undefined && args.max_results !== null) cleanArgs.max_results = args.max_results;
67
+ if (args.output_mode && (args.output_mode as string) !== 'null') cleanArgs.output_mode = args.output_mode;
68
+ if (args.exclude_pattern && args.exclude_pattern !== 'null') cleanArgs.exclude_pattern = args.exclude_pattern;
69
+
70
+ const result = await executeTool('grep', cleanArgs);
16
71
  if (!result.success) return { error: result.error || 'Unknown error occurred' };
17
72
  return result.result;
18
73
  },
19
- });
74
+ });
@@ -0,0 +1,27 @@
1
+ import { tool, type CoreTool } from 'ai';
2
+ import { z } from 'zod';
3
+
4
+ const PlanStepSchema = z.object({
5
+ step: z.string().describe('A short, specific action item.'),
6
+ status: z.enum(['pending', 'in_progress', 'completed']).describe('Current status of the step.'),
7
+ });
8
+
9
+ export const plan: CoreTool = tool({
10
+ description: 'Create or update a task plan for longer work, and keep it up to date as you progress. Update the plan after each step.',
11
+ parameters: z.object({
12
+ explanation: z.string().optional().describe('Optional context about the plan or changes.'),
13
+ plan: z.array(PlanStepSchema).min(1).describe('Ordered list of steps with statuses.'),
14
+ }),
15
+ execute: async (args) => {
16
+ const rawPlan = Array.isArray(args.plan) ? args.plan : [];
17
+ const normalizedPlan = rawPlan
18
+ .map((item) => ({
19
+ step: typeof item.step === 'string' ? item.step : '',
20
+ status: item.status === 'completed' || item.status === 'in_progress' ? item.status : 'pending',
21
+ }))
22
+ .filter((item) => item.step.trim().length > 0);
23
+
24
+ const explanation = typeof args.explanation === 'string' ? args.explanation : undefined;
25
+ return { explanation, plan: normalizedPlan };
26
+ },
27
+ });
@@ -10,11 +10,17 @@ export const question: CoreTool = tool({
10
10
  z.object({
11
11
  label: z.string().describe('The option label shown to the user.'),
12
12
  value: z.string().nullable().optional().describe('Optional value returned for the selected option. Use null if not needed.'),
13
+ group: z.string().optional().describe('Optional group name. Consecutive options with the same group are displayed under a shared header.'),
13
14
  })
14
15
  ).describe('List of options the user can pick from. A text input field is automatically displayed below the options where the user can type a custom response instead.'),
16
+ timeout: z.number().optional().describe('Optional timeout in seconds. The question is automatically rejected when time runs out.'),
17
+ validation: z.object({
18
+ pattern: z.string().describe('Regex pattern the custom text must match.'),
19
+ message: z.string().optional().describe('Error message shown when validation fails.'),
20
+ }).optional().describe('Optional validation for the custom text input.'),
15
21
  }),
16
22
  execute: async (args) => {
17
- const answer = await askQuestion(args.prompt, args.options);
23
+ const answer = await askQuestion(args.prompt, args.options, args.timeout, args.validation);
18
24
  return answer;
19
25
  },
20
26
  });
@@ -6,6 +6,8 @@ export const read: CoreTool = tool({
6
6
  description: 'Read the contents of a file from the workspace',
7
7
  parameters: z.object({
8
8
  path: z.string().describe('The path to the file relative to the workspace root'),
9
+ start_line: z.number().optional().describe('The line number to start reading from (1-based)'),
10
+ end_line: z.number().optional().describe('The line number to end reading at (1-based)'),
9
11
  }),
10
12
  execute: async (args) => {
11
13
  const result = await executeTool('read', args);
@@ -1,4 +1,4 @@
1
- import { CoreMessage, CoreTool } from 'ai';
1
+ import { CoreMessage, CoreTool, UserContent } from 'ai';
2
2
 
3
3
  export type AgentEventType =
4
4
  | 'text-delta'
@@ -117,14 +117,15 @@ export type AgentEvent =
117
117
  | FinishEvent
118
118
  | ErrorEvent;
119
119
 
120
- export interface ProviderConfig {
121
- provider: string;
122
- model: string;
123
- apiKey?: string;
124
- systemPrompt: string;
125
- tools?: Record<string, CoreTool>;
126
- maxSteps?: number;
127
- }
120
+ export interface ProviderConfig {
121
+ provider: string;
122
+ model: string;
123
+ apiKey?: string;
124
+ systemPrompt: string;
125
+ tools?: Record<string, CoreTool>;
126
+ maxSteps?: number;
127
+ maxContextTokens?: number;
128
+ }
128
129
 
129
130
  export interface AgentConfig {
130
131
  maxSteps?: number;
@@ -137,10 +138,10 @@ export interface AgentContext {
137
138
  config: AgentConfig;
138
139
  }
139
140
 
140
- export interface AgentMessage {
141
- role: 'user' | 'assistant';
142
- content: string;
143
- }
141
+ export interface AgentMessage {
142
+ role: 'user' | 'assistant';
143
+ content: string | UserContent;
144
+ }
144
145
 
145
146
  export interface ProviderSendOptions {
146
147
  abortSignal?: AbortSignal;
@@ -152,4 +153,4 @@ export interface Provider {
152
153
  config: ProviderConfig,
153
154
  options?: ProviderSendOptions
154
155
  ): AsyncGenerator<AgentEvent>;
155
- }
156
+ }
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useCallback } from 'react';
1
+ import { useState, useEffect, useCallback, useRef } from 'react';
2
2
  import type { KeyEvent } from '@opentui/core';
3
3
  import { isFirstRun, markFirstRunComplete } from '../utils/config';
4
4
  import { useRenderer } from '@opentui/react';
@@ -10,6 +10,10 @@ import { CommandModal } from './CommandsModal';
10
10
  import { Notification, type NotificationData } from './Notification';
11
11
  import { exec } from 'child_process';
12
12
  import { promisify } from 'util';
13
+ import { subscribeNotifications } from '../utils/notificationBridge';
14
+ import { shouldRequireApprovals, setRequireApprovals } from '../utils/config';
15
+ import { getCurrentApproval, respondApproval } from '../utils/approvalBridge';
16
+ import { emitApprovalMode } from '../utils/approvalModeBridge';
13
17
 
14
18
  const execAsync = promisify(exec);
15
19
 
@@ -28,7 +32,8 @@ export function App({ initialMessage }: AppProps) {
28
32
  const [shortcutsTab, setShortcutsTab] = useState<0 | 1>(0);
29
33
  const [commandsOpen, setCommandsOpen] = useState(false);
30
34
  const [notifications, setNotifications] = useState<NotificationData[]>([]);
31
- const [pendingMessage, setPendingMessage] = useState<string | undefined>(initialMessage);
35
+ const [pendingMessage] = useState<string | undefined>(initialMessage);
36
+ const lastSelectionRef = useRef<{ text: string; at: number } | null>(null);
32
37
 
33
38
  const renderer = useRenderer();
34
39
 
@@ -41,11 +46,11 @@ export function App({ initialMessage }: AppProps) {
41
46
  setNotifications(prev => prev.filter(n => n.id !== id));
42
47
  }, []);
43
48
 
44
- const copyToClipboard = async (text: string) => {
49
+ const copyToClipboard = useCallback(async (text: string) => {
45
50
  try {
46
51
  if (process.platform === 'win32') {
47
- const escaped = text.replace(/'/g, "''");
48
- await execAsync(`powershell -command "Set-Clipboard -Value '${escaped}'"`);
52
+ const encoded = Buffer.from(text, 'utf8').toString('base64');
53
+ await execAsync(`powershell.exe -NoProfile -Command "$bytes=[Convert]::FromBase64String('${encoded}');$text=[System.Text.Encoding]::UTF8.GetString($bytes);Set-Clipboard -Value $text"`);
49
54
  } else if (process.platform === 'darwin') {
50
55
  await execAsync(`echo ${JSON.stringify(text)} | pbcopy`);
51
56
  } else {
@@ -54,7 +59,33 @@ export function App({ initialMessage }: AppProps) {
54
59
  } catch (error) {
55
60
  console.error('Failed to copy to clipboard:', error);
56
61
  }
57
- };
62
+ }, []);
63
+
64
+ useEffect(() => {
65
+ const handler = (selection: any) => {
66
+ if (!selection || selection.isSelecting || !selection.isActive) return;
67
+ const text = typeof selection.getSelectedText === 'function' ? selection.getSelectedText() : '';
68
+ if (!text) return;
69
+ const now = Date.now();
70
+ const last = lastSelectionRef.current;
71
+ if (last && last.text === text && now - last.at < 400) return;
72
+ lastSelectionRef.current = { text, at: now };
73
+ copyToClipboard(text);
74
+ addNotification('Copied to clipboard', 'info', 2000);
75
+ };
76
+
77
+ const rendererAny = renderer as any;
78
+ rendererAny.on?.('selection', handler);
79
+ return () => {
80
+ rendererAny.off?.('selection', handler);
81
+ };
82
+ }, [renderer, copyToClipboard, addNotification]);
83
+
84
+ useEffect(() => {
85
+ return subscribeNotifications((payload) => {
86
+ addNotification(payload.message, payload.type ?? 'info', payload.duration);
87
+ });
88
+ }, [addNotification]);
58
89
 
59
90
  useEffect(() => {
60
91
  const isDarwin = process.platform === 'darwin';
@@ -68,6 +99,17 @@ export function App({ initialMessage }: AppProps) {
68
99
  return;
69
100
  }
70
101
 
102
+ if (k.name === 'tab' && k.shift) {
103
+ const next = !shouldRequireApprovals();
104
+ setRequireApprovals(next);
105
+ if (!next && getCurrentApproval()) {
106
+ respondApproval(true);
107
+ }
108
+ emitApprovalMode(next);
109
+ addNotification(next ? 'Approvals enabled' : 'Auto-approve enabled', 'info', 2500);
110
+ return;
111
+ }
112
+
71
113
  if (shortcutsOpen && (k.name === 'f1' || k.name === 'f2')) {
72
114
  setShortcutsTab(k.name === 'f2' ? 1 : 0);
73
115
  return;
@@ -90,7 +132,7 @@ export function App({ initialMessage }: AppProps) {
90
132
  return;
91
133
  }
92
134
 
93
- if (k.name === 'c' && !k.shift && (k.ctrl || (isDarwin && k.meta && !k.alt) || (!isDarwin && (k.alt || k.meta) && !k.ctrl)) || seq === '\x03') {
135
+ if (k.name === 'c' && !k.shift && ((isDarwin && k.meta && !k.ctrl && !k.alt) || (!isDarwin && k.alt && !k.ctrl))) {
94
136
  setCopyRequestId(prev => prev + 1);
95
137
  return;
96
138
  }
@@ -171,4 +213,4 @@ export function App({ initialMessage }: AppProps) {
171
213
  );
172
214
  }
173
215
 
174
- export default App;
216
+ export default App;