Skip to content

Commit 4cb4afb

Browse files
authored
feat: setting preset (close: #57) (#61)
* feat: implement preset setting (close: #57) * fix(renderer): setting page cannot scroll * refactor(renderer): setting layout and scroll behavior * refactor(renderer): remote preset management ui * refactor(renderer): setting input should be disabled when remote preset management take effect * fix(renderer): reset to manual does not work * chore(renderer): dispatch get setting after import preset * feat(renderer): clear setting * refactor(renderer): naming * chore: tweaks * chore: add setting store change log * refactor: enhance impl of `setting:resetPreset` * refactor: enhance impl of `setting:clear` * chore: tweaks * refactor: using self-implemented setting state management * chore: tweaks
1 parent 83e2fe2 commit 4cb4afb

File tree

15 files changed

+876
-247
lines changed

15 files changed

+876
-247
lines changed

src/main/agent/llm/ui-tars.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import OpenAI from 'openai';
66

77
import { convertToOpenAIMessages } from '@main/agent/utils';
88
import { logger } from '@main/logger';
9-
import { store } from '@main/store/create';
109
import { preprocessResizeImage } from '@main/utils/image';
1110

1211
import { MAX_PIXELS } from '../constant';
1312
import { VLM, VlmRequest, VlmRequestOptions, VlmResponse } from './base';
13+
import { SettingStore } from '@main/store/setting';
1414

1515
export interface UITARSOptions {
1616
reflection: boolean;
@@ -26,7 +26,7 @@ export interface UITARSOptions {
2626

2727
export class UITARS implements VLM<VlmRequest, VlmResponse> {
2828
get vlmModel() {
29-
return store.getState().getSetting('vlmModelName');
29+
return SettingStore.getStore().vlmModelName;
3030
}
3131

3232
// [image, prompt]
@@ -44,8 +44,8 @@ export class UITARS implements VLM<VlmRequest, VlmResponse> {
4444
conversations,
4545
images: compressedImages,
4646
});
47-
const vlmBaseUrl = store.getState().getSetting('vlmBaseUrl');
48-
const vlmApiKey = store.getState().getSetting('vlmApiKey');
47+
const vlmBaseUrl = SettingStore.getStore().vlmBaseUrl;
48+
const vlmApiKey = SettingStore.getStore().vlmApiKey;
4949
logger.info('vlmBaseUrl', vlmBaseUrl, 'vlmApiKey', vlmApiKey);
5050

5151
const openai = new OpenAI({

src/main/main.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
} from 'electron';
1414
import squirrelStartup from 'electron-squirrel-startup';
1515
import ElectronStore from 'electron-store';
16-
import { updateElectronApp, UpdateSourceType } from 'update-electron-app';
16+
import { UpdateSourceType, updateElectronApp } from 'update-electron-app';
1717
import { mainZustandBridge } from 'zutron/main';
1818

1919
import * as env from '@main/env';
@@ -26,7 +26,9 @@ import {
2626

2727
import { UTIOService } from './services/utio';
2828
import { store } from './store/create';
29+
import { SettingStore } from './store/setting';
2930
import { createTray } from './tray';
31+
import { registerSettingsHandlers } from './services/settings';
3032

3133
const { isProd } = env;
3234

@@ -164,6 +166,19 @@ const initializeApp = async () => {
164166
app.on('quit', unsubscribe);
165167

166168
logger.info('initializeApp end');
169+
170+
// Check and update remote presets
171+
const settings = SettingStore.getStore();
172+
if (
173+
settings.presetSource?.type === 'remote' &&
174+
settings.presetSource.autoUpdate
175+
) {
176+
try {
177+
await SettingStore.importPresetFromUrl(settings.presetSource.url!, true);
178+
} catch (error) {
179+
logger.error('Failed to update preset:', error);
180+
}
181+
}
167182
};
168183

169184
/**
@@ -181,6 +196,8 @@ const registerIPCHandlers = () => {
181196
screenHeight: primaryDisplay.size.height,
182197
};
183198
});
199+
200+
registerSettingsHandlers();
184201
};
185202

186203
/**

src/main/services/settings.ts

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Copyright (c) 2025 Bytedance, Inc. and its affiliates.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import { ipcMain } from 'electron';
6+
import { SettingStore } from '../store/setting';
7+
import { logger } from '../logger';
8+
import { LocalStore } from '@main/store/validate';
9+
10+
export function registerSettingsHandlers() {
11+
/**
12+
* Get setting
13+
*/
14+
ipcMain.handle('setting:get', () => {
15+
return SettingStore.getStore();
16+
});
17+
18+
/**
19+
* Clear setting
20+
*/
21+
ipcMain.handle('setting:clear', () => {
22+
SettingStore.clear();
23+
});
24+
25+
/**
26+
* Reset setting preset
27+
*/
28+
ipcMain.handle('setting:resetPreset', () => {
29+
SettingStore.getInstance().delete('presetSource');
30+
});
31+
32+
/**
33+
* Update setting
34+
*/
35+
ipcMain.handle('setting:update', async (_, settings: LocalStore) => {
36+
SettingStore.setStore(settings);
37+
});
38+
39+
/**
40+
* Import setting preset from text
41+
*/
42+
ipcMain.handle('setting:importPresetFromText', async (_, yamlContent) => {
43+
try {
44+
const newSettings = await SettingStore.importPresetFromText(yamlContent);
45+
SettingStore.setStore(newSettings);
46+
} catch (error) {
47+
logger.error('Failed to import preset:', error);
48+
throw error;
49+
}
50+
});
51+
52+
/**
53+
* Import setting preset from url
54+
*/
55+
ipcMain.handle('setting:importPresetFromUrl', async (_, url, autoUpdate) => {
56+
try {
57+
const newSettings = await SettingStore.fetchPresetFromUrl(url);
58+
SettingStore.setStore({
59+
...newSettings,
60+
presetSource: {
61+
type: 'remote',
62+
url: url,
63+
autoUpdate: autoUpdate,
64+
lastUpdated: Date.now(),
65+
},
66+
});
67+
} catch (error) {
68+
logger.error('Failed to import preset from URL:', error);
69+
throw error;
70+
}
71+
});
72+
73+
/**
74+
* Update setting preset from url
75+
*/
76+
ipcMain.handle('setting:updatePresetFromRemote', async () => {
77+
const settings = SettingStore.getStore();
78+
if (settings.presetSource?.type === 'remote' && settings.presetSource.url) {
79+
const newSettings = await SettingStore.fetchPresetFromUrl(
80+
settings.presetSource.url,
81+
);
82+
SettingStore.setStore({
83+
...newSettings,
84+
presetSource: {
85+
type: 'remote',
86+
url: settings.presetSource.url,
87+
autoUpdate: settings.presetSource.autoUpdate,
88+
lastUpdated: Date.now(),
89+
},
90+
});
91+
} else {
92+
throw new Error('No remote preset configured');
93+
}
94+
});
95+
}

src/main/services/utio.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
/**
2+
* Copyright (c) 2025 Bytedance, Inc. and its affiliates.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
15
import os from 'node:os';
26

37
import { screen } from 'electron';
48

59
import { UTIO, UTIOPayload } from '@ui-tars/utio';
610

711
import { logger } from '../logger';
8-
import { store } from '../store/create';
12+
import { SettingStore } from '@main/store/setting';
913

1014
export class UTIOService {
1115
private static instance: UTIOService;
@@ -19,7 +23,7 @@ export class UTIOService {
1923
}
2024

2125
private getEndpoint(): string | undefined {
22-
const endpoint = store.getState().getSetting('utioBaseUrl');
26+
const endpoint = SettingStore.getStore().utioBaseUrl;
2327
logger.debug('[UTIO] endpoint:', endpoint);
2428
return endpoint;
2529
}

src/main/store/create.ts

+1-14
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ import {
1717

1818
import { closeScreenMarker } from '@main/window/ScreenMarker';
1919
import { runAgent } from './runAgent';
20-
import { SettingStore } from './setting';
21-
import { AppState } from './types';
20+
import type { AppState } from './types';
2221

2322
export const store = createStore<AppState>(
2423
(set, get) =>
@@ -28,9 +27,7 @@ export const store = createStore<AppState>(
2827
instructions: '',
2928
status: StatusEnum.INIT,
3029
messages: [],
31-
settings: null,
3230
errorMsg: null,
33-
getSetting: (key) => SettingStore.get(key),
3431
ensurePermissions: {},
3532

3633
abortController: null,
@@ -54,16 +51,6 @@ export const store = createStore<AppState>(
5451
LauncherWindow.getInstance().hide();
5552
},
5653

57-
GET_SETTINGS: () => {
58-
const settings = SettingStore.getStore();
59-
set({ settings });
60-
},
61-
62-
SET_SETTINGS: (state) => {
63-
SettingStore.setStore(state);
64-
set({ settings: SettingStore.getStore() });
65-
},
66-
6754
GET_ENSURE_PERMISSIONS: async () => {
6855
if (env.isMacOS) {
6956
const { ensurePermissions } = await import(

src/main/store/runAgent.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ export const runAgent = async (
2929
) => {
3030
logger.info('runAgent');
3131
const settings = SettingStore.getStore();
32-
const { instructions, abortController, getSetting } = getState();
32+
const { instructions, abortController } = getState();
3333
const device = new Desktop();
3434
const vlm = new UITARS();
3535
assert(instructions, 'instructions is required');
3636

37-
const language = getSetting('language') || 'en';
37+
const language = settings.language ?? 'en';
3838

3939
const agent = new ComputerUseAgent({
4040
systemPrompt: getSystemPrompt(language),

0 commit comments

Comments
 (0)