Skip to content

Commit 6e3204a

Browse files
authored
feat(ui-tars): local browser availability detection (#509)
1 parent be96f27 commit 6e3204a

File tree

14 files changed

+220
-75
lines changed

14 files changed

+220
-75
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Copyright (c) 2025 Bytedance, Inc. and its affiliates.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import { initIpc } from '@ui-tars/electron-ipc/main';
6+
import { checkBrowserAvailability } from '../services/browserCheck';
7+
8+
const t = initIpc.create();
9+
10+
export const browserRoute = t.router({
11+
checkBrowserAvailability: t.procedure.input<void>().handle(async () => {
12+
return await checkBrowserAvailability();
13+
}),
14+
});

apps/ui-tars/src/main/ipcRoutes/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { screenRoute } from './screen';
77
import { windowRoute } from './window';
88
import { permissionRoute } from './permission';
99
import { agentRoute } from './agent';
10+
import { browserRoute } from './browser';
1011

1112
const t = initIpc.create();
1213

@@ -15,6 +16,7 @@ export const ipcRoutes = t.router({
1516
...windowRoute,
1617
...permissionRoute,
1718
...agentRoute,
19+
...browserRoute,
1820
});
1921
export type Router = typeof ipcRoutes;
2022

apps/ui-tars/src/main/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { createTray } from './tray';
3434
import { registerSettingsHandlers } from './services/settings';
3535
import { sanitizeState } from './utils/sanitizeState';
3636
import { windowManager } from './services/windowManager';
37+
import { checkBrowserAvailability } from './services/browserCheck';
3738

3839
const { isProd } = env;
3940

@@ -86,6 +87,8 @@ const initializeApp = async () => {
8687
logger.info('ensureScreenCapturePermission', ensureScreenCapturePermission);
8788
}
8889

90+
await checkBrowserAvailability();
91+
8992
// if (env.isDev) {
9093
await loadDevDebugTools();
9194
// }
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Copyright (c) 2025 Bytedance, Inc. and its affiliates.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import { DefaultBrowserOperator } from '@ui-tars/operator-browser';
6+
import { logger } from '@main/logger';
7+
import { store } from '@main/store/create';
8+
9+
/**
10+
* Check if there is a browser available in the system
11+
*/
12+
export async function checkBrowserAvailability(): Promise<boolean> {
13+
try {
14+
logger.info('Checking browser availability...');
15+
const available = DefaultBrowserOperator.hasBrowser();
16+
logger.info(`Browser availability: ${available}`);
17+
store.setState({ browserAvailable: available });
18+
return available;
19+
} catch (error) {
20+
logger.error('Error checking browser availability:', error);
21+
store.setState({ browserAvailable: false });
22+
return false;
23+
}
24+
}

apps/ui-tars/src/main/services/runAgent.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import { SettingStore } from '@main/store/setting';
2626
import { AppState, VLMProviderV2 } from '@main/store/types';
2727
import { GUIAgentManager } from '../ipcRoutes/agent';
28+
import { checkBrowserAvailability } from './browserCheck';
2829

2930
const getModelVersion = (
3031
provider: VLMProviderV2 | undefined,
@@ -132,6 +133,17 @@ export const runAgent = async (
132133
if (settings.operator === 'nutjs') {
133134
operator = new NutJSElectronOperator();
134135
} else {
136+
await checkBrowserAvailability();
137+
const { browserAvailable } = getState();
138+
if (!browserAvailable) {
139+
setState({
140+
...getState(),
141+
status: StatusEnum.ERROR,
142+
errorMsg:
143+
'Browser is not available. Please install Chrome and try again.',
144+
});
145+
return;
146+
}
135147
operator = await DefaultBrowserOperator.getInstance(
136148
false,
137149
false,

apps/ui-tars/src/main/store/create.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ export const store = createStore<AppState>(
2020
ensurePermissions: {},
2121
abortController: null,
2222
thinking: false,
23+
browserAvailable: false, // Defaults to false until the detection is complete
2324
}) satisfies AppState,
2425
);

apps/ui-tars/src/main/store/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export type AppState = {
3131
messages: ConversationWithSoM[];
3232
abortController: AbortController | null;
3333
thinking: boolean;
34+
browserAvailable: boolean;
3435
};
3536

3637
export enum VlmProvider {

apps/ui-tars/src/renderer/src/components/ChatInput/SelectOperator.tsx

Lines changed: 127 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,16 @@
22
* Copyright (c) 2025 Bytedance, Inc. and its affiliates.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
5+
import { useEffect } from 'react';
56
import { Button } from '@renderer/components/ui/button';
6-
import { ChevronDown, Globe, Monitor, Check } from 'lucide-react';
7+
import {
8+
ChevronDown,
9+
Globe,
10+
Monitor,
11+
Check,
12+
AlertCircle,
13+
RefreshCw,
14+
} from 'lucide-react';
715
import {
816
DropdownMenu,
917
DropdownMenuContent,
@@ -13,6 +21,15 @@ import {
1321
import { useSetting } from '@renderer/hooks/useSetting';
1422
import { useState } from 'react';
1523
import { BROWSER_USE, COMPUTERR_USE } from '@renderer/const';
24+
import { useStore } from '@renderer/hooks/useStore';
25+
import { api } from '@renderer/api';
26+
import { toast } from 'sonner';
27+
import {
28+
Tooltip,
29+
TooltipContent,
30+
TooltipProvider,
31+
TooltipTrigger,
32+
} from '@renderer/components/ui/tooltip';
1633

1734
type Operator = 'nutjs' | 'browser';
1835

@@ -40,18 +57,62 @@ const getOperatorLabel = (type: string) => {
4057

4158
export const SelectOperator = () => {
4259
const [isOpen, setIsOpen] = useState(false);
60+
const [tooltipOpen, setTooltipOpen] = useState(false);
61+
4362
const { settings, updateSetting } = useSetting();
63+
const { browserAvailable } = useStore();
64+
const [isRetrying, setIsRetrying] = useState(false);
4465

45-
// console.log('settings', settings);
66+
// Get the current operating mode and automatically
67+
// switch to computer mode if browser mode is not available
68+
const currentOperator = browserAvailable
69+
? settings.operator || 'nutjs'
70+
: 'nutjs';
4671

47-
const currentOperator = settings.operator || 'nutjs';
72+
// If the current setting is browser but the browser
73+
// is not available, automatically switched to Computer Use mode.
74+
useEffect(() => {
75+
if (settings.operator === 'browser' && !browserAvailable) {
76+
updateSetting({
77+
...settings,
78+
operator: 'nutjs',
79+
});
80+
toast.info('Automatically switched to Computer Use mode', {
81+
description: 'Browser mode is not available',
82+
});
83+
}
84+
}, [browserAvailable, settings, updateSetting]);
4885

4986
const handleSelect = (type: Operator) => {
87+
if (type === 'browser' && !browserAvailable) {
88+
return;
89+
}
90+
5091
updateSetting({
5192
...settings,
5293
operator: type,
5394
});
54-
console.log('handleSelect', type);
95+
};
96+
97+
const handleRetryBrowserCheck = async () => {
98+
try {
99+
setIsRetrying(true);
100+
const available = await api.checkBrowserAvailability();
101+
if (available) {
102+
toast.success('Browser detected successfully!', {
103+
description: 'You can now use Browser mode.',
104+
});
105+
setTooltipOpen(false);
106+
} else {
107+
toast.error('No browser detected', {
108+
description: 'Please install Chrome and try again.',
109+
});
110+
}
111+
} catch (error) {
112+
toast.error('Failed to check browser availability');
113+
} finally {
114+
setIsRetrying(false);
115+
}
55116
};
56117

57118
return (
@@ -74,13 +135,69 @@ export const SelectOperator = () => {
74135
Computer Use
75136
{currentOperator === 'nutjs' && <Check className="h-4 w-4 ml-2" />}
76137
</DropdownMenuItem>
77-
<DropdownMenuItem onClick={() => handleSelect('browser')}>
78-
<Globe className="h-4 w-4 mr-2" />
79-
Browser Use
80-
{currentOperator === 'browser' && (
81-
<Check className="h-4 w-4 ml-2" />
138+
139+
<div className="relative">
140+
<DropdownMenuItem
141+
onClick={() => browserAvailable && handleSelect('browser')}
142+
disabled={!browserAvailable}
143+
className="flex items-center justify-start"
144+
>
145+
<Globe className="h-4 w-4 mr-2" />
146+
Browser Use
147+
{currentOperator === 'browser' && (
148+
<Check className="h-4 w-4 ml-2" />
149+
)}
150+
</DropdownMenuItem>
151+
152+
{!browserAvailable && (
153+
<div
154+
className="absolute right-2 top-1/2 -translate-y-1/2"
155+
onClick={(e) => e.stopPropagation()}
156+
>
157+
<TooltipProvider>
158+
<Tooltip open={tooltipOpen} onOpenChange={setTooltipOpen}>
159+
<TooltipTrigger asChild>
160+
<AlertCircle
161+
className="h-4 w-4 text-yellow-500"
162+
onClick={(e) => {
163+
e.stopPropagation();
164+
setTooltipOpen(true);
165+
}}
166+
/>
167+
</TooltipTrigger>
168+
<TooltipContent
169+
side="right"
170+
align="center"
171+
className="p-3 bg-white border border-gray-200 shadow-md w-64 z-50"
172+
>
173+
<div className="flex flex-col gap-3">
174+
<p className="text-sm text-gray-700 font-medium text-center">
175+
Chrome browser not detected.
176+
</p>
177+
<Button
178+
size="sm"
179+
variant="secondary"
180+
onClick={(e) => {
181+
e.preventDefault();
182+
e.stopPropagation();
183+
handleRetryBrowserCheck();
184+
}}
185+
disabled={isRetrying}
186+
>
187+
{isRetrying ? (
188+
<RefreshCw className="h-4 w-4 animate-spin" />
189+
) : (
190+
<RefreshCw className="h-4 w-4" />
191+
)}
192+
{isRetrying ? 'Checking...' : 'Retry Detection'}
193+
</Button>
194+
</div>
195+
</TooltipContent>
196+
</Tooltip>
197+
</TooltipProvider>
198+
</div>
82199
)}
83-
</DropdownMenuItem>
200+
</div>
84201
</DropdownMenuContent>
85202
</DropdownMenu>
86203
</div>

apps/ui-tars/src/renderer/src/components/ui/tooltip.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ function TooltipContent({
5454
{...props}
5555
>
5656
{children}
57-
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
57+
<TooltipPrimitive.Arrow className="bg-white fill-white z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
5858
</TooltipPrimitive.Content>
5959
</TooltipPrimitive.Portal>
6060
);

docs/quick-start.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ The previous version of UI-TARS Desktop version 0.0.8 will be upgraded to a new
66

77
<br />
88

9+
## Prerequisites
10+
11+
- Install [Chrome](https://www.google.com/chrome/) for **Browser use**.
12+
13+
<br />
14+
915
## Download
1016

1117
You can download the [latest release](https://github.com/bytedance/UI-TARS-desktop/releases/latest) version of UI-TARS Desktop from our releases page.

packages/ui-tars/operators/browser-operator/examples/default.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@
55
import { LocalBrowser } from '@agent-infra/browser';
66
import { ConsoleLogger } from '@agent-infra/logger';
77
import { GUIAgent, StatusEnum } from '@ui-tars/sdk';
8-
import { BrowserOperator } from '../src';
8+
import { BrowserOperator, DefaultBrowserOperator } from '../src';
99

1010
async function main() {
11+
if (!DefaultBrowserOperator.hasBrowser()) {
12+
console.error('No available browser found on this system.');
13+
process.exit(1);
14+
}
15+
1116
// 1. Create a local browser
1217
const logger = new ConsoleLogger('[BrowserGUIAgent]');
1318
const browser = new LocalBrowser({

packages/ui-tars/operators/browser-operator/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"registry": "https://registry.npmjs.org"
3737
},
3838
"dependencies": {
39+
"@agent-infra/browser": "workspace:*",
3940
"@agent-infra/logger": "workspace:*",
4041
"@ui-tars/sdk": "workspace:*"
4142
},
@@ -50,4 +51,4 @@
5051
"ts-node": "^10.9.2",
5152
"@types/big.js": "^6.2.2"
5253
}
53-
}
54+
}

packages/ui-tars/operators/browser-operator/src/browser-operator.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
} from '@ui-tars/sdk/core';
1515
import { BrowserOperatorOptions } from './types';
1616
import { UIHelper } from './ui-helper';
17+
import { BrowserFinder } from '@agent-infra/browser';
1718

1819
const KEY_MAPPINGS: Record<string, string> = {
1920
enter: 'Enter',
@@ -501,6 +502,23 @@ export class DefaultBrowserOperator extends BrowserOperator {
501502
super(options);
502503
}
503504

505+
/**
506+
* Check whether the local environment has a browser available
507+
* @returns {boolean}
508+
*/
509+
public static hasBrowser(): boolean {
510+
try {
511+
const browserFinder = new BrowserFinder();
512+
browserFinder.findBrowser();
513+
return true;
514+
} catch (error) {
515+
if (this.logger) {
516+
this.logger.error('No available browser found:', error);
517+
}
518+
return false;
519+
}
520+
}
521+
504522
public static async getInstance(
505523
highlight = false,
506524
showActionInfo = false,

0 commit comments

Comments
 (0)