@kirosnn/mosaic 0.0.91 → 0.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +2 -2
- package/package.json +52 -47
- package/src/agent/prompts/systemPrompt.ts +198 -68
- package/src/agent/prompts/toolsPrompt.ts +217 -135
- package/src/agent/provider/anthropic.ts +19 -15
- package/src/agent/provider/google.ts +21 -17
- package/src/agent/provider/ollama.ts +80 -41
- package/src/agent/provider/openai.ts +107 -67
- package/src/agent/provider/reasoning.ts +29 -0
- package/src/agent/provider/xai.ts +19 -15
- package/src/agent/tools/definitions.ts +9 -5
- package/src/agent/tools/executor.ts +655 -46
- package/src/agent/tools/exploreExecutor.ts +12 -12
- package/src/agent/tools/fetch.ts +58 -0
- package/src/agent/tools/glob.ts +20 -4
- package/src/agent/tools/grep.ts +62 -8
- package/src/agent/tools/plan.ts +27 -0
- package/src/agent/tools/read.ts +2 -0
- package/src/agent/types.ts +6 -6
- package/src/components/App.tsx +67 -25
- package/src/components/CustomInput.tsx +274 -68
- package/src/components/Main.tsx +323 -168
- package/src/components/ShortcutsModal.tsx +11 -8
- package/src/components/main/ChatPage.tsx +217 -58
- package/src/components/main/HomePage.tsx +5 -1
- package/src/components/main/ThinkingIndicator.tsx +11 -1
- package/src/components/main/types.ts +11 -10
- package/src/index.tsx +3 -5
- package/src/utils/approvalBridge.ts +29 -8
- package/src/utils/approvalModeBridge.ts +17 -0
- package/src/utils/commands/approvals.ts +48 -0
- package/src/utils/commands/image.ts +109 -0
- package/src/utils/commands/index.ts +5 -1
- package/src/utils/diffRendering.tsx +13 -14
- package/src/utils/history.ts +82 -40
- package/src/utils/imageBridge.ts +28 -0
- package/src/utils/images.ts +31 -0
- package/src/utils/models.ts +0 -7
- package/src/utils/notificationBridge.ts +23 -0
- package/src/utils/toolFormatting.ts +162 -43
- package/src/web/app.tsx +94 -34
- package/src/web/assets/css/ChatPage.css +102 -30
- package/src/web/assets/css/MessageItem.css +26 -29
- package/src/web/assets/css/ThinkingIndicator.css +44 -6
- package/src/web/assets/css/ToolMessage.css +36 -14
- package/src/web/components/ChatPage.tsx +228 -105
- package/src/web/components/HomePage.tsx +6 -6
- package/src/web/components/MessageItem.tsx +88 -89
- package/src/web/components/Setup.tsx +1 -1
- package/src/web/components/Sidebar.tsx +1 -1
- package/src/web/components/ThinkingIndicator.tsx +40 -21
- package/src/web/router.ts +1 -1
- package/src/web/server.tsx +187 -39
- package/src/web/storage.ts +23 -1
- 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().
|
|
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('
|
|
134
|
-
path: z.string().
|
|
135
|
-
case_sensitive: z.boolean().
|
|
136
|
-
max_results: z.number().
|
|
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().
|
|
160
|
-
filter: z.string().
|
|
161
|
-
include_hidden: z.boolean().
|
|
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
|
+
});
|
package/src/agent/tools/glob.ts
CHANGED
|
@@ -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:
|
|
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
|
|
9
|
-
path: z.string().
|
|
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
|
|
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
|
},
|
package/src/agent/tools/grep.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
+
});
|
package/src/agent/tools/read.ts
CHANGED
|
@@ -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);
|
package/src/agent/types.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/components/App.tsx
CHANGED
|
@@ -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
|
|
48
|
-
await execAsync(`powershell -
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 && (
|
|
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;
|