Skip to content

Commit a79a534

Browse files
Copilotsawka
andauthored
Refine global atom split: keep atom init in global-atoms.ts, move About-menu wiring back to global.ts (#2900)
This refactor keeps `global-atoms.ts` as the owner of atom state/initialization while correcting ownership boundaries for UI/event wiring. Specifically, the About menu handler was moved out of atom initialization and back into `global.ts` startup orchestration. - **Context / intent** - `global-atoms.ts` should own atom construction and registries only. - `global.ts` should orchestrate app-level startup side effects and re-export compatibility APIs. - **What changed** - **`global-atoms.ts`** - Removed `onMenuItemAbout` registration from `initGlobalAtoms`. - Removed `modalsModel` import (no longer needed; improves layering). - Continued to own atom state (`atoms`), atom init (`initGlobalAtoms`), and atom registries/caches. - **`global.ts`** - Added `onMenuItemAbout` registration to `initGlobal` immediately after `initGlobalAtoms(initOpts)`. - Keeps UI-level event wiring with global startup orchestration. - **Boundary after this change** - `global-atoms.ts`: atom ownership + atom graph init. - `global.ts`: runtime orchestration + menu/event side effects + compatibility re-exports. ```ts // global.ts function initGlobal(initOpts: GlobalInitOptions) { globalEnvironment = initOpts.environment; globalPrimaryTabStartup = initOpts.primaryTabStartup ?? false; setPlatform(initOpts.platform); initGlobalAtoms(initOpts); try { getApi().onMenuItemAbout(() => { modalsModel.pushModal("AboutModal"); }); } catch (e) { console.log("failed to initialize onMenuItemAbout handler", e); } } ``` <!-- START COPILOT CODING AGENT TIPS --> --- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs. --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: sawka <[email protected]>
1 parent 386faf7 commit a79a534

4 files changed

Lines changed: 188 additions & 148 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright 2025, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { describe, expect, it } from "vitest";
5+
import { getAtoms } from "./global-atoms";
6+
7+
describe("global-atoms", () => {
8+
it("throws before initialization", () => {
9+
expect(() => getAtoms()).toThrow("Global atoms accessed before initialization");
10+
});
11+
});

frontend/app/store/global-atoms.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Copyright 2025, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { atom, Atom, PrimitiveAtom } from "jotai";
5+
import { globalStore } from "./jotaiStore";
6+
import * as WOS from "./wos";
7+
8+
let atoms!: GlobalAtomsType;
9+
const blockComponentModelMap = new Map<string, BlockComponentModel>();
10+
const ConnStatusMapAtom = atom(new Map<string, PrimitiveAtom<ConnStatus>>());
11+
const TabIndicatorMap = new Map<string, PrimitiveAtom<TabIndicator>>();
12+
const orefAtomCache = new Map<string, Map<string, Atom<any>>>();
13+
14+
function initGlobalAtoms(initOpts: GlobalInitOptions) {
15+
const windowIdAtom = atom(initOpts.windowId) as PrimitiveAtom<string>;
16+
const builderIdAtom = atom(initOpts.builderId) as PrimitiveAtom<string>;
17+
const builderAppIdAtom = atom<string>(null) as PrimitiveAtom<string>;
18+
const waveWindowTypeAtom = atom((get) => {
19+
const builderId = get(builderIdAtom);
20+
return builderId != null ? "builder" : "tab";
21+
}) as Atom<"tab" | "builder">;
22+
const uiContextAtom = atom((get) => {
23+
const uiContext: UIContext = {
24+
windowid: initOpts.windowId,
25+
activetabid: initOpts.tabId,
26+
};
27+
return uiContext;
28+
}) as Atom<UIContext>;
29+
30+
const isFullScreenAtom = atom(false) as PrimitiveAtom<boolean>;
31+
try {
32+
getApi().onFullScreenChange((isFullScreen) => {
33+
globalStore.set(isFullScreenAtom, isFullScreen);
34+
});
35+
} catch (e) {
36+
console.log("failed to initialize isFullScreenAtom", e);
37+
}
38+
39+
const zoomFactorAtom = atom(1.0) as PrimitiveAtom<number>;
40+
try {
41+
globalStore.set(zoomFactorAtom, getApi().getZoomFactor());
42+
getApi().onZoomFactorChange((zoomFactor) => {
43+
globalStore.set(zoomFactorAtom, zoomFactor);
44+
});
45+
} catch (e) {
46+
console.log("failed to initialize zoomFactorAtom", e);
47+
}
48+
49+
const workspaceAtom: Atom<Workspace> = atom((get) => {
50+
const windowData = WOS.getObjectValue<WaveWindow>(WOS.makeORef("window", get(windowIdAtom)), get);
51+
if (windowData == null) {
52+
return null;
53+
}
54+
return WOS.getObjectValue(WOS.makeORef("workspace", windowData.workspaceid), get);
55+
});
56+
const fullConfigAtom = atom(null) as PrimitiveAtom<FullConfigType>;
57+
const waveaiModeConfigAtom = atom(null) as PrimitiveAtom<Record<string, AIModeConfigType>>;
58+
const settingsAtom = atom((get) => {
59+
return get(fullConfigAtom)?.settings ?? {};
60+
}) as Atom<SettingsType>;
61+
const hasCustomAIPresetsAtom = atom((get) => {
62+
const fullConfig = get(fullConfigAtom);
63+
if (!fullConfig?.presets) {
64+
return false;
65+
}
66+
for (const presetId in fullConfig.presets) {
67+
if (presetId.startsWith("ai@") && presetId !== "ai@global" && presetId !== "ai@wave") {
68+
return true;
69+
}
70+
}
71+
return false;
72+
}) as Atom<boolean>;
73+
// this is *the* tab that this tabview represents. it should never change.
74+
const staticTabIdAtom: Atom<string> = atom(initOpts.tabId);
75+
const controlShiftDelayAtom = atom(false);
76+
const updaterStatusAtom = atom<UpdaterStatus>("up-to-date") as PrimitiveAtom<UpdaterStatus>;
77+
try {
78+
globalStore.set(updaterStatusAtom, getApi().getUpdaterStatus());
79+
getApi().onUpdaterStatusChange((status) => {
80+
globalStore.set(updaterStatusAtom, status);
81+
});
82+
} catch (e) {
83+
console.log("failed to initialize updaterStatusAtom", e);
84+
}
85+
86+
const reducedMotionSettingAtom = atom((get) => get(settingsAtom)?.["window:reducedmotion"]);
87+
const reducedMotionSystemPreferenceAtom = atom(false);
88+
89+
// Composite of the prefers-reduced-motion media query and the window:reducedmotion user setting.
90+
const prefersReducedMotionAtom = atom((get) => {
91+
const reducedMotionSetting = get(reducedMotionSettingAtom);
92+
const reducedMotionSystemPreference = get(reducedMotionSystemPreferenceAtom);
93+
return reducedMotionSetting || reducedMotionSystemPreference;
94+
});
95+
96+
// Set up a handler for changes to the prefers-reduced-motion media query.
97+
if (globalThis.window != null) {
98+
const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
99+
globalStore.set(reducedMotionSystemPreferenceAtom, !reducedMotionQuery || reducedMotionQuery.matches);
100+
reducedMotionQuery?.addEventListener("change", () => {
101+
globalStore.set(reducedMotionSystemPreferenceAtom, reducedMotionQuery.matches);
102+
});
103+
}
104+
105+
const documentHasFocusAtom = atom(true) as PrimitiveAtom<boolean>;
106+
if (globalThis.window != null) {
107+
globalStore.set(documentHasFocusAtom, document.hasFocus());
108+
window.addEventListener("focus", () => {
109+
globalStore.set(documentHasFocusAtom, true);
110+
});
111+
window.addEventListener("blur", () => {
112+
globalStore.set(documentHasFocusAtom, false);
113+
});
114+
}
115+
116+
const modalOpen = atom(false);
117+
const allConnStatusAtom = atom<ConnStatus[]>((get) => {
118+
const connStatusMap = get(ConnStatusMapAtom);
119+
const connStatuses = Array.from(connStatusMap.values()).map((atom) => get(atom));
120+
return connStatuses;
121+
});
122+
const flashErrorsAtom = atom<FlashErrorType[]>([]);
123+
const notificationsAtom = atom<NotificationType[]>([]);
124+
const notificationPopoverModeAtom = atom<boolean>(false);
125+
const reinitVersion = atom(0);
126+
const rateLimitInfoAtom = atom(null) as PrimitiveAtom<RateLimitInfo>;
127+
atoms = {
128+
// initialized in wave.ts (will not be null inside of application)
129+
builderId: builderIdAtom,
130+
builderAppId: builderAppIdAtom,
131+
waveWindowType: waveWindowTypeAtom,
132+
uiContext: uiContextAtom,
133+
workspace: workspaceAtom,
134+
fullConfigAtom,
135+
waveaiModeConfigAtom,
136+
settingsAtom,
137+
hasCustomAIPresetsAtom,
138+
staticTabId: staticTabIdAtom,
139+
isFullScreen: isFullScreenAtom,
140+
zoomFactorAtom,
141+
controlShiftDelayAtom,
142+
updaterStatusAtom,
143+
prefersReducedMotionAtom,
144+
documentHasFocus: documentHasFocusAtom,
145+
modalOpen,
146+
allConnStatus: allConnStatusAtom,
147+
flashErrors: flashErrorsAtom,
148+
notifications: notificationsAtom,
149+
notificationPopoverMode: notificationPopoverModeAtom,
150+
reinitVersion,
151+
waveAIRateLimitInfoAtom: rateLimitInfoAtom,
152+
} as GlobalAtomsType;
153+
}
154+
155+
function getAtoms(): GlobalAtomsType {
156+
if (atoms == null) {
157+
throw new Error("Global atoms accessed before initialization");
158+
}
159+
return atoms;
160+
}
161+
162+
function getApi(): ElectronApi {
163+
return (window as any).api;
164+
}
165+
166+
export { atoms, blockComponentModelMap, ConnStatusMapAtom, getAtoms, initGlobalAtoms, orefAtomCache, TabIndicatorMap };

frontend/app/store/global.ts

Lines changed: 10 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -27,174 +27,35 @@ import {
2727
isWslConnName,
2828
} from "@/util/util";
2929
import { atom, Atom, PrimitiveAtom, useAtomValue } from "jotai";
30+
import {
31+
atoms,
32+
blockComponentModelMap,
33+
ConnStatusMapAtom,
34+
initGlobalAtoms,
35+
orefAtomCache,
36+
TabIndicatorMap,
37+
} from "./global-atoms";
3038
import { globalStore } from "./jotaiStore";
3139
import { modalsModel } from "./modalmodel";
3240
import { ClientService, ObjectService } from "./services";
3341
import * as WOS from "./wos";
3442
import { getFileSubject, waveEventSubscribeSingle } from "./wps";
3543

36-
let atoms: GlobalAtomsType;
3744
let globalEnvironment: "electron" | "renderer";
3845
let globalPrimaryTabStartup: boolean = false;
39-
const blockComponentModelMap = new Map<string, BlockComponentModel>();
40-
const ConnStatusMapAtom = atom(new Map<string, PrimitiveAtom<ConnStatus>>());
41-
const TabIndicatorMap = new Map<string, PrimitiveAtom<TabIndicator>>();
42-
const orefAtomCache = new Map<string, Map<string, Atom<any>>>();
4346

4447
function initGlobal(initOpts: GlobalInitOptions) {
4548
globalEnvironment = initOpts.environment;
4649
globalPrimaryTabStartup = initOpts.primaryTabStartup ?? false;
4750
setPlatform(initOpts.platform);
4851
initGlobalAtoms(initOpts);
49-
}
50-
51-
function initGlobalAtoms(initOpts: GlobalInitOptions) {
52-
const windowIdAtom = atom(initOpts.windowId) as PrimitiveAtom<string>;
53-
const builderIdAtom = atom(initOpts.builderId) as PrimitiveAtom<string>;
54-
const builderAppIdAtom = atom<string>(null) as PrimitiveAtom<string>;
55-
const waveWindowTypeAtom = atom((get) => {
56-
const builderId = get(builderIdAtom);
57-
return builderId != null ? "builder" : "tab";
58-
}) as Atom<"tab" | "builder">;
59-
const uiContextAtom = atom((get) => {
60-
const uiContext: UIContext = {
61-
windowid: initOpts.windowId,
62-
activetabid: initOpts.tabId,
63-
};
64-
return uiContext;
65-
}) as Atom<UIContext>;
66-
67-
const isFullScreenAtom = atom(false) as PrimitiveAtom<boolean>;
68-
try {
69-
getApi().onFullScreenChange((isFullScreen) => {
70-
globalStore.set(isFullScreenAtom, isFullScreen);
71-
});
72-
} catch (e) {
73-
console.log("failed to initialize isFullScreenAtom", e);
74-
}
75-
76-
const zoomFactorAtom = atom(1.0) as PrimitiveAtom<number>;
77-
try {
78-
globalStore.set(zoomFactorAtom, getApi().getZoomFactor());
79-
getApi().onZoomFactorChange((zoomFactor) => {
80-
globalStore.set(zoomFactorAtom, zoomFactor);
81-
});
82-
} catch (e) {
83-
console.log("failed to initialize zoomFactorAtom", e);
84-
}
85-
8652
try {
8753
getApi().onMenuItemAbout(() => {
8854
modalsModel.pushModal("AboutModal");
8955
});
9056
} catch (e) {
9157
console.log("failed to initialize onMenuItemAbout handler", e);
9258
}
93-
94-
const workspaceAtom: Atom<Workspace> = atom((get) => {
95-
const windowData = WOS.getObjectValue<WaveWindow>(WOS.makeORef("window", get(windowIdAtom)), get);
96-
if (windowData == null) {
97-
return null;
98-
}
99-
return WOS.getObjectValue(WOS.makeORef("workspace", windowData.workspaceid), get);
100-
});
101-
const fullConfigAtom = atom(null) as PrimitiveAtom<FullConfigType>;
102-
const waveaiModeConfigAtom = atom(null) as PrimitiveAtom<Record<string, AIModeConfigType>>;
103-
const settingsAtom = atom((get) => {
104-
return get(fullConfigAtom)?.settings ?? {};
105-
}) as Atom<SettingsType>;
106-
const hasCustomAIPresetsAtom = atom((get) => {
107-
const fullConfig = get(fullConfigAtom);
108-
if (!fullConfig?.presets) {
109-
return false;
110-
}
111-
for (const presetId in fullConfig.presets) {
112-
if (presetId.startsWith("ai@") && presetId !== "ai@global" && presetId !== "ai@wave") {
113-
return true;
114-
}
115-
}
116-
return false;
117-
}) as Atom<boolean>;
118-
// this is *the* tab that this tabview represents. it should never change.
119-
const staticTabIdAtom: Atom<string> = atom(initOpts.tabId);
120-
const controlShiftDelayAtom = atom(false);
121-
const updaterStatusAtom = atom<UpdaterStatus>("up-to-date") as PrimitiveAtom<UpdaterStatus>;
122-
try {
123-
globalStore.set(updaterStatusAtom, getApi().getUpdaterStatus());
124-
getApi().onUpdaterStatusChange((status) => {
125-
globalStore.set(updaterStatusAtom, status);
126-
});
127-
} catch (e) {
128-
console.log("failed to initialize updaterStatusAtom", e);
129-
}
130-
131-
const reducedMotionSettingAtom = atom((get) => get(settingsAtom)?.["window:reducedmotion"]);
132-
const reducedMotionSystemPreferenceAtom = atom(false);
133-
134-
// Composite of the prefers-reduced-motion media query and the window:reducedmotion user setting.
135-
const prefersReducedMotionAtom = atom((get) => {
136-
const reducedMotionSetting = get(reducedMotionSettingAtom);
137-
const reducedMotionSystemPreference = get(reducedMotionSystemPreferenceAtom);
138-
return reducedMotionSetting || reducedMotionSystemPreference;
139-
});
140-
141-
// Set up a handler for changes to the prefers-reduced-motion media query.
142-
if (globalThis.window != null) {
143-
const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
144-
globalStore.set(reducedMotionSystemPreferenceAtom, !reducedMotionQuery || reducedMotionQuery.matches);
145-
reducedMotionQuery?.addEventListener("change", () => {
146-
globalStore.set(reducedMotionSystemPreferenceAtom, reducedMotionQuery.matches);
147-
});
148-
}
149-
150-
const documentHasFocusAtom = atom(true) as PrimitiveAtom<boolean>;
151-
if (globalThis.window != null) {
152-
globalStore.set(documentHasFocusAtom, document.hasFocus());
153-
window.addEventListener("focus", () => {
154-
globalStore.set(documentHasFocusAtom, true);
155-
});
156-
window.addEventListener("blur", () => {
157-
globalStore.set(documentHasFocusAtom, false);
158-
});
159-
}
160-
161-
const modalOpen = atom(false);
162-
const allConnStatusAtom = atom<ConnStatus[]>((get) => {
163-
const connStatusMap = get(ConnStatusMapAtom);
164-
const connStatuses = Array.from(connStatusMap.values()).map((atom) => get(atom));
165-
return connStatuses;
166-
});
167-
const flashErrorsAtom = atom<FlashErrorType[]>([]);
168-
const notificationsAtom = atom<NotificationType[]>([]);
169-
const notificationPopoverModeAtom = atom<boolean>(false);
170-
const reinitVersion = atom(0);
171-
const rateLimitInfoAtom = atom(null) as PrimitiveAtom<RateLimitInfo>;
172-
atoms = {
173-
// initialized in wave.ts (will not be null inside of application)
174-
builderId: builderIdAtom,
175-
builderAppId: builderAppIdAtom,
176-
waveWindowType: waveWindowTypeAtom,
177-
uiContext: uiContextAtom,
178-
workspace: workspaceAtom,
179-
fullConfigAtom,
180-
waveaiModeConfigAtom,
181-
settingsAtom,
182-
hasCustomAIPresetsAtom,
183-
staticTabId: staticTabIdAtom,
184-
isFullScreen: isFullScreenAtom,
185-
zoomFactorAtom,
186-
controlShiftDelayAtom,
187-
updaterStatusAtom,
188-
prefersReducedMotionAtom,
189-
documentHasFocus: documentHasFocusAtom,
190-
modalOpen,
191-
allConnStatus: allConnStatusAtom,
192-
flashErrors: flashErrorsAtom,
193-
notifications: notificationsAtom,
194-
notificationPopoverMode: notificationPopoverModeAtom,
195-
reinitVersion,
196-
waveAIRateLimitInfoAtom: rateLimitInfoAtom,
197-
} as GlobalAtomsType;
19859
}
19960

20061
function initGlobalWaveEventSubs(initOpts: WaveInitOpts) {
@@ -935,6 +796,8 @@ function recordTEvent(event: string, props?: TEventProps) {
935796
RpcApi.RecordTEventCommand(TabRpcClient, { event, props }, { noresponse: true });
936797
}
937798

799+
export { ConnStatusMapAtom, getAtoms, initGlobalAtoms, orefAtomCache, TabIndicatorMap, blockComponentModelMap } from "./global-atoms";
800+
938801
export {
939802
atoms,
940803
clearAllTabIndicators,

frontend/app/store/modalmodel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import * as jotai from "jotai";
5-
import { globalStore } from "./global";
5+
import { globalStore } from "./jotaiStore";
66

77
class ModalsModel {
88
modalsAtom: jotai.PrimitiveAtom<Array<{ displayName: string; props?: any }>>;

0 commit comments

Comments
 (0)