From d7d920979516a700ae0ef9df6b79e739fadeb634 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:40:01 -0700 Subject: [PATCH] fix(desktop): derive the loud scope note from the shared settings-scope store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite of the original component-local `editingNonDefault` computation onto the existing scope architecture (owner's ask: "use the same existing architecture" as the Capabilities/toolset scope handling). - store/settings-scope.ts gains `$settingsScopeEditsNonDefault`, a computed over `$settingsScopeProfile` × `$profiles`, so "the settings pages are editing a non-default profile" is a store fact any surface can subscribe to, not a per-component recomputation. - profile-scope.tsx reads `$settingsScopeProfile` for the selected chip (instead of re-deriving `override ?? active`) and the new selector for the loud/quiet note. User-visible outcome is unchanged: accented note for any non-default target, override or not; quiet note for an explicit override onto the default; nothing when following the active default. - Review: an unloaded roster (no `is_default` entry yet) used to suppress the note exactly at the landing moment where a bot may already be the active profile. The selector now assumes the root profile's canonical key as the default, so an unknown default fails loud, not quiet. The four-cell truth table is documented at the JSX conditional. - settings-scope.test.ts pins the selector across active-profile, override and unloaded-roster inputs; the component tests from the original PR are kept as-is and still pass. --- .../src/app/settings/profile-scope.tsx | 27 ++++++------ apps/desktop/src/store/settings-scope.test.ts | 42 +++++++++++++++++-- apps/desktop/src/store/settings-scope.ts | 16 ++++++- 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src/app/settings/profile-scope.tsx b/apps/desktop/src/app/settings/profile-scope.tsx index 451d3943eb..4e5c8667b1 100644 --- a/apps/desktop/src/app/settings/profile-scope.tsx +++ b/apps/desktop/src/app/settings/profile-scope.tsx @@ -3,8 +3,13 @@ import { useEffect } from 'react' import { useI18n } from '@/i18n' import { cn } from '@/lib/utils' -import { $activeGatewayProfile, $profiles, normalizeProfileKey, refreshProfiles } from '@/store/profile' -import { $settingsScopeOverride, setSettingsScope } from '@/store/settings-scope' +import { $profiles, normalizeProfileKey, refreshProfiles } from '@/store/profile' +import { + $settingsScopeEditsNonDefault, + $settingsScopeOverride, + $settingsScopeProfile, + setSettingsScope +} from '@/store/settings-scope' // The same chip affordance the Gateway page uses for its per-profile // connection overrides (gateway-settings ScopeChip). That one stays local to @@ -37,7 +42,8 @@ export function SettingsProfileScope({ className }: { className?: string }) { const { t } = useI18n() const scope = t.settings.profileScope const override = useStore($settingsScopeOverride) - const active = useStore($activeGatewayProfile) + const selected = useStore($settingsScopeProfile) + const editingNonDefault = useStore($settingsScopeEditsNonDefault) const profiles = useStore($profiles) // Refresh lazily so a profile created elsewhere shows up; the cached list @@ -50,10 +56,6 @@ export function SettingsProfileScope({ className }: { className?: string }) { return null } - const selected = normalizeProfileKey(override ?? active) - const defaultProfile = profiles.find(profile => profile.is_default) - const editingNonDefault = defaultProfile ? selected !== normalizeProfileKey(defaultProfile.name) : false - return (
({
@@ -16,14 +18,20 @@ vi.mock('@/hermes', () => ({
vi.mock('@/lib/query-client', () => ({ invalidateProfileScopedQueries: vi.fn() }))
vi.mock('@/store/starmap', () => ({ resetStarmapGraph: vi.fn() }))
-const { $activeGatewayProfile } = await import('./profile')
+const { $activeGatewayProfile, $profiles } = await import('./profile')
-const { $settingsRequestProfile, $settingsScopeOverride, $settingsScopeProfile, setSettingsScope } =
- await import('./settings-scope')
+const {
+ $settingsRequestProfile,
+ $settingsScopeEditsNonDefault,
+ $settingsScopeOverride,
+ $settingsScopeProfile,
+ setSettingsScope
+} = await import('./settings-scope')
beforeEach(() => {
$activeGatewayProfile.set('default')
$settingsScopeOverride.set(null)
+ $profiles.set([])
})
describe('settings scope store', () => {
@@ -72,6 +80,34 @@ describe('settings scope store', () => {
expect($settingsRequestProfile.get()).toBeUndefined()
})
+ it('flags a non-default edit target whether it comes from the active profile or an override', () => {
+ const roster = [
+ { is_default: true, name: 'default' },
+ { is_default: false, name: 'scout' }
+ ] as unknown as ProfileInfo[]
+ $profiles.set(roster)
+
+ // Following the active DEFAULT profile → editing the default.
+ expect($settingsScopeEditsNonDefault.get()).toBe(false)
+
+ // A Bot Mode chat made the bot the active profile; no override is set,
+ // yet the settings pages now edit profiles/scout/config.yaml.
+ $activeGatewayProfile.set('scout')
+ expect($settingsScopeEditsNonDefault.get()).toBe(true)
+
+ // Explicit override back onto the default → editing the default again.
+ setSettingsScope('default')
+ expect($settingsScopeEditsNonDefault.get()).toBe(false)
+ })
+
+ it('treats an unloaded roster as "default = the root profile", so an unknown default fails loud', () => {
+ $activeGatewayProfile.set('scout')
+ expect($settingsScopeEditsNonDefault.get()).toBe(true)
+
+ $activeGatewayProfile.set('default')
+ expect($settingsScopeEditsNonDefault.get()).toBe(false)
+ })
+
it('drops the override on an app-wide profile switch', () => {
setSettingsScope('research')
expect($settingsScopeOverride.get()).toBe('research')
diff --git a/apps/desktop/src/store/settings-scope.ts b/apps/desktop/src/store/settings-scope.ts
index 115707b8d5..fe9f8f99a7 100644
--- a/apps/desktop/src/store/settings-scope.ts
+++ b/apps/desktop/src/store/settings-scope.ts
@@ -1,6 +1,6 @@
import { atom, computed } from 'nanostores'
-import { $activeGatewayProfile, normalizeProfileKey } from '@/store/profile'
+import { $activeGatewayProfile, $profiles, normalizeProfileKey } from '@/store/profile'
// ── Shared settings "Applies to" scope ──────────────────────────────────────
// One selection shared by every config-backed settings page (Model, Workspace,
@@ -16,6 +16,20 @@ export const $settingsScopeProfile = computed([$settingsScopeOverride, $activeGa
normalizeProfileKey(override ?? active)
)
+// Whether the settings pages are editing a profile OTHER than the default
+// one. The scope follows the app's active profile when no override is set —
+// which, after opening any Bot Mode chat, is the BOT's profile — so an edit
+// can land in profiles/