@kirosnn/mosaic 0.0.9 → 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 +83 -19
  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
@@ -104,10 +104,10 @@ function createExploreTools() {
104
104
  },
105
105
  }),
106
106
  glob: createTool({
107
- description: 'Find files matching a glob pattern',
107
+ description: 'Find files matching a glob pattern. Do NOT use this to list directory contents (use "list" instead).',
108
108
  parameters: z.object({
109
109
  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)'),
110
+ path: z.string().describe('Directory to search in (use "." for workspace root)'),
111
111
  }),
112
112
  execute: async (args) => {
113
113
  if (isExploreAborted()) return { error: 'Exploration aborted' };
@@ -127,17 +127,17 @@ function createExploreTools() {
127
127
  },
128
128
  }),
129
129
  grep: createTool({
130
- description: 'Search for text content within files',
130
+ description: 'Search for text content within files using regular expressions',
131
131
  parameters: z.object({
132
132
  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'),
133
+ query: z.string().describe('Regular expression pattern to search for'),
134
+ path: z.string().describe('Directory to search in (use "." for workspace root)'),
135
+ case_sensitive: z.boolean().describe('Case-sensitive search (pass false for default)'),
136
+ max_results: z.number().describe('Maximum results (pass 50 for default)'),
137
137
  }),
138
138
  execute: async (args) => {
139
139
  if (isExploreAborted()) return { error: 'Exploration aborted' };
140
- const result = await executeTool('grep', args);
140
+ const result = await executeTool('grep', { ...args, regex: true });
141
141
  const resultLen = result.result?.length || 0;
142
142
  let preview = result.error || 'error';
143
143
  if (result.success && result.result) {
@@ -156,9 +156,9 @@ function createExploreTools() {
156
156
  description: 'List files and directories',
157
157
  parameters: z.object({
158
158
  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'),
159
+ recursive: z.boolean().describe('List recursively (pass false for default)'),
160
+ filter: z.string().describe('Filter pattern (pass empty string for no filter)'),
161
+ include_hidden: z.boolean().describe('Include hidden files (pass false for default)'),
162
162
  }),
163
163
  execute: async (args) => {
164
164
  if (isExploreAborted()) return { error: 'Exploration aborted' };
@@ -196,7 +196,7 @@ function formatExploreLogs(): string {
196
196
  const lines = ['Tools used:'];
197
197
  for (const log of exploreLogs) {
198
198
  const argStr = log.args.path || log.args.pattern || log.args.query || '';
199
- const status = log.success ? '+' : '-';
199
+ const status = log.success ? '' : '-';
200
200
  lines.push(` ${status} ${log.tool}(${argStr}) -> ${log.resultPreview || 'ok'}`);
201
201
  }
202
202
  return lines.join('\n');
@@ -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,72 @@ 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
+ sh: ['.sh', '.bash', '.zsh'],
26
+ sql: ['.sql'],
27
+ vue: ['.vue'],
28
+ svelte: ['.svelte'],
29
+ };
30
+
31
+ export { FILE_TYPE_EXTENSIONS };
32
+
5
33
  export const grep: CoreTool = tool({
6
- description: 'Search for text content within files matching a glob pattern. Combines pattern matching with content search.',
34
+ description: `Search for text within files using regular expressions.
35
+
36
+ RECOMMENDED: Use file_type for best results (automatically searches all subdirectories).
37
+
38
+ Examples:
39
+ - grep(query="interface User", file_type="ts") - Search in TypeScript files
40
+ - grep(query="export function", file_type="tsx") - Search in React files
41
+ - grep(query="TODO") - Search in all files
42
+ - grep(query="class.*Component", file_type="ts") - Reuse regex search
43
+ - grep(query="handleClick", output_mode="files") - Just list matching files`,
7
44
  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)'),
45
+ query: z.string().describe('Regular expression pattern to search for'),
46
+ file_type: z.string().optional().describe('File type: ts, js, tsx, jsx, py, java, go, rust, c, cpp, rb, php, json, yaml, md, html, css'),
47
+ pattern: z.string().optional().describe('Glob pattern for files (e.g., "**/*.config.ts"). Usually file_type is easier.'),
48
+ path: z.string().optional().describe('Directory to search (defaults to workspace root)'),
49
+ case_sensitive: z.boolean().optional().describe('Case-sensitive (default: false)'),
50
+ whole_word: z.boolean().optional().describe('Match whole words only (default: false)'),
51
+ context: z.number().optional().describe('Lines of context around matches (default: 0)'),
52
+ max_results: z.number().optional().describe('Max results (default: 500)'),
53
+ output_mode: z.enum(['matches', 'files', 'count']).optional().describe('"matches" (default), "files", or "count"'),
54
+ exclude_pattern: z.string().optional().describe('Glob pattern to exclude'),
13
55
  }),
14
56
  execute: async (args) => {
15
- const result = await executeTool('grep', args);
57
+ const cleanArgs: Record<string, unknown> = { query: args.query, regex: true };
58
+
59
+ if (args.file_type && args.file_type !== 'null') cleanArgs.file_type = args.file_type;
60
+ if (args.pattern && args.pattern !== 'null') cleanArgs.pattern = args.pattern;
61
+ if (args.path && args.path !== 'null') cleanArgs.path = args.path;
62
+ if (args.case_sensitive !== undefined && args.case_sensitive !== null) cleanArgs.case_sensitive = args.case_sensitive;
63
+ if (args.whole_word !== undefined && args.whole_word !== null) cleanArgs.whole_word = args.whole_word;
64
+ if (args.context !== undefined && args.context !== null) cleanArgs.context = args.context;
65
+ if (args.max_results !== undefined && args.max_results !== null) cleanArgs.max_results = args.max_results;
66
+ if (args.output_mode && (args.output_mode as string) !== 'null') cleanArgs.output_mode = args.output_mode;
67
+ if (args.exclude_pattern && args.exclude_pattern !== 'null') cleanArgs.exclude_pattern = args.exclude_pattern;
68
+
69
+ const result = await executeTool('grep', cleanArgs);
16
70
  if (!result.success) return { error: result.error || 'Unknown error occurred' };
17
71
  return result.result;
18
72
  },
19
- });
73
+ });
@@ -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
+ });
@@ -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'
@@ -137,10 +137,10 @@ export interface AgentContext {
137
137
  config: AgentConfig;
138
138
  }
139
139
 
140
- export interface AgentMessage {
141
- role: 'user' | 'assistant';
142
- content: string;
143
- }
140
+ export interface AgentMessage {
141
+ role: 'user' | 'assistant';
142
+ content: string | UserContent;
143
+ }
144
144
 
145
145
  export interface ProviderSendOptions {
146
146
  abortSignal?: AbortSignal;
@@ -152,4 +152,4 @@ export interface Provider {
152
152
  config: ProviderConfig,
153
153
  options?: ProviderSendOptions
154
154
  ): AsyncGenerator<AgentEvent>;
155
- }
155
+ }
@@ -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';
@@ -7,9 +7,13 @@ import { Setup } from './Setup';
7
7
  import { Main } from './Main';
8
8
  import { ShortcutsModal } from './ShortcutsModal';
9
9
  import { CommandModal } from './CommandsModal';
10
- import { Notification, type NotificationData } from './Notification';
11
- import { exec } from 'child_process';
12
- import { promisify } from 'util';
10
+ import { Notification, type NotificationData } from './Notification';
11
+ import { exec } from 'child_process';
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
 
@@ -29,6 +33,7 @@ export function App({ initialMessage }: AppProps) {
29
33
  const [commandsOpen, setCommandsOpen] = useState(false);
30
34
  const [notifications, setNotifications] = useState<NotificationData[]>([]);
31
35
  const [pendingMessage, setPendingMessage] = 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,24 +59,61 @@ export function App({ initialMessage }: AppProps) {
54
59
  } catch (error) {
55
60
  console.error('Failed to copy to clipboard:', error);
56
61
  }
57
- };
58
-
59
- useEffect(() => {
60
- const isDarwin = process.platform === 'darwin';
61
-
62
- const handleKeyPress = (key: KeyEvent) => {
63
- const k = key as any;
62
+ }, []);
64
63
 
65
- if (k.name === 'escape') {
66
- setShortcutsOpen(false);
67
- setCommandsOpen(false);
68
- return;
69
- }
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
+ };
70
76
 
71
- if (shortcutsOpen && (k.name === 'f1' || k.name === 'f2')) {
72
- setShortcutsTab(k.name === 'f2' ? 1 : 0);
73
- return;
74
- }
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]);
89
+
90
+ useEffect(() => {
91
+ const isDarwin = process.platform === 'darwin';
92
+
93
+ const handleKeyPress = (key: KeyEvent) => {
94
+ const k = key as any;
95
+
96
+ if (k.name === 'escape') {
97
+ setShortcutsOpen(false);
98
+ setCommandsOpen(false);
99
+ return;
100
+ }
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
+
113
+ if (shortcutsOpen && (k.name === 'f1' || k.name === 'f2')) {
114
+ setShortcutsTab(k.name === 'f2' ? 1 : 0);
115
+ return;
116
+ }
75
117
 
76
118
  const seq = k.sequence;
77
119
 
@@ -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;