Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion desktop/renderer/src/browser-openclaw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,57 @@ export function createBrowserOpenClawMock(): OpenClawAPI {
},
},
usage: {
getStats: async () => ({ totalSpend: 0, totalTokens: 0 }),
getStats: async () => ({
totalSpend: 6.82,
maxBudget: 12,
modelSpend: {
"gpt-4.1": 2.41,
"claude-sonnet-4": 3.78,
"gpt-4.1-mini": 0.63,
},
keyName: "browser-dev-preview",
budgetDuration: "month",
budgetResetAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 14).toISOString(),
totalPromptTokens: 184320,
totalCompletionTokens: 76240,
totalTokens: 260560,
totalRequests: 428,
modelBreakdown: {
"gpt-4.1": {
requests: 136,
promptTokens: 68210,
completionTokens: 21940,
spend: 2.41,
},
"claude-sonnet-4": {
requests: 204,
promptTokens: 91360,
completionTokens: 41680,
spend: 3.78,
},
"gpt-4.1-mini": {
requests: 88,
promptTokens: 24750,
completionTokens: 12620,
spend: 0.63,
},
},
dailySpend: {
"2026-08-06": 0.72,
"2026-08-07": 0.94,
"2026-08-08": 1.16,
"2026-08-09": 0.83,
"2026-08-10": 1.49,
"2026-08-11": 1.68,
},
hasDetailedLogs: true,
cacheReadTokens: 38420,
cacheWriteTokens: 9210,
sessionCount: 37,
toolCalls: 96,
exchangeRate: 7.2,
currency: "CNY",
}),
getDetailedStats: async () => ({}),
getExchangeRate: async () => ({
rate: 7.2,
Expand Down
7 changes: 6 additions & 1 deletion desktop/renderer/src/i18n/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,10 +346,12 @@ export default {
"settings.gatewayLogsExported": "Gateway logs exported.",
"settings.gatewayLogsExportFailed": "Could not export Gateway logs: {error}",
"settings.webSearchDesc":
"MicroClaw uses the configured provider for web search. The gateway restarts automatically after you save.",
"MicroClaw uses the configured provider for web search. Changes are saved automatically.",
"settings.noApiKeyRequired": "No API key required",
"settings.getApiKey": "Get API key",
"settings.save": "Save",
"settings.saving": "Saving...",
"settings.saved": "Saved",
"settings.configSaveFailed": "Failed to save configuration: {error}",
"settings.modelSwitched": "Model switched to {model}",
"settings.customModelAdded": "Custom model added",
Expand All @@ -364,6 +366,9 @@ export default {
"settings.searchConfigured": "Configured",
"settings.searchNotConfigured": "Not configured",
"settings.searchDefault": "Default",
"settings.searchApiKeyRequired": "API key required",
"settings.searchApiKeyFormatInvalid": "Check the key format",
"settings.searchSaveFailed": "Failed to save",
"settings.saveFailed": "Save failed: {error}",
"settings.clearHistoryConfirm":
"Are you sure you want to clear all chat history? This action cannot be undone.",
Expand Down
7 changes: 6 additions & 1 deletion desktop/renderer/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,10 +325,12 @@ export default {
"settings.export": "导出",
"settings.gatewayLogsExported": "网关日志已导出。",
"settings.gatewayLogsExportFailed": "无法导出网关日志:{error}",
"settings.webSearchDesc": "MicroClaw 使用已配置的提供商进行联网搜索。保存后网关将自动重启生效。",
"settings.webSearchDesc": "MicroClaw 使用已配置的提供商进行联网搜索。更改会自动保存。",
"settings.noApiKeyRequired": "无需 API 密钥",
"settings.getApiKey": "获取 API 密钥",
"settings.save": "保存",
"settings.saving": "正在保存...",
"settings.saved": "已保存",
"settings.configSaveFailed": "配置保存失败:{error}",
"settings.modelSwitched": "模型已切换为 {model}",
"settings.customModelAdded": "自定义模型已添加",
Expand All @@ -342,6 +344,9 @@ export default {
"settings.searchConfigured": "已配置",
"settings.searchNotConfigured": "未配置",
"settings.searchDefault": "默认",
"settings.searchApiKeyRequired": "需要 API 密钥",
"settings.searchApiKeyFormatInvalid": "请检查密钥格式",
"settings.searchSaveFailed": "保存失败",
"settings.saveFailed": "保存失败:{error}",
"settings.clearHistoryConfirm": "确定要清除所有聊天记录吗?此操作不可撤销。",
"settings.confirm": "确认",
Expand Down
87 changes: 65 additions & 22 deletions desktop/renderer/src/views/SettingsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -311,21 +311,20 @@
<div class="sub-label-row">
<span class="sub-label">{{ t("settings.webSearch") }}</span>
<div class="sub-label-actions">
<el-button
size="small"
type="primary"
plain
:disabled="!searchDirty"
:loading="searchSavingAll"
@click="saveSearchSettings"
>{{ t("settings.save") }}</el-button
<span
v-if="searchSaveStatus !== 'idle'"
class="status-indicator"
:class="searchSaveStatusClass"
>
<span class="status-dot"></span>
{{ searchSaveStatusText }}
</span>
</div>
</div>
<div class="card-group">
<div class="card-row">
<div class="card-row" :class="{ 'no-border': !activeSearchProvider.requiresKey }">
<span class="row-label">{{ t("settings.provider") }}</span>
<el-select v-model="searchProvider" style="width: 130px">
<el-select v-model="searchProvider" style="width: 130px" @change="handleSearchProviderChange">
<el-option v-for="p in searchProviders" :key="p.id" :label="p.name" :value="p.id" />
</el-select>
</div>
Expand All @@ -343,12 +342,10 @@
show-password
:placeholder="activeSearchProvider.placeholder"
class="provider-key-input"
@blur="saveSearchKeyIfReady"
@keyup.enter="saveSearchKeyIfReady"
/>
</div>
<div v-else class="card-row no-border">
<span class="row-label">{{ t("settings.apiKey") }}</span>
<span class="placeholder-text">{{ t("settings.noApiKeyRequired") }}</span>
</div>
</div>
<div class="section-footer">{{ t("settings.webSearchDesc") }}</div>
</div>
Expand Down Expand Up @@ -1271,10 +1268,20 @@ const savedSearchKeys = reactive<Record<SearchProviderId, string>>({
brave: "",
tavily: "",
});
const searchSavingAll = ref(false);
const searchDirty = computed(() => {
if (searchProvider.value !== savedSearchProvider.value) return true;
return searchProviders.some((p) => searchKeys[p.id].trim() !== savedSearchKeys[p.id]);
type SearchSaveStatus = "idle" | "saving" | "saved" | "error" | "invalid";
const searchSaveStatus = ref<SearchSaveStatus>("idle");
const searchSaveError = ref("");
const searchSaveStatusClass = computed(() => {
if (searchSaveStatus.value === "saved") return "status-indicator--ok";
if (searchSaveStatus.value === "saving") return "status-indicator--muted";
return "status-indicator--error";
});
const searchSaveStatusText = computed(() => {
if (searchSaveStatus.value === "saving") return t("settings.saving");
if (searchSaveStatus.value === "saved") return t("settings.saved");
if (searchSaveStatus.value === "invalid") return searchSaveError.value;
if (searchSaveStatus.value === "error") return t("settings.searchSaveFailed");
return "";
});

// --- Usage state ---
Expand Down Expand Up @@ -1763,6 +1770,24 @@ function snapshotSearchConfig(): void {
for (const p of searchProviders) savedSearchKeys[p.id] = searchKeys[p.id].trim();
}

function validateActiveSearchKey(): string | null {
if (!activeSearchProvider.value.requiresKey) return null;
const key = searchKeys[searchProvider.value].trim();
if (!key) return t("settings.searchApiKeyRequired");
if (searchProvider.value === "brave" && !key.startsWith("BSA")) {
return t("settings.searchApiKeyFormatInvalid");
}
if (searchProvider.value === "tavily" && !key.startsWith("tvly-")) {
return t("settings.searchApiKeyFormatInvalid");
}
return null;
}

function setSearchValidationError(message: string): void {
searchSaveError.value = message;
searchSaveStatus.value = "invalid";
}

// Writes only the active provider into tools.web.search — the single object the gateway
// consumes. The gateway schema rejects any extra keys under tools.web, so no per-provider map
// is persisted; unsaved keys for other providers live only in-memory for quick switching.
Expand All @@ -1785,18 +1810,36 @@ async function persistSearchConfig(): Promise<void> {
// Single save for the Web search section: commits both provider keys and the selected default
// provider in one action. The gateway hot-reloads the tools config on write.
async function saveSearchSettings(): Promise<void> {
searchSavingAll.value = true;
searchSaveStatus.value = "saving";
try {
await persistSearchConfig();
snapshotSearchConfig();
ElMessage.success(t("settings.searchSettingsSaved"));
searchSaveError.value = "";
searchSaveStatus.value = "saved";
} catch (err: any) {
searchSaveStatus.value = "error";
ElMessage.error(t("settings.saveFailed", { error: err.message || err }));
} finally {
searchSavingAll.value = false;
}
}

async function handleSearchProviderChange(): Promise<void> {
const validationError = validateActiveSearchKey();
if (validationError) {
setSearchValidationError(validationError);
return;
}
await saveSearchSettings();
}

async function saveSearchKeyIfReady(): Promise<void> {
const validationError = validateActiveSearchKey();
if (validationError) {
setSearchValidationError(validationError);
return;
}
await saveSearchSettings();
}

function openExternal(url: string) {
window.openclaw.shell.openExternal(url);
}
Expand Down
Loading