feat(desktop): polish desktop shell and shared WebUI surfaces (#4195)

* feat(desktop): add native host scaffold

* feat(webui): track turns and usage in gateway

* feat(webui): polish desktop chat experience

* feat(apps): add ArcGIS and Joplin logos

* feat(desktop): polish shell and shared surfaces

* fix(webui): avoid preview chips for glob references

* test: align CI expectations for token fallback

* feat(webui): preview prompt rail entries

* feat(webui): add prompt navigator drawer

* style(webui): refine prompt navigator placement

* style(webui): align prompt navigator with header actions

* style(webui): simplify prompt navigator header

* refactor(webui): clean thread resource refresh

* feat(desktop): add native reply notifications

* fix(webui): preserve desktop restart and replay state

* fix(desktop): harden gateway proxy startup

* fix(web): fall back when readability is unavailable

* fix(desktop): hide window instead of closing on macos

* fix(webui): unify desktop header actions

* fix(webui): simplify prompt history rows

* fix(desktop): log notification delivery failures

* chore(desktop): clean source package artifacts

* fix(cron): support one-time relative reminders

* fix(webui): reveal scroll button in place

* Revert "fix(cron): support one-time relative reminders"

This reverts commit 4c4661da120a3c7283e0768412bae48604e7390b.

* refactor(webui): extract token usage heatmap

* docs(desktop): clarify contributor guides

---------

Co-authored-by: chengyongru <2755839590@qq.com>
This commit is contained in:
Xubin Ren
2026-06-06 19:49:33 +08:00
committed by GitHub
co-authored by chengyongru
parent a1b9577224
commit ab9f49970d
103 changed files with 10483 additions and 1003 deletions
+386 -1
View File
@@ -118,8 +118,9 @@ const installedAnyGen = {
function renderSettingsView(
options: {
initialSection?: "apps" | "advanced" | "models";
initialSection?: "overview" | "apps" | "advanced" | "models";
onSettingsChange?: (payload: SettingsPayload) => void;
onNativeEngineRestart?: () => Promise<string>;
} = {},
) {
render(
@@ -131,6 +132,7 @@ function renderSettingsView(
onBackToChat={() => {}}
onModelNameChange={() => {}}
onSettingsChange={options.onSettingsChange}
onNativeEngineRestart={options.onNativeEngineRestart}
/>
</ClientProvider>,
);
@@ -219,6 +221,55 @@ describe("SettingsView Apps catalog", () => {
await waitFor(() => expect(onSettingsChange).toHaveBeenCalledWith(payload));
});
it("shows token activity on the overview", async () => {
const payload: SettingsPayload = {
...settingsPayload(),
usage: {
days: [
{
date: "2026-06-03",
prompt_tokens: 1200,
completion_tokens: 300,
cached_tokens: 500,
total_tokens: 1500,
requests: 2,
},
],
total_tokens: 1500,
total_tokens_30d: 1500,
total_tokens_365d: 1500,
peak_day_tokens: 1500,
current_streak_days: 1,
longest_streak_days: 1,
active_days_30d: 1,
requests_30d: 2,
updated_at: "2026-06-03T00:00:00Z",
},
};
vi.stubGlobal(
"fetch",
vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url === "/api/settings") return jsonResponse(payload);
if (url === "/api/settings/cli-apps") {
return jsonResponse({ apps: [], installed_count: 0 });
}
if (url === "/api/settings/mcp-presets") {
return jsonResponse({ presets: [], installed_count: 0 });
}
return { ok: false, status: 404, json: async () => ({}) } as Response;
}),
);
renderSettingsView({ initialSection: "overview" });
expect(await screen.findByLabelText("Token activity")).toBeInTheDocument();
expect(screen.getByText("Token Usage")).toBeInTheDocument();
expect(screen.queryByText("Token activity")).not.toBeInTheDocument();
expect(screen.queryByText("Total tokens")).not.toBeInTheDocument();
expect(screen.queryByText("Peak tokens")).not.toBeInTheDocument();
});
it("shows context window options in model settings", async () => {
vi.stubGlobal(
"fetch",
@@ -242,6 +293,280 @@ describe("SettingsView Apps catalog", () => {
expect(screen.getByRole("button", { name: "256K" })).toBeInTheDocument();
});
it("marks the current model as unconfigured when its provider needs setup", async () => {
const payload: SettingsPayload = {
...settingsPayload(),
agent: {
...settingsPayload().agent,
model: "openai-codex/gpt-5.1-codex",
provider: "openai_codex",
resolved_provider: "openai_codex",
has_api_key: false,
},
model_presets: [
{
...settingsPayload().model_presets[0],
model: "openai-codex/gpt-5.1-codex",
provider: "openai_codex",
},
],
providers: [
{
name: "openai_codex",
label: "OpenAI Codex",
configured: false,
auth_type: "oauth",
api_key_required: false,
api_key_hint: null,
api_base: null,
default_api_base: null,
oauth_account: null,
oauth_expires_at: null,
oauth_login_supported: true,
},
],
};
vi.stubGlobal(
"fetch",
vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url === "/api/settings") return jsonResponse(payload);
if (url === "/api/settings/cli-apps") {
return jsonResponse({ apps: [], installed_count: 0 });
}
if (url === "/api/settings/mcp-presets") {
return jsonResponse({ presets: [], installed_count: 0 });
}
return { ok: false, status: 404, json: async () => ({}) } as Response;
}),
);
renderSettingsView({ initialSection: "models" });
const configurationButton = await screen.findByRole("button", {
name: "Current configuration",
});
expect(configurationButton).toHaveTextContent("Not configured");
expect(configurationButton).toHaveTextContent("OpenAI Codex · openai-codex/gpt-5.1-codex");
expect(await screen.findByRole("button", { name: "Sign in" })).toBeInTheDocument();
});
it("keeps unsigned OAuth providers out of the active provider picker", async () => {
const payload: SettingsPayload = {
...settingsPayload(),
agent: {
...settingsPayload().agent,
model: "deepseek-chat",
provider: "deepseek",
resolved_provider: "deepseek",
},
model_presets: [
{
...settingsPayload().model_presets[0],
model: "deepseek-chat",
provider: "deepseek",
},
],
providers: [
{
name: "deepseek",
label: "DeepSeek",
configured: true,
auth_type: "api_key",
api_key_required: true,
api_key_hint: "sk-...",
api_base: "https://api.deepseek.com",
default_api_base: "https://api.deepseek.com",
},
{
name: "openai_codex",
label: "OpenAI Codex",
configured: false,
auth_type: "oauth",
api_key_required: false,
api_key_hint: null,
api_base: null,
default_api_base: null,
oauth_account: null,
oauth_expires_at: null,
oauth_login_supported: true,
},
{
name: "github_copilot",
label: "GitHub Copilot",
configured: false,
auth_type: "oauth",
api_key_required: false,
api_key_hint: null,
api_base: null,
default_api_base: "https://api.githubcopilot.com",
oauth_account: null,
oauth_expires_at: null,
oauth_login_supported: true,
},
],
};
vi.stubGlobal(
"fetch",
vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url === "/api/settings") return jsonResponse(payload);
if (url === "/api/settings/cli-apps") {
return jsonResponse({ apps: [], installed_count: 0 });
}
if (url === "/api/settings/mcp-presets") {
return jsonResponse({ presets: [], installed_count: 0 });
}
return { ok: false, status: 404, json: async () => ({}) } as Response;
}),
);
renderSettingsView({ initialSection: "models" });
const deepseekButtons = await screen.findAllByRole("button", { name: /DeepSeek/ });
const providerPicker = deepseekButtons.find(
(button) => button.getAttribute("aria-haspopup") === "menu",
);
if (!providerPicker) throw new Error("provider picker was not found");
fireEvent.pointerDown(providerPicker);
expect(await screen.findByRole("menuitem", { name: /DeepSeek/ })).toBeInTheDocument();
expect(screen.queryByRole("menuitem", { name: /OpenAI Codex/ })).not.toBeInTheDocument();
expect(screen.queryByRole("menuitem", { name: /GitHub Copilot/ })).not.toBeInTheDocument();
});
it("does not fetch model lists for unsigned OAuth providers", async () => {
const payload: SettingsPayload = {
...settingsPayload(),
agent: {
...settingsPayload().agent,
model: "",
provider: "openai_codex",
resolved_provider: "openai_codex",
},
model_presets: [
{
...settingsPayload().model_presets[0],
model: "",
provider: "openai_codex",
},
],
providers: [
{
name: "openai_codex",
label: "OpenAI Codex",
configured: false,
auth_type: "oauth",
api_key_required: false,
api_key_hint: null,
api_base: null,
default_api_base: null,
oauth_account: null,
oauth_expires_at: null,
oauth_login_supported: true,
},
{
name: "github_copilot",
label: "GitHub Copilot",
configured: false,
auth_type: "oauth",
api_key_required: false,
api_key_hint: null,
api_base: null,
default_api_base: "https://api.githubcopilot.com",
oauth_account: null,
oauth_expires_at: null,
oauth_login_supported: true,
},
],
};
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url === "/api/settings") return jsonResponse(payload);
if (url === "/api/settings/cli-apps") {
return jsonResponse({ apps: [], installed_count: 0 });
}
if (url === "/api/settings/mcp-presets") {
return jsonResponse({ presets: [], installed_count: 0 });
}
return { ok: false, status: 404, json: async () => ({}) } as Response;
});
vi.stubGlobal("fetch", fetchMock);
renderSettingsView({ initialSection: "models" });
fireEvent.pointerDown(await screen.findByRole("button", { name: /Select model/i }));
expect(
await screen.findByText("Configure this provider before loading models."),
).toBeInTheDocument();
expect(
fetchMock.mock.calls.some(([input]) =>
String(input).startsWith("/api/settings/provider-models"),
),
).toBe(false);
});
it("prefills manual model ids for configured OAuth providers", async () => {
const payload: SettingsPayload = {
...settingsPayload(),
agent: {
...settingsPayload().agent,
model: "open-codex/gpt-5.5",
provider: "openai_codex",
resolved_provider: "openai_codex",
},
model_presets: [
{
...settingsPayload().model_presets[0],
model: "open-codex/gpt-5.5",
provider: "openai_codex",
},
],
providers: [
{
name: "openai_codex",
label: "OpenAI Codex",
configured: true,
auth_type: "oauth",
api_key_required: false,
api_key_hint: null,
api_base: null,
default_api_base: null,
oauth_account: "acct-test",
oauth_expires_at: null,
oauth_login_supported: true,
},
],
};
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url === "/api/settings") return jsonResponse(payload);
if (url === "/api/settings/cli-apps") {
return jsonResponse({ apps: [], installed_count: 0 });
}
if (url === "/api/settings/mcp-presets") {
return jsonResponse({ presets: [], installed_count: 0 });
}
return { ok: false, status: 404, json: async () => ({}) } as Response;
});
vi.stubGlobal("fetch", fetchMock);
renderSettingsView({ initialSection: "models" });
const modelButtons = await screen.findAllByRole("button", { name: /open-codex\/gpt-5\.5/i });
fireEvent.pointerDown(modelButtons[modelButtons.length - 1]);
const input = (await screen.findByPlaceholderText("Search or type model ID")) as HTMLInputElement;
expect(input.value).toBe("open-codex/gpt-5.5");
fireEvent.change(input, { target: { value: "openai-codex/gpt-5.5" } });
expect(await screen.findByText("“openai-codex/gpt-5.5”")).toBeInTheDocument();
expect(
fetchMock.mock.calls.some(([input]) =>
String(input).startsWith("/api/settings/provider-models"),
),
).toBe(false);
});
it("can close the new configuration dialog without trapping the settings page", async () => {
vi.stubGlobal(
"fetch",
@@ -443,4 +768,64 @@ describe("SettingsView Apps catalog", () => {
expect(screen.queryByText("Web safety")).not.toBeInTheDocument();
expect(screen.getByText("Allow Full Access shell commands to reach services on this Mac.")).toBeInTheDocument();
});
it("refreshes settings with a fresh token after native engine restart", async () => {
const payload = {
...settingsPayload(),
surface: "native" as const,
runtime_surface: "native" as const,
runtime_capabilities: {
can_restart_engine: true,
can_pick_folder: true,
can_open_logs: true,
can_export_diagnostics: true,
},
};
const restartedPayload = {
...payload,
advanced: { ...payload.advanced, webui_allow_local_service_access: false },
requires_restart: true,
restart_required_sections: ["runtime"],
};
const refreshedPayload = {
...restartedPayload,
requires_restart: false,
restart_required_sections: [],
};
const restartEngine = vi.fn(async () => "fresh-token");
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
const auth = (init?.headers as Record<string, string> | undefined)?.Authorization;
if (url === "/api/settings" && auth === "Bearer fresh-token") {
return jsonResponse(refreshedPayload);
}
if (url === "/api/settings") return jsonResponse(payload);
if (url === "/api/settings/cli-apps") return jsonResponse({ apps: [], installed_count: 0 });
if (url === "/api/settings/mcp-presets") return jsonResponse({ presets: [], installed_count: 0 });
if (url === "/api/settings/network-safety/update?webui_allow_local_service_access=false&webui_default_access_mode=default") {
return jsonResponse(restartedPayload);
}
return { ok: false, status: 404, json: async () => ({}) } as Response;
});
vi.stubGlobal("fetch", fetchMock);
renderSettingsView({
initialSection: "advanced",
onNativeEngineRestart: restartEngine,
});
expect(await screen.findByText("App safety")).toBeInTheDocument();
fireEvent.click(screen.getByRole("switch", { name: "Local services" }));
fireEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => expect(restartEngine).toHaveBeenCalledTimes(1));
await waitFor(() =>
expect(fetchMock).toHaveBeenCalledWith(
"/api/settings",
expect.objectContaining({
headers: { Authorization: "Bearer fresh-token" },
}),
),
);
});
});