Skip to content
Merged
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
4 changes: 2 additions & 2 deletions desktop/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
"check:css": "node scripts/check-css-syntax.mjs src/styles.css && node scripts/check-z-index-tokens.mjs src/styles.css",
"test:todo-visibility": "node scripts/test-todo-visibility.mjs",
"typecheck": "tsc --noEmit",
"test": "node scripts/test-todo-visibility.mjs && tsx src/__tests__/anchored-popover-scroll.test.tsx && tsx src/__tests__/math-golden.test.ts && tsx src/__tests__/text-size.test.ts && tsx src/__tests__/font-availability.test.ts && tsx src/__tests__/theme-auto-background.test.ts && tsx src/__tests__/typography-overflow-contract.test.ts && tsx src/__tests__/provider-model-refresh.test.ts && tsx src/__tests__/composer-profile.test.ts && tsx src/__tests__/composer-history.test.ts && tsx src/__tests__/composer-keyboard.test.ts && tsx src/__tests__/use-controller-meta.test.ts && tsx src/__tests__/workspace-layout.test.ts && tsx src/__tests__/tool-approval-mode.test.ts && tsx src/__tests__/keyboard-shortcuts.test.ts && tsx src/__tests__/send-failed.test.ts && tsx src/__tests__/edit-replay.test.ts && tsx src/__tests__/history-tool-status.test.ts && tsx src/__tests__/message-selection-copy.test.ts && tsx src/__tests__/reasoning-display.test.ts && tsx src/__tests__/attachment-display.test.ts && tsx src/__tests__/crash-reporting.test.ts && tsx src/__tests__/bridge-drag-rejection.test.ts && tsx src/__tests__/command-palette-css.test.ts && tsx src/__tests__/context-panel-breakdown.test.ts && tsx src/__tests__/statusbar-workspace.test.tsx && tsx src/__tests__/capabilities-panel-actions.test.ts && tsx src/__tests__/tool-data-archive.test.ts && tsx src/__tests__/diff-rendering.test.ts && tsx src/__tests__/render-optimization.test.ts && tsx src/__tests__/transcript-grouping.test.ts && tsx src/__tests__/project-tree-runtime.test.ts && tsx src/__tests__/app-chrome-tabs.test.ts",
"test": "node scripts/test-todo-visibility.mjs && tsx src/__tests__/anchored-popover-scroll.test.tsx && tsx src/__tests__/math-golden.test.ts && tsx src/__tests__/text-size.test.ts && tsx src/__tests__/font-availability.test.ts && tsx src/__tests__/theme-auto-background.test.ts && tsx src/__tests__/typography-overflow-contract.test.ts && tsx src/__tests__/provider-model-refresh.test.ts && tsx src/__tests__/composer-profile.test.ts && tsx src/__tests__/composer-history.test.ts && tsx src/__tests__/composer-keyboard.test.ts && tsx src/__tests__/use-controller-meta.test.ts && tsx src/__tests__/workspace-layout.test.ts && tsx src/__tests__/tool-approval-mode.test.ts && tsx src/__tests__/keyboard-shortcuts.test.ts && tsx src/__tests__/send-failed.test.ts && tsx src/__tests__/edit-replay.test.ts && tsx src/__tests__/history-tool-status.test.ts && tsx src/__tests__/message-selection-copy.test.ts && tsx src/__tests__/reasoning-display.test.ts && tsx src/__tests__/attachment-display.test.ts && tsx src/__tests__/crash-reporting.test.ts && tsx src/__tests__/bridge-drag-rejection.test.ts && tsx src/__tests__/command-palette-css.test.ts && tsx src/__tests__/context-panel-breakdown.test.ts && tsx src/__tests__/statusbar-workspace.test.tsx && tsx src/__tests__/settings-refresh-snapshot.test.tsx && tsx src/__tests__/capabilities-panel-actions.test.ts && tsx src/__tests__/tool-data-archive.test.ts && tsx src/__tests__/diff-rendering.test.ts && tsx src/__tests__/render-optimization.test.ts && tsx src/__tests__/transcript-grouping.test.ts && tsx src/__tests__/project-tree-runtime.test.ts && tsx src/__tests__/app-chrome-tabs.test.ts",
"test:typecheck": "tsc --noEmit -p tsconfig.test.json",
"test:all": "tsc --noEmit -p tsconfig.test.json && node scripts/test-todo-visibility.mjs && tsx src/__tests__/anchored-popover-scroll.test.tsx && tsx src/__tests__/math-golden.test.ts && tsx src/__tests__/text-size.test.ts && tsx src/__tests__/font-availability.test.ts && tsx src/__tests__/theme-auto-background.test.ts && tsx src/__tests__/typography-overflow-contract.test.ts && tsx src/__tests__/provider-model-refresh.test.ts && tsx src/__tests__/composer-profile.test.ts && tsx src/__tests__/composer-history.test.ts && tsx src/__tests__/composer-keyboard.test.ts && tsx src/__tests__/use-controller-meta.test.ts && tsx src/__tests__/workspace-layout.test.ts && tsx src/__tests__/tool-approval-mode.test.ts && tsx src/__tests__/keyboard-shortcuts.test.ts && tsx src/__tests__/send-failed.test.ts && tsx src/__tests__/edit-replay.test.ts && tsx src/__tests__/history-tool-status.test.ts && tsx src/__tests__/message-selection-copy.test.ts && tsx src/__tests__/reasoning-display.test.ts && tsx src/__tests__/attachment-display.test.ts && tsx src/__tests__/crash-reporting.test.ts && tsx src/__tests__/bridge-drag-rejection.test.ts && tsx src/__tests__/command-palette-css.test.ts && tsx src/__tests__/context-panel-breakdown.test.ts && tsx src/__tests__/statusbar-workspace.test.tsx && tsx src/__tests__/capabilities-panel-actions.test.ts && tsx src/__tests__/tool-data-archive.test.ts && tsx src/__tests__/diff-rendering.test.ts && tsx src/__tests__/render-optimization.test.ts && tsx src/__tests__/transcript-grouping.test.ts && tsx src/__tests__/project-tree-runtime.test.ts && tsx src/__tests__/app-chrome-tabs.test.ts"
"test:all": "tsc --noEmit -p tsconfig.test.json && node scripts/test-todo-visibility.mjs && tsx src/__tests__/anchored-popover-scroll.test.tsx && tsx src/__tests__/math-golden.test.ts && tsx src/__tests__/text-size.test.ts && tsx src/__tests__/font-availability.test.ts && tsx src/__tests__/theme-auto-background.test.ts && tsx src/__tests__/typography-overflow-contract.test.ts && tsx src/__tests__/provider-model-refresh.test.ts && tsx src/__tests__/composer-profile.test.ts && tsx src/__tests__/composer-history.test.ts && tsx src/__tests__/composer-keyboard.test.ts && tsx src/__tests__/use-controller-meta.test.ts && tsx src/__tests__/workspace-layout.test.ts && tsx src/__tests__/tool-approval-mode.test.ts && tsx src/__tests__/keyboard-shortcuts.test.ts && tsx src/__tests__/send-failed.test.ts && tsx src/__tests__/edit-replay.test.ts && tsx src/__tests__/history-tool-status.test.ts && tsx src/__tests__/message-selection-copy.test.ts && tsx src/__tests__/reasoning-display.test.ts && tsx src/__tests__/attachment-display.test.ts && tsx src/__tests__/crash-reporting.test.ts && tsx src/__tests__/bridge-drag-rejection.test.ts && tsx src/__tests__/command-palette-css.test.ts && tsx src/__tests__/context-panel-breakdown.test.ts && tsx src/__tests__/statusbar-workspace.test.tsx && tsx src/__tests__/settings-refresh-snapshot.test.tsx && tsx src/__tests__/capabilities-panel-actions.test.ts && tsx src/__tests__/tool-data-archive.test.ts && tsx src/__tests__/diff-rendering.test.ts && tsx src/__tests__/render-optimization.test.ts && tsx src/__tests__/transcript-grouping.test.ts && tsx src/__tests__/project-tree-runtime.test.ts && tsx src/__tests__/app-chrome-tabs.test.ts"
},
"dependencies": {
"@gsap/react": "^2.1.2",
Expand Down
13 changes: 12 additions & 1 deletion desktop/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,12 @@ export default function App() {
setImTopicSources(sidebarImTopicSourcesFromBot(settings.bot, t));
}, [t]);

const refreshSidebarImConnectionsFromSettings = useCallback(async (settings: Pick<SettingsView, "bot">) => {
const runtimeStatus = await loadBotRuntimeStatus();
setSidebarImConnections(sidebarImConnectionsFromBot(settings.bot, t, runtimeStatus));
setImTopicSources(sidebarImTopicSourcesFromBot(settings.bot, t));
}, [t]);

const openBotSettings = useCallback(() => {
closeTransientOverlays();
setSidebarImDetailConnectionId("");
Expand Down Expand Up @@ -2986,8 +2992,13 @@ export default function App() {
setSettingsFocus(null);
setSettingsTarget(null);
}}
onChanged={() => {
onChanged={(settings) => {
void refreshMeta();
if (settings) {
applyDesktopPreferences(settings);
void refreshSidebarImConnectionsFromSettings(settings).catch((e) => console.warn("bot sidebar refresh failed", e));
return;
}
void reloadSidebarImConnections().catch((e) => console.warn("bot sidebar refresh failed", e));
void app.Settings()
.then(applyDesktopPreferences)
Expand Down
154 changes: 154 additions & 0 deletions desktop/frontend/src/__tests__/settings-refresh-snapshot.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Run: tsx src/__tests__/settings-refresh-snapshot.test.tsx

import { JSDOM } from "jsdom";
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { SettingsPanel } from "../components/SettingsPanel";
import { LocaleProvider } from "../lib/i18n";
import type { AppBindings } from "../lib/bridge";
import type { SettingsView } from "../lib/types";

let passed = 0;
let failed = 0;

function ok(value: boolean, label: string) {
if (value) {
process.stdout.write(` PASS ${label}\n`);
passed += 1;
} else {
process.stdout.write(` FAIL ${label}\n`);
failed += 1;
}
}

function eq(actual: unknown, expected: unknown, label: string) {
if (actual === expected) {
ok(true, label);
} else {
ok(false, `${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
}
}

function flushPromises(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 0));
}

function baseSettings(displayMode: "standard" | "compact" = "standard"): SettingsView {
return {
defaultModel: "",
plannerModel: "",
subagentModel: "",
subagentEffort: "",
autoPlan: "off",
providers: [],
officialProviders: [],
permissions: { mode: "ask", allow: [], ask: [], deny: [] },
sandbox: { bash: "enforce", network: false, workspaceRoot: "", allowWrite: [], shell: "auto" },
network: { proxyMode: "auto", proxyUrl: "", noProxy: "", proxy: { type: "socks5", server: "", port: 0, username: "", password: "" } },
agent: { temperature: 0, maxSteps: 0, plannerMaxSteps: 12, systemPrompt: "", coldResumePrune: true, reasoningLanguage: "auto" },
bot: {
enabled: false,
model: "",
toolApprovalMode: "",
maxSteps: 0,
debounceMs: 0,
allowlist: { enabled: false, allowAll: false, qqUsers: [], feishuUsers: [], weixinUsers: [], qqGroups: [], feishuGroups: [], weixinGroups: [] },
qq: { enabled: false, appId: "", appSecretEnv: "", secretSet: false, sandbox: false },
feishu: { enabled: false, domain: "feishu", appId: "", appSecretEnv: "", secretSet: false, verificationToken: "", mode: "webhook", webhookPort: 0, requireMention: false },
weixin: { enabled: false, accountId: "", tokenEnv: "", tokenSet: false, apiBase: "" },
connections: [],
},
desktopLanguage: "en",
desktopLayoutStyle: "workbench",
desktopTheme: "auto",
desktopThemeStyle: "graphite",
closeBehavior: "background",
displayMode,
statusBarStyle: "text",
statusBarItems: ["model", "workspace", "git_branch", "cache", "balance"],
checkUpdates: true,
telemetry: true,
metrics: true,
configPath: "/tmp/reasonix/config.toml",
providerKinds: [],
autoApproveTools: false,
bypass: false,
};
}

console.log("\nsettings refresh snapshot");

const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
pretendToBeVisual: true,
url: "http://localhost/",
});
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
globalThis.window = dom.window as unknown as Window & typeof globalThis;
globalThis.document = dom.window.document;
Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
globalThis.Node = dom.window.Node;
globalThis.HTMLElement = dom.window.HTMLElement;
globalThis.Event = dom.window.Event;
globalThis.CustomEvent = dom.window.CustomEvent;
globalThis.KeyboardEvent = dom.window.KeyboardEvent;
globalThis.MouseEvent = dom.window.MouseEvent;
globalThis.localStorage = dom.window.localStorage;
globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
window.scrollTo = () => {};

const settingsSnapshots = [baseSettings("standard"), baseSettings("compact")];
let settingsCalls = 0;
let setDisplayModeCalls = 0;
let onChangedSettings: SettingsView | undefined;

window.go = {
main: {
App: {
Settings: async () => settingsSnapshots[Math.min(settingsCalls++, settingsSnapshots.length - 1)],
SetDisplayMode: async () => {
setDisplayModeCalls += 1;
},
} as Partial<AppBindings> as AppBindings,
},
};

const rootEl = document.getElementById("root");
if (!rootEl) throw new Error("missing root");
const root = createRoot(rootEl);

await act(async () => {
root.render(
<LocaleProvider>
<SettingsPanel
initialTab="general"
onClose={() => {}}
onChanged={(settings?: SettingsView) => {
onChangedSettings = settings;
}}
/>
</LocaleProvider>,
);
await flushPromises();
});

const compactButton = Array.from(document.querySelectorAll("button")).find((button) => button.textContent?.trim() === "Compact") as HTMLButtonElement | undefined;
if (!compactButton) throw new Error("compact display mode button did not render");

await act(async () => {
compactButton.click();
await flushPromises();
});

eq(setDisplayModeCalls, 1, "display mode mutation is invoked once");
eq(settingsCalls, 2, "settings panel reads Settings only for initial load and post-save reload");
ok(onChangedSettings?.displayMode === "compact", "onChanged receives the post-save SettingsView snapshot");

await act(async () => {
root.unmount();
});
dom.window.close();

console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
if (failed > 0) process.exit(1);
18 changes: 11 additions & 7 deletions desktop/frontend/src/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function SettingsPanel({
agentRunning = false,
}: {
onClose: () => void;
onChanged: () => void;
onChanged: (settings?: SettingsView | null) => void;
initialTab?: SettingsTab;
initialFocus?: SettingsInitialFocus;
agentRunning?: boolean;
Expand All @@ -92,7 +92,11 @@ export function SettingsPanel({
// Play the modal exit animation, then let the parent unmount us.
const { status, requestClose } = useDeferredClose(onClose, 240);

const reload = async () => setS(normalizeSettingsView(await app.Settings().catch(() => null)));
const reload = async () => {
const next = normalizeSettingsView(await app.Settings().catch(() => null));
setS(next);
return next;
};
useEffect(() => {
void reload();
if (initialTab) setTab(initialTab === "providers" ? "models" : initialTab);
Expand All @@ -112,8 +116,8 @@ export function SettingsPanel({
setWarning(null);
try {
const result = await fn();
await reload();
onChanged();
const next = await reload();
onChanged(next);
if (typeof result === "string" && result.trim()) {
setWarning(result.trim());
}
Expand All @@ -128,8 +132,8 @@ export function SettingsPanel({
setWarning(null);
try {
await fn();
await reload();
onChanged();
const next = await reload();
onChanged(next);
} catch (e) {
setErr(String((e as Error)?.message ?? e));
}
Expand Down Expand Up @@ -4744,7 +4748,7 @@ function ruleListHint(list: string, t: ReturnType<typeof useT>): string {

type HookScope = "global" | "project";

function HooksSection({ onChanged }: { onChanged: () => void }) {
function HooksSection({ onChanged }: { onChanged: (settings?: SettingsView | null) => void }) {
const t = useT();
const [scope, setScope] = useState<HookScope>("global");
const [view, setView] = useState<HooksSettingsView | null>(null);
Expand Down