setSettingsOpen(true)} />
+
) : messages.length === 0 ? (
setModelPopoverOpen(false)}
onConfigChange={() => void refreshLlm()}
- onOpenFullSettings={() => setSettingsOpen(true)}
+ onOpenFullSettings={openProviderSettings}
/>
) : null}
+ {/* Settings surfaces together, above the separator. Both rows are labelled with the
+ title of the dialog they open, so neither can drift from it — and unlike the AI
+ panel's own entry points, this one is reachable in Media and Rec too, which is
+ the whole reason the dialog's open state was lifted out of LeftPanel (#420). */}
+
{/* Only the PERMANENT half of the veto is applied here. A Store/Flathub/Snap/Nix
copy never offers the check at all; the transient half — not during a take —
diff --git a/src/contexts/ProviderSettingsContext.tsx b/src/contexts/ProviderSettingsContext.tsx
new file mode 100644
index 000000000..db4d74d30
--- /dev/null
+++ b/src/contexts/ProviderSettingsContext.tsx
@@ -0,0 +1,41 @@
+import { createContext, type ReactNode, useCallback, useContext, useMemo, useState } from "react";
+
+// Open/close state for the AI provider settings dialog, lifted out of the chat panel.
+//
+// It used to be a `useState` inside `LeftPanel`'s `ChatStripPanel`, which mounts only in Edit
+// mode with the chat panel expanded. Nothing outside that component could open the dialog, so
+// the app menu had no way to offer it (issue #420). The state lives here for the same reason
+// `ShortcutsContext` owns `isConfigOpen`: the surfaces that open a dialog and the place it is
+// mounted are on different branches of the tree.
+//
+// Deliberately just the three members. Everything the dialog itself needs — the provider
+// snapshot, the credentials, the save — it reads over the native bridge on open.
+interface ProviderSettingsContextValue {
+ isProviderSettingsOpen: boolean;
+ openProviderSettings: () => void;
+ closeProviderSettings: () => void;
+}
+
+const ProviderSettingsContext = createContext(null);
+
+export function useProviderSettings(): ProviderSettingsContextValue {
+ const ctx = useContext(ProviderSettingsContext);
+ if (!ctx) throw new Error("useProviderSettings must be used within ");
+ return ctx;
+}
+
+export function ProviderSettingsProvider({ children }: { children: ReactNode }) {
+ const [isProviderSettingsOpen, setIsProviderSettingsOpen] = useState(false);
+
+ const openProviderSettings = useCallback(() => setIsProviderSettingsOpen(true), []);
+ const closeProviderSettings = useCallback(() => setIsProviderSettingsOpen(false), []);
+
+ const value = useMemo(
+ () => ({ isProviderSettingsOpen, openProviderSettings, closeProviderSettings }),
+ [isProviderSettingsOpen, openProviderSettings, closeProviderSettings],
+ );
+
+ return (
+ {children}
+ );
+}
diff --git a/technical-documentation/architecture/llm-providers.md b/technical-documentation/architecture/llm-providers.md
index f01f02b37..d727ac320 100644
--- a/technical-documentation/architecture/llm-providers.md
+++ b/technical-documentation/architecture/llm-providers.md
@@ -10,7 +10,14 @@ The provider layer defines model metadata, protects credentials, discovers model
| [`electron/ai-edition/deep-agent/chat-model.ts`](../../electron/ai-edition/deep-agent/chat-model.ts) | `createOpenScreenChatModel` — the single transport. Picks a `@langchain/*` chat model class per provider. |
| [`electron/ai-edition/deep-agent/chat-model.ts`](../../electron/ai-edition/deep-agent/chat-model.ts) | Per-provider reasoning-effort capability and its LangChain wire options. |
| [`electron/native-bridge/services/aiEditionService.ts`](../../electron/native-bridge/services/aiEditionService.ts) | IPC surface: connect / disconnect, snapshot, `llmListProviderModels`. |
-| [`src/components/ai-edition/ProviderSettings.tsx`](../../src/components/ai-edition/ProviderSettings.tsx) | Renders cards and forms directly from `PROVIDER_DEFINITIONS`. |
+| [`src/components/ai-edition/ProviderSettings.tsx`](../../src/components/ai-edition/ProviderSettings.tsx) | Renders cards and forms directly from `PROVIDER_DEFINITIONS`. `ProviderSettingsDialog`, in the same file, is its one mount — `App.tsx`, beside `ShortcutsConfigDialog`. |
+| [`src/contexts/ProviderSettingsContext.tsx`](../../src/contexts/ProviderSettingsContext.tsx) | Open state for that dialog, so every entry point opens the same one. |
+
+> **Two doors, one dialog.** The settings dialog is opened from the AI panel — its gear, the
+> welcome card's CTA, the model pill with nothing configured, and a send attempt with no
+> provider — and from the app menu under the wordmark. Its open state lives in a context rather
+> than in `LeftPanel` because that panel mounts only in Edit mode with the chat panel expanded,
+> which would have left the menu item dead in Media and Rec (issue #420).
> **There is one transport.** `llm-call.ts` (`streamLlm` / `callLlm`) and
> `codex-session.ts` were deleted in 1.8.0 along with the two account-backed
diff --git a/technical-documentation/testing/manual-e2e-checklist.md b/technical-documentation/testing/manual-e2e-checklist.md
index c7af9d121..c34916ffc 100644
--- a/technical-documentation/testing/manual-e2e-checklist.md
+++ b/technical-documentation/testing/manual-e2e-checklist.md
@@ -328,12 +328,15 @@ The agent may only call the fixed tool set in [ai-agent.md](../architecture/ai-a
- [ ] On macOS, open the application menu and confirm About OpenScreen is followed by Check for Updates.
- [ ] On Windows and Linux, right-click the tray icon and confirm it lists Check for Updates and About OpenScreen. Outside the editor the tray is the only surface reachable by default there: the HUD is frameless and the editor and notes windows auto-hide their menu bar, so the Help menu appears only while Alt is held over one of those two windows.
- [ ] On Windows and Linux, open the editor, hold Alt, and confirm the Help menu lists Check for Updates and About OpenScreen.
-- [ ] In the editor, click the OpenScreen wordmark in the top bar and confirm it opens a menu listing Keyboard Shortcuts, Check for Updates and About OpenScreen. This is the discoverable path on Windows and Linux, where the two above are not.
+- [ ] In the editor, click the OpenScreen wordmark in the top bar and confirm it opens a menu listing Keyboard Shortcuts, AI settings, Check for Updates and About OpenScreen. This is the discoverable path on Windows and Linux, where the two above are not.
- [ ] Confirm the About row in that menu shows the running version, and that it matches what the About box then reports.
- [ ] Open the wordmark menu and pick Keyboard Shortcuts; confirm it opens the same dialog the top bar's gear does, and that only one dialog appears.
+- [ ] Open the wordmark menu and pick AI settings; confirm it opens the same provider dialog the AI panel's gear does, and that only one dialog appears.
+- [ ] Repeat that in Media mode, in Rec mode, and in Edit mode with the chat panel collapsed — the three states in which the dialog had no owner before, and the reason the row must not be Edit-only.
+- [ ] Connect or disconnect a provider from the menu's dialog while the chat panel is open behind it, close the dialog, and confirm the composer and the model pill follow without reopening the panel.
- [ ] Open the wordmark menu, then press Escape, click elsewhere in the top bar, and click the wordmark again — confirm each closes it and that the window does not start dragging instead of registering the click.
- [ ] With the wordmark menu open, walk it with the Down and Up arrows and confirm focus wraps at both ends.
-- [ ] Switch the app language and confirm the wordmark menu's three labels follow, matching the wording the macOS app menu and the tray use.
+- [ ] Switch the app language and confirm the wordmark menu's four labels follow — the first two matching the dialogs they open, the last two the wording the macOS app menu and the tray use.
- [ ] Open About and confirm it names the running version, the Electron/Chromium/Node versions, and the install channel.
- [ ] Confirm the About box opens in front of the HUD rather than behind it.
- [ ] On Windows and Linux, press Copy in the About box and confirm the clipboard holds that same block.