Skip to content

Commit 1bb5131

Browse files
authored
feat: implement comprehensive edit tools toolset (#2613)
Add a complete set of editing tools for enhanced AI integration: ## New Tools Implemented - **Task**: Launch specialized agents for complex multi-step tasks - **Bash**: Execute terminal commands through sandbox session - **Glob**: File pattern matching using find commands - **Grep**: Text search using ripgrep with full option support - **LS**: Directory listing with ignore patterns - **Read**: File reading with line numbers and pagination - **Edit**: String replacement in files with safety checks - **MultiEdit**: Batch edits in a single file operation - **Write**: File creation and overwriting - **NotebookRead/Edit**: Jupyter notebook cell manipulation - **WebFetch/Search**: Web content fetching and search - **TodoWrite**: Task list management - **ExitPlanMode**: Planning mode exit utility ## Key Features ✅ Full sandbox integration - no direct Node.js API usage ✅ TypeScript support with Zod validation schemas ✅ Comprehensive error handling and safety checks ✅ Seamless integration with existing Onlook tools ✅ Build-tested and type-safe ## Files - `src/components/edit-tools.ts` - Main implementation - `src/components/edit-tools/index.ts` - Export utilities - `src/components/tools.ts` - Integration layer
1 parent ab8c4e0 commit 1bb5131

File tree

31 files changed

+1185
-430
lines changed

31 files changed

+1185
-430
lines changed

apps/web/client/public/onlook-preload-script.js

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/web/client/src/app/api/chat/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { env } from '@/env';
22
import { createClient as createTRPCClient } from '@/trpc/request-server';
33
import { trackEvent } from '@/utils/analytics/server';
44
import { createClient as createSupabaseClient } from '@/utils/supabase/request-server';
5-
import { askToolSet, buildToolSet, getAskModeSystemPrompt, getCreatePageSystemPrompt, getSystemPrompt, initModel } from '@onlook/ai';
5+
import { ASK_TOOL_SET, BUILD_TOOL_SET, getAskModeSystemPrompt, getCreatePageSystemPrompt, getSystemPrompt, initModel } from '@onlook/ai';
66
import { ChatType, CLAUDE_MODELS, type InitialModelPayload, LLMProvider, OPENROUTER_MODELS, type Usage, UsageType } from '@onlook/models';
77
import { generateObject, NoSuchToolError, streamText } from 'ai';
88
import { type NextRequest } from 'next/server';
@@ -135,7 +135,7 @@ export const streamResponse = async (req: NextRequest) => {
135135
systemPrompt = getSystemPrompt();
136136
break;
137137
}
138-
const toolSet = chatType === ChatType.ASK ? askToolSet : buildToolSet;
138+
const toolSet = chatType === ChatType.ASK ? ASK_TOOL_SET : BUILD_TOOL_SET;
139139
const result = streamText({
140140
model,
141141
headers,

apps/web/client/src/app/project/[id]/_components/bottom-bar/terminal.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import '@xterm/xterm/css/xterm.css';
44

55
import { useEditorEngine } from '@/components/store/editor';
66
import { cn } from '@onlook/ui/utils';
7-
import { FitAddon } from '@xterm/addon-fit';
87
import { type ITheme } from '@xterm/xterm';
98
import { observer } from 'mobx-react-lite';
109
import { useTheme } from 'next-themes';
@@ -94,15 +93,15 @@ export const Terminal = memo(observer(({ hidden = false, terminalSessionId }: Te
9493
// Handle container resize
9594
useEffect(() => {
9695
if (!containerRef.current || !terminalSession?.fitAddon || hidden) return;
97-
96+
9897
const resizeObserver = new ResizeObserver(() => {
9998
if (!hidden) {
10099
terminalSession.fitAddon.fit();
101100
}
102101
});
103-
102+
104103
resizeObserver.observe(containerRef.current);
105-
104+
106105
return () => {
107106
resizeObserver.disconnect();
108107
};

apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-display.tsx

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import { CREATE_FILE_TOOL_NAME, EDIT_FILE_TOOL_NAME, TERMINAL_COMMAND_TOOL_NAME } from '@onlook/ai';
1+
import { FUZZY_EDIT_FILE_TOOL_NAME, FUZZY_EDIT_FILE_TOOL_PARAMETERS, SEARCH_REPLACE_EDIT_FILE_TOOL_NAME, SEARCH_REPLACE_EDIT_FILE_TOOL_PARAMETERS, SEARCH_REPLACE_MULTI_EDIT_FILE_TOOL_NAME, SEARCH_REPLACE_MULTI_EDIT_FILE_TOOL_PARAMETERS, TERMINAL_COMMAND_TOOL_NAME, TODO_WRITE_TOOL_NAME, TODO_WRITE_TOOL_PARAMETERS, WRITE_FILE_TOOL_NAME, WRITE_FILE_TOOL_PARAMETERS } from '@onlook/ai';
2+
import { Icons } from '@onlook/ui/icons/index';
3+
import { cn } from '@onlook/ui/utils';
24
import type { ToolInvocation } from 'ai';
5+
import { z } from 'zod';
36
import { BashCodeDisplay } from '../../code-display/bash-code-display';
47
import { CollapsibleCodeBlock } from '../../code-display/collapsible-code-block';
58
import { ToolCallSimple } from './tool-call-simple';
@@ -33,9 +36,10 @@ export const ToolCallDisplay = ({
3336
);
3437
}
3538

36-
if (toolInvocation.toolName === EDIT_FILE_TOOL_NAME || toolInvocation.toolName === CREATE_FILE_TOOL_NAME) {
37-
const filePath = toolInvocation.args.path;
38-
const codeContent = toolInvocation.args.content;
39+
if (toolInvocation.toolName === WRITE_FILE_TOOL_NAME) {
40+
const args = toolInvocation.args as z.infer<typeof WRITE_FILE_TOOL_PARAMETERS>
41+
const filePath = args.file_path;
42+
const codeContent = args.content;
3943
return (
4044
<CollapsibleCodeBlock
4145
path={filePath}
@@ -48,6 +52,76 @@ export const ToolCallDisplay = ({
4852
/>
4953
);
5054
}
55+
56+
if (toolInvocation.toolName === FUZZY_EDIT_FILE_TOOL_NAME) {
57+
const args = toolInvocation.args as z.infer<typeof FUZZY_EDIT_FILE_TOOL_PARAMETERS>;
58+
const filePath = args.file_path;
59+
const codeContent = args.content;
60+
return (
61+
<CollapsibleCodeBlock
62+
path={filePath}
63+
content={codeContent}
64+
messageId={messageId}
65+
applied={applied}
66+
isStream={isStream}
67+
originalContent={codeContent}
68+
updatedContent={codeContent}
69+
/>
70+
);
71+
}
72+
73+
if (toolInvocation.toolName === SEARCH_REPLACE_EDIT_FILE_TOOL_NAME) {
74+
const args = toolInvocation.args as z.infer<typeof SEARCH_REPLACE_EDIT_FILE_TOOL_PARAMETERS>;
75+
const filePath = args.file_path;
76+
const codeContent = args.new_string;
77+
return (
78+
<CollapsibleCodeBlock
79+
path={filePath}
80+
content={codeContent}
81+
messageId={messageId}
82+
applied={applied}
83+
isStream={isStream}
84+
originalContent={codeContent}
85+
updatedContent={codeContent}
86+
/>
87+
);
88+
}
89+
90+
if (toolInvocation.toolName === SEARCH_REPLACE_MULTI_EDIT_FILE_TOOL_NAME) {
91+
const args = toolInvocation.args as z.infer<typeof SEARCH_REPLACE_MULTI_EDIT_FILE_TOOL_PARAMETERS>;
92+
const filePath = args.file_path;
93+
const codeContent = args.edits.map((edit) => edit.new_string).join('\n...\n');
94+
return (
95+
<CollapsibleCodeBlock
96+
path={filePath}
97+
content={codeContent}
98+
messageId={messageId}
99+
applied={applied}
100+
isStream={isStream}
101+
originalContent={codeContent}
102+
updatedContent={codeContent}
103+
/>
104+
);
105+
}
106+
107+
if (toolInvocation.toolName === TODO_WRITE_TOOL_NAME) {
108+
const args = toolInvocation.args as z.infer<typeof TODO_WRITE_TOOL_PARAMETERS>;
109+
const todos = args.todos;
110+
return (
111+
<div>
112+
{todos.map((todo) => (
113+
<div className="flex items-center gap-2 text-sm" key={todo.content}>
114+
{todo.status === 'completed' ? <Icons.SquareCheck className="w-4 h-4" /> : <Icons.Square className="w-4 h-4" />}
115+
<p className={cn(
116+
todo.status === 'completed' ? 'line-through text-green-500' : '',
117+
todo.status === 'in_progress' ? 'text-yellow-500' : '',
118+
todo.status === 'pending' ? 'text-gray-500' : '',
119+
)}>{todo.content}</p>
120+
</div>
121+
))}
122+
</div>
123+
);
124+
}
51125
}
52126

53127
return (

apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-simple.tsx

Lines changed: 125 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,55 @@
11
import {
2-
CREATE_FILE_TOOL_NAME,
3-
EDIT_FILE_TOOL_NAME,
2+
BASH_EDIT_TOOL_NAME,
3+
BASH_EDIT_TOOL_PARAMETERS,
4+
BASH_READ_TOOL_NAME,
5+
BASH_READ_TOOL_PARAMETERS,
6+
EXIT_PLAN_MODE_TOOL_NAME,
7+
FUZZY_EDIT_FILE_TOOL_NAME,
8+
FUZZY_EDIT_FILE_TOOL_PARAMETERS,
9+
GREP_TOOL_NAME,
410
LIST_FILES_TOOL_NAME,
11+
LIST_FILES_TOOL_PARAMETERS,
512
ONLOOK_INSTRUCTIONS_TOOL_NAME,
6-
READ_FILES_TOOL_NAME,
13+
READ_FILE_TOOL_NAME,
14+
READ_FILE_TOOL_PARAMETERS,
715
READ_STYLE_GUIDE_TOOL_NAME,
816
SANDBOX_TOOL_NAME,
917
SCRAPE_URL_TOOL_NAME,
10-
TERMINAL_COMMAND_TOOL_NAME
18+
SCRAPE_URL_TOOL_PARAMETERS,
19+
SEARCH_REPLACE_EDIT_FILE_TOOL_NAME,
20+
SEARCH_REPLACE_EDIT_FILE_TOOL_PARAMETERS,
21+
SEARCH_REPLACE_MULTI_EDIT_FILE_TOOL_NAME,
22+
SEARCH_REPLACE_MULTI_EDIT_FILE_TOOL_PARAMETERS,
23+
TERMINAL_COMMAND_TOOL_NAME,
24+
TODO_WRITE_TOOL_NAME,
25+
TODO_WRITE_TOOL_PARAMETERS,
26+
WRITE_FILE_TOOL_NAME,
27+
WRITE_FILE_TOOL_PARAMETERS
1128
} from '@onlook/ai';
1229
import { Icons } from '@onlook/ui/icons';
1330
import { cn } from '@onlook/ui/utils';
1431
import type { ToolInvocation } from 'ai';
32+
import { z } from 'zod';
1533

1634
// Map tool names to specific icon components
1735
const TOOL_ICONS: Record<string, any> = {
1836
[LIST_FILES_TOOL_NAME]: Icons.ListBullet,
19-
[READ_FILES_TOOL_NAME]: Icons.EyeOpen,
37+
[READ_FILE_TOOL_NAME]: Icons.EyeOpen,
2038
[READ_STYLE_GUIDE_TOOL_NAME]: Icons.Brand,
2139
[ONLOOK_INSTRUCTIONS_TOOL_NAME]: Icons.OnlookLogo,
22-
[EDIT_FILE_TOOL_NAME]: Icons.Pencil,
23-
[CREATE_FILE_TOOL_NAME]: Icons.FilePlus,
40+
[SEARCH_REPLACE_EDIT_FILE_TOOL_NAME]: Icons.Pencil,
41+
[SEARCH_REPLACE_MULTI_EDIT_FILE_TOOL_NAME]: Icons.Pencil,
42+
[FUZZY_EDIT_FILE_TOOL_NAME]: Icons.Pencil,
43+
[WRITE_FILE_TOOL_NAME]: Icons.FilePlus,
2444
[TERMINAL_COMMAND_TOOL_NAME]: Icons.Terminal,
45+
[BASH_EDIT_TOOL_NAME]: Icons.Terminal,
46+
[GREP_TOOL_NAME]: Icons.MagnifyingGlass,
2547
[SCRAPE_URL_TOOL_NAME]: Icons.Globe,
2648
[SANDBOX_TOOL_NAME]: Icons.Cube,
27-
};
49+
[TODO_WRITE_TOOL_NAME]: Icons.ListBullet,
50+
[EXIT_PLAN_MODE_TOOL_NAME]: Icons.ListBullet,
51+
[BASH_READ_TOOL_NAME]: Icons.EyeOpen,
52+
} as const;
2853

2954
export function ToolCallSimple({
3055
toolInvocation,
@@ -40,59 +65,105 @@ export function ToolCallSimple({
4065

4166
const getLabel = () => {
4267
try {
43-
let label = '';
44-
if (toolName === TERMINAL_COMMAND_TOOL_NAME) {
45-
return 'Terminal';
46-
}
47-
if (toolName === EDIT_FILE_TOOL_NAME) {
48-
if (toolInvocation.args && 'path' in toolInvocation.args) {
49-
label = "Editing " + (toolInvocation.args.path.split('/').pop() || '');
50-
} else {
51-
label = "Editing file";
52-
}
53-
} else if (toolName === CREATE_FILE_TOOL_NAME) {
54-
if (toolInvocation.args && 'path' in toolInvocation.args) {
55-
label = "Creating file " + (toolInvocation.args.path.split('/').pop() || '');
56-
} else {
57-
label = "Creating file";
58-
}
59-
} else if (toolName === LIST_FILES_TOOL_NAME) {
60-
if (toolInvocation.args && 'path' in toolInvocation.args) {
61-
label = "Reading directory " + (toolInvocation.args.path.split('/').pop() || '');
62-
} else {
63-
label = "Reading directory";
64-
}
65-
} else if (toolName === READ_FILES_TOOL_NAME) {
66-
if (toolInvocation.args && 'paths' in toolInvocation.args) {
67-
label = "Reading file" + (toolInvocation.args.paths.length > 1 ? 's' : '') + ' ' + (toolInvocation.args.paths.map((path: string) => path.split('/').pop()).join(', ') || '');
68-
} else {
69-
label = "Reading files";
70-
}
71-
} else if (toolName === READ_STYLE_GUIDE_TOOL_NAME) {
72-
label = "Reading style guide";
73-
} else if (toolName === ONLOOK_INSTRUCTIONS_TOOL_NAME) {
74-
label = "Reading Onlook instructions";
75-
} else if (toolName === SCRAPE_URL_TOOL_NAME) {
76-
if (toolInvocation.args && 'url' in toolInvocation.args) {
77-
try {
78-
const url = new URL(toolInvocation.args.url as string);
79-
label = "Visiting " + url.hostname;
80-
} catch {
81-
label = "Visiting URL";
68+
switch (toolName as keyof typeof TOOL_ICONS) {
69+
case TERMINAL_COMMAND_TOOL_NAME:
70+
return 'Terminal';
71+
case SEARCH_REPLACE_EDIT_FILE_TOOL_NAME:
72+
const params = toolInvocation.args as z.infer<typeof SEARCH_REPLACE_EDIT_FILE_TOOL_PARAMETERS>;
73+
if (params.file_path) {
74+
return 'Editing ' + (params.file_path.split('/').pop() || '');
75+
} else {
76+
return 'Editing file';
77+
}
78+
case SEARCH_REPLACE_MULTI_EDIT_FILE_TOOL_NAME:
79+
const params1 = toolInvocation.args as z.infer<typeof SEARCH_REPLACE_MULTI_EDIT_FILE_TOOL_PARAMETERS>;
80+
if (params1.edits) {
81+
return 'Editing ' + (params1.edits.map((edit: { old_string: string; new_string: string; replace_all: boolean; }) => edit.old_string).join(', ') || '');
82+
} else {
83+
return 'Editing files';
84+
}
85+
case FUZZY_EDIT_FILE_TOOL_NAME:
86+
const params2 = toolInvocation.args as z.infer<typeof FUZZY_EDIT_FILE_TOOL_PARAMETERS>;
87+
if (params2.file_path) {
88+
return 'Editing ' + (params2.file_path.split('/').pop() || '');
89+
} else {
90+
return 'Editing file';
91+
}
92+
case WRITE_FILE_TOOL_NAME:
93+
const params3 = toolInvocation.args as z.infer<typeof WRITE_FILE_TOOL_PARAMETERS>;
94+
if (params3.file_path) {
95+
return 'Creating file ' + (params3.file_path.split('/').pop() || '');
96+
} else {
97+
return 'Creating file';
98+
}
99+
case LIST_FILES_TOOL_NAME:
100+
const params4 = toolInvocation.args as z.infer<typeof LIST_FILES_TOOL_PARAMETERS>;
101+
if (params4.path) {
102+
return 'Reading directory ' + (params4.path.split('/').pop() || '');
103+
} else {
104+
return 'Reading directory';
105+
}
106+
case READ_FILE_TOOL_NAME:
107+
const params5 = toolInvocation.args as z.infer<typeof READ_FILE_TOOL_PARAMETERS>;
108+
if (params5.file_path) {
109+
return 'Reading file ' + (params5.file_path.split('/').pop() || '');
110+
} else {
111+
return 'Reading files';
112+
}
113+
case SCRAPE_URL_TOOL_NAME:
114+
const params6 = toolInvocation.args as z.infer<typeof SCRAPE_URL_TOOL_PARAMETERS>;
115+
if (params6.url) {
116+
return 'Visiting ' + (new URL(params6.url).hostname || 'URL');
117+
} else {
118+
return 'Visiting URL';
82119
}
83-
} else {
84-
label = "Visiting URL";
85-
}
86-
} else {
87-
label = toolName.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
120+
case SANDBOX_TOOL_NAME:
121+
if (toolInvocation.args && 'command' in toolInvocation.args) {
122+
return 'Sandbox: ' + toolInvocation.args.command;
123+
} else {
124+
return 'Sandbox';
125+
}
126+
case GREP_TOOL_NAME:
127+
if (toolInvocation.args && 'pattern' in toolInvocation.args) {
128+
return 'Searching for ' + toolInvocation.args.pattern;
129+
} else {
130+
return 'Searching';
131+
}
132+
case BASH_EDIT_TOOL_NAME:
133+
const params7 = toolInvocation.args as z.infer<typeof BASH_EDIT_TOOL_PARAMETERS>;
134+
if (params7.command) {
135+
return 'Running command ' + (params7.command.split('/').pop() || '');
136+
} else {
137+
return 'Running command';
138+
}
139+
case BASH_READ_TOOL_NAME:
140+
const params8 = toolInvocation.args as z.infer<typeof BASH_READ_TOOL_PARAMETERS>;
141+
if (params8.command) {
142+
return 'Reading file ' + (params8.command.split('/').pop() || '');
143+
} else {
144+
return 'Reading file';
145+
}
146+
case TODO_WRITE_TOOL_NAME:
147+
const params9 = toolInvocation.args as z.infer<typeof TODO_WRITE_TOOL_PARAMETERS>;
148+
if (params9.todos) {
149+
return 'Writing todos ' + (params9.todos.map((todo: { content: string; status: string; priority: string; }) => todo.content).join(', ') || '');
150+
} else {
151+
return 'Writing todos';
152+
}
153+
case EXIT_PLAN_MODE_TOOL_NAME:
154+
return 'Exiting plan mode';
155+
case READ_STYLE_GUIDE_TOOL_NAME:
156+
return 'Reading style guide';
157+
case ONLOOK_INSTRUCTIONS_TOOL_NAME:
158+
return 'Reading Onlook instructions';
159+
default:
160+
return toolName.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
88161
}
89-
return label;
90162
} catch (error) {
91163
console.error('Error getting label', error);
92164
return toolName.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
93165
}
94166
}
95-
96167
return (
97168
<div className={cn('flex items-center gap-2 ml-2 text-foreground-tertiary/80', className)}>
98169
<Icon className="w-4 h-4" />

apps/web/client/src/components/store/editor/sandbox/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ export class SandboxManager {
178178
private async writeRemoteFile(
179179
filePath: string,
180180
content: string | Uint8Array,
181+
overwrite: boolean = true,
181182
): Promise<boolean> {
182183
if (!this.session.session) {
183184
console.error('No session found for remote write');
@@ -186,9 +187,13 @@ export class SandboxManager {
186187

187188
try {
188189
if (content instanceof Uint8Array) {
189-
await this.session.session.fs.writeFile(filePath, content);
190+
await this.session.session.fs.writeFile(filePath, content, {
191+
overwrite,
192+
});
190193
} else {
191-
await this.session.session.fs.writeTextFile(filePath, content);
194+
await this.session.session.fs.writeTextFile(filePath, content, {
195+
overwrite,
196+
});
192197
}
193198
return true;
194199
} catch (error) {

0 commit comments

Comments
 (0)