diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json index b91ba2cdec..bb6369a8dc 100644 --- a/apps/desktop/renderer-architecture.json +++ b/apps/desktop/renderer-architecture.json @@ -7,7 +7,6 @@ "src/renderer/app-shell-chrome-actions.tsx", "src/renderer/app-shell-command-actions.ts", "src/renderer/app-shell-copy.ts", - "src/renderer/app-shell-detail-panel.tsx", "src/renderer/app-shell-e2e-fixture.ts", "src/renderer/app-shell-effects.ts", "src/renderer/app-shell-overlays.tsx", @@ -571,18 +570,6 @@ "importSpecifiers": 1, "nonTriviaTokens": 249 }, - "src/renderer/app-shell-detail-panel.tsx": { - "importDeclarations": 0, - "bridgePaths": {}, - "environmentCapabilities": {}, - "hookCalls": {}, - "lifecycleMethods": {}, - "unresolvedDependencies": 0, - "actionFactories": [], - "dependencyPaths": {}, - "importSpecifiers": 0, - "nonTriviaTokens": 87 - }, "src/renderer/app-shell-e2e-fixture.ts": { "importDeclarations": 1, "bridgePaths": { @@ -660,7 +647,7 @@ "nonTriviaTokens": 858 }, "src/renderer/app-shell.tsx": { - "importDeclarations": 42, + "importDeclarations": 40, "bridgePaths": {}, "environmentCapabilities": { "window.requestAnimationFrame": 3 @@ -675,7 +662,6 @@ "useAppShellSessionUiReads": 1, "useAppShellSessionWorkspace": 1, "useAppShellTurnPresentation": 1, - "useEffect": 1, "useLayoutEffect": 1, "useRef": 4, "useSessionNavigationReads": 1, @@ -698,7 +684,6 @@ "dependencyPaths": { "./agent-graph-panel": 1, "./app-shell-chrome-actions": 1, - "./app-shell-detail-panel": 1, "./app-shell-e2e-fixture": 1, "./app-shell-effects": 1, "./app-shell-overlays": 1, @@ -738,8 +723,8 @@ "./platform/desktop/default-runtime-host-operation.js": 1, "./scroll-motion-policy": 1, "./settings/provider-brand-marks": 1, - "./settings/provider-display": 1, "./settings/runtime-host-ssh-terminal-dialog.js": 1, + "./shell/detail-panel": 1, "./shell/frame-style": 1, "./use-app-shell-session-ui-reads": 1, "./use-app-shell-session-workspace": 1, @@ -761,8 +746,8 @@ "@maka/ui": 1, "react": 1 }, - "importSpecifiers": 60, - "nonTriviaTokens": 8009 + "importSpecifiers": 57, + "nonTriviaTokens": 7852 }, "src/renderer/use-app-shell-session-list.ts": { "importDeclarations": 4, @@ -3336,16 +3321,14 @@ "bridgePaths": { "window.maka.app.sessionProjectInfo": 1, "window.maka.projects.getDefaultContext": 1, - "window.maka.projects.getLocalSnapshot": 1, "window.maka.projects.getSnapshot": 1, - "window.maka.projects.subscribeChanges": 2, - "window.maka.projects.subscribeLocalChanges": 1 + "window.maka.projects.subscribeChanges": 2 }, "environmentCapabilities": {}, "hookCalls": { - "useEffect": 4, + "useEffect": 3, "useRef": 1, - "useState": 7 + "useState": 6 }, "lifecycleMethods": {}, "unresolvedDependencies": 0, @@ -3725,8 +3708,7 @@ "capability": "shell-root-and-frame", "targetZone": "shell", "legacyPaths": [ - "src/renderer/app-shell-chrome-actions.tsx", - "src/renderer/app-shell-detail-panel.tsx" + "src/renderer/app-shell-chrome-actions.tsx" ] }, { @@ -3756,14 +3738,14 @@ "targetZone": "features/overlays", "legacyPaths": [ "src/renderer/app-shell-command-actions.ts", + "src/renderer/app-shell-copy.ts", "src/renderer/app-shell-overlays.tsx" ] }, { - "capability": "mixed-bootstrap-and-conversation-effects", + "capability": "root-lifecycle-effects", "targetZone": "split-by-capability", "legacyPaths": [ - "src/renderer/app-shell-copy.ts", "src/renderer/app-shell-effects.ts" ] }, diff --git a/apps/desktop/src/main/__tests__/app-shell-retired-props.test.ts b/apps/desktop/src/main/__tests__/app-shell-retired-props.test.ts new file mode 100644 index 0000000000..bfb3d414d9 --- /dev/null +++ b/apps/desktop/src/main/__tests__/app-shell-retired-props.test.ts @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import type { ComponentProps } from 'react'; +import type { ChatView } from '@maka/ui'; +import type { + SessionNavigationPorts, + SessionNavigationProvider, +} from '../../renderer/features/session-navigation/index.js'; + +// The root stopped threading these: ChatView never read them, the rail derives +// its WorkHub entry from `workHubActive`, and no rail action activates a Session. +type Declares = Extract extends never ? false : true; +const retired = { + chatViewModelProps: false as Declares, + 'activeConnectionLabel' | 'activeModel' | 'activeModelLabel' | 'activeProviderType' + | 'renderProviderMark' | 'modelChoices' | 'onModelChange'>, + railWorkHubEntry: false as Declares, 'workHubEntry'>, + portActivateSession: false as Declares, +} satisfies Record; + +test('the root no longer threads props their readers never use', () => { + assert.deepEqual(Object.values(retired), [false, false, false]); +}); diff --git a/apps/desktop/src/main/__tests__/onboarding-authority.test.ts b/apps/desktop/src/main/__tests__/onboarding-authority.test.ts index d20a634dfa..b248f9de2f 100644 --- a/apps/desktop/src/main/__tests__/onboarding-authority.test.ts +++ b/apps/desktop/src/main/__tests__/onboarding-authority.test.ts @@ -29,6 +29,9 @@ import { createOnboardingSnapshotPoller, getOnboardingActivationCandidate, OnboardingAuthorityProvider, + OnboardingConnectionSeed, + type OnboardingAuthority, + type OnboardingProjection, OnboardingProjectionRoot, onboardingSnapshotProjectionEqual, type OnboardingShellProjection, @@ -126,6 +129,33 @@ describe('createOnboardingAuthority', () => { assert.equal(fake.calls.at(-1), 'unsubscribe', 'the root is the subscriber that keeps the reads alive'); }); + it('seeds the default Host connections from each new snapshot, and refreshes them when reads fail', async () => { + let projection: OnboardingProjection = { snapshot: null, failed: false }; + const listeners = new Set<() => void>(); + const authority: OnboardingAuthority = { + getProjection: () => projection, + subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); }, + refresh: () => {}, + skipInitialOnboarding: async () => {}, + }; + const publish = (next: OnboardingProjection) => act(() => { projection = next; listeners.forEach((listener) => listener()); }); + const calls: unknown[] = []; + const { root } = installReactRenderer(); + await act(async () => root.render(createElement(OnboardingAuthorityProvider, { value: authority, + children: createElement(OnboardingConnectionSeed, { + seed: (snapshot) => calls.push(snapshot), + refresh: () => calls.push('refresh'), + }) }))); + assert.deepEqual(calls, [], 'nothing to seed before the first read'); + const withConnections = { ...READY_SNAPSHOT, defaultSlug: 'openai' }; + publish({ snapshot: withConnections, failed: false }); + publish({ snapshot: withConnections, failed: false }); + assert.deepEqual(calls.splice(0), [{ connections: [], defaultConnection: 'openai', chatModelChoices: [] }]); + publish({ snapshot: null, failed: true }); + assert.deepEqual(calls.splice(0), ['refresh']); + await act(async () => root.unmount()); + }); + it('fails without its provider instead of holding the first-run gate closed', () => { const { root } = installReactRenderer(); assert.throws(() => act(() => root.render(createElement(OnboardingProjectionRoot, { children: () => null }))), diff --git a/apps/desktop/src/main/__tests__/session-navigation-controller.test.ts b/apps/desktop/src/main/__tests__/session-navigation-controller.test.ts index 5ed13e42f5..77a2ecb2c4 100644 --- a/apps/desktop/src/main/__tests__/session-navigation-controller.test.ts +++ b/apps/desktop/src/main/__tests__/session-navigation-controller.test.ts @@ -156,7 +156,6 @@ function ports( return { sessionsRef: { current: sessions }, acquireAutomaticQueryBlock: () => ({ release: () => undefined }), - activateSession: (sessionId) => calls.push(`activate:${sessionId ?? 'none'}`), clearSessionRendererState: (sessionId) => calls.push(`clear:${sessionId}`), refreshSessions: async () => sessions, toastApi: { @@ -205,7 +204,7 @@ function navigationTree( shell: { activeSessionId: string; workHubActive: boolean; - workHubEntry?: { active: boolean; label: string; onSelect(): void }; + onOpenWorkHub?(): void; }, sibling: ReactNode, child: ReactNode, @@ -674,7 +673,7 @@ describe('SessionNavigationProvider selection', () => { await act(async () => root.render(navigationTree(catalog, { activeSessionId: 'root', workHubActive: false, - workHubEntry: { active: false, label: 'WorkHub', onSelect: () => opened.push('workhub') }, + onOpenWorkHub: () => opened.push('workhub'), }, null, createElement(ChromeProbe), undefined, { workHub: enablement }))); const workHubEntry = () => chrome?.workHubEntry; assert.equal(workHubEntry(), undefined); diff --git a/apps/desktop/src/main/__tests__/use-project-context.test.ts b/apps/desktop/src/main/__tests__/use-project-context.test.ts index 8f7273051d..f667b6985e 100644 --- a/apps/desktop/src/main/__tests__/use-project-context.test.ts +++ b/apps/desktop/src/main/__tests__/use-project-context.test.ts @@ -70,22 +70,17 @@ test('discards a pending Project projection after the default Host changes', asy return pendingContext.promise; }, subscribeChanges: () => () => {}, - getLocalSnapshot: async () => ({ - projects: [], - capabilities: NO_PROJECT_CAPABILITIES, - }), - subscribeLocalChanges: () => () => {}, }, }; - let projects: ProjectRecord[] = []; - let selectedProjectId: string | null | undefined; + let projectInfo: unknown; + let currentProjectId: string | null | undefined; function Probe() { const context = projectContext.useAppShellProjectContext({ rendererMountedRef: { current: true }, }); - projects = context.projects; - selectedProjectId = context.selectedProjectId; + projectInfo = context.projectInfo; + currentProjectId = context.currentProjectId; return null; } @@ -110,8 +105,8 @@ test('discards a pending Project projection after the default Host changes', asy await commitGuardChecked.promise; }); - assert.deepEqual(projects, []); - assert.equal(selectedProjectId, undefined); + assert.equal(projectInfo, null); + assert.equal(currentProjectId, undefined); }); afterEach(() => { diff --git a/apps/desktop/src/renderer/README.md b/apps/desktop/src/renderer/README.md index 075ee1c1c2..3c260f4c85 100644 --- a/apps/desktop/src/renderer/README.md +++ b/apps/desktop/src/renderer/README.md @@ -277,33 +277,32 @@ or reason, which stays with review. | --- | --- | --- | --- | --- | --- | --- | --- | | `AppShell` | `useState` | `uiLocalePreference` | `LocaleProvider`; appearance settings | AppShell | the persisted locale preference and its setter | locale | — | | `AppShell` | `useState` | `uiLocaleOverride` | `LocaleProvider`; E2E locale override | AppShell | a runtime locale override above every region | locale | — | -| `AppShell` | `useSystemUiLocale` | `systemUiLocale` | `resolveUiLocale` for `LocaleProvider` | AppShell | read the OS locale and its changes | locale | — | +| `AppShell` | `useSystemUiLocale` | `systemUiLocale` | `resolveUiLocale`, whose result reaches `LocaleProvider`, `ErrorBoundary` and `AppShellContent` | legacy `use-system-ui-locale.ts` | read the OS locale and its changes | locale | — | | `AppShellContent` | `useActiveExecutionBoundary` | `activeExecutionBoundary` | Composer permission control; the Composer submission owner reloads it after a boundary answer | Conversation | read and reload the owner Session's execution boundary | — | M3 | -| `AppShellContent` | `useAppShellBootstrapSubscriptions` | Main change subscriptions | Session, connection, Host-profile and settings refreshers; app-window commands | legacy `app-shell-effects.ts` | startup refreshes, the global shortcuts and the handlers `ShellLifecycleSubscriptions` subscribes with the injected `ShellLifecycleSources`; no bridge access | application lifecycle | — | -| `AppShellContent` | `useAppShellHostEffects` | titlebar modal sync | titlebar | legacy `app-shell-effects.ts` | observe top-layer modals; the `data-os` platform tag is applied by `ShellLifecycleSources` | layout | — | -| `AppShellContent` | `useAppShellNavRefSync` | `navSelectionRef` | ownership checks of async results | AppShell | mirror the navigation selection into a ref | navigation | — | +| `AppShellContent` | `useAppShellBootstrapSubscriptions` | lifecycle handlers and startup | `ShellLifecycleSubscriptions` (Session, connection, Host-profile and settings refreshes, window commands); the startup Session and E2E-fixture refresh; ⌘, and ⌘N | legacy `app-shell-effects.ts` | startup refreshes, the global shortcuts, the mounted flag and pending-ref cleanup; returns the handlers `ShellLifecycleSubscriptions` subscribes with the injected `ShellLifecycleSources`; no bridge access | application lifecycle | — | +| `AppShellContent` | `useAppShellHostEffects` | titlebar modal sync | the native window controls, dimmed through `titlebar-modal-sync.ts` | legacy `app-shell-effects.ts` | observe top-layer modals; the `data-os` platform tag is applied by `ShellLifecycleSources` | layout | — | +| `AppShellContent` | `useAppShellNavRefSync` | `navSelectionRef` | ownership checks of async results (Composer claim, surface-owner checks) | legacy `app-shell-effects.ts` | mirror the navigation selection into a ref | navigation | — | | `AppShellContent` | `useAppShellPersistenceEffects` | theme and navigation persistence | `` theme class and palette; stored navigation | legacy `app-shell-effects.ts` | apply the theme preference and palette; persist the navigation state | layout | — | -| `AppShellContent` | `useAppShellProjectContext` | project context | titlebar project name and path; Workbar, Module Hub and palette project inputs; the default-Host project refresh | legacy `use-project-context.ts` | read the owner Session's and the default Host's project projection; project mutations and the open-folder commands belong to Task Entry | — | M5 | +| `AppShellContent` | `useAppShellProjectContext` | project context | titlebar project name, path and open-folder gate; Workbar project id and aliases; Module Hub and command-palette client-path access; composer mentions project path; the default-Host refresh in the lifecycle handlers | legacy `use-project-context.ts` | read the owner Session's project (path, Git state, current project, capabilities) and, with no Session, the default Host's, and re-read it on change; no writes: project mutations and the open-folder commands belong to Task Entry | application lifecycle | — | | `AppShellContent` | `useAppShellSessionUiReads` | displayed Session chrome | interaction, queue, live-turn and execution chrome; Composer props | Conversation (transitional reader) | fixed-purpose reads of the displayed and owner Session | — | M3 | -| `AppShellContent` | `useAppShellSessionWorkspace` | Session workspace | every region's requested, published and owner Session | legacy `use-app-shell-session-workspace.ts` over the Session catalog and Conversation | Session selection and the catalog controller | navigation | — | +| `AppShellContent` | `useAppShellSessionWorkspace` | Session workspace | every region's requested, published and owner Session; transcript flags; the queue surface and Composer ref; the Session UI reads store | legacy `use-app-shell-session-workspace.ts` over the Session catalog and Conversation | Session selection, the catalog controller, list refresh and row patches, message reads and refresh | navigation | — | | `AppShellContent` | `useAppShellTurnPresentation` | `deriveTurnPresentation` | `ChatView` turn footer | application contract `turn-presentation` | derive turn presentation from the transcript projection and pending turn actions | — | M3 | -| `AppShellContent` | `useEffect` | `defaultHostConnections`: onboarding connection seed | default-Host connection projection | AppShell | seed default-Host connections from the onboarding authority's read-only projection | — | M5 | -| `AppShellContent` | `useLayoutEffect` | `openSessionInChatRef` publication | turn footer, Module Hub, titlebar parent link | AppShell | publish the current open-Session command into a ref | cross-region command | — | -| `AppShellContent` | `useSessionNavigationReads` | rail reads | command palette sessions, titlebar parent, `--maka-sidenav-width` | Session Navigation | revision navigation, the active parent Session and the rail layout | navigation | — | +| `AppShellContent` | `useLayoutEffect` | `openSessionInChatRef` publication | every caller of `openSessionInChat`: turn footer, transcript lineage, linked-Session and revision links, Composer submission, Workbar, Module Hub, command palette, Settings overlays, turn-request approval, agent graph, titlebar parent link | AppShell | publish the current open-Session command into a ref | cross-region command | — | +| `AppShellContent` | `useSessionNavigationReads` | rail reads | transcript revision navigation, titlebar parent and `sidebarCollapsed`, frame `data-sidebar-state` and `--maka-sidenav-width` | Session Navigation | revision navigation, the active parent Session and the rail layout; writes the compact flag into `sessionRailLayoutStore` | navigation | — | | `AppShellContent` | `useSessionSettingIntent` | selected-Session setting overlay | Composer model and mode controls; new-task settings for creation | Session Settings | an equality-selected overlay read, the new-task settings and setting commands | — | M3 | -| `AppShellContent` | `useShellAppearance` | appearance settings | theme, palette, user label, Workbar toggle position, locale update gate | legacy `use-shell-appearance.ts` | read and write client appearance settings | layout | — | +| `AppShellContent` | `useShellAppearance` | appearance settings | theme, palette, user label, Workbar toggle position, locale update gate; `appearanceHydrated` for the previous-shutdown notice | legacy `use-shell-appearance.ts` | read client and Host appearance settings and E2E state, hydrate the locale, apply theme, palette and font sizes, and expose local setters | layout | — | | `AppShellContent` | `useShellChatModel` | Composer model selection | model picker, health notice, new-chat model | Conversation (transitional) | derive model, thinking and executor selection | — | M3 | -| `AppShellContent` | `useShellConnections` | `newTaskConnections` | new-task model choices | legacy `use-shell-connections.ts` | the new-task target's connection snapshot and refresh | application lifecycle | — | -| `AppShellContent` | `useShellConnections` | `defaultHostConnections` | Settings, global commands, model setup | legacy `use-shell-connections.ts` | the default Host's connection snapshot and refresh | application lifecycle | — | -| `AppShellContent` | `useShellConnections` | `sessionHostConnections` | owner Session model choices | legacy `use-shell-connections.ts` | the owner Session Host's connection snapshot and refresh | application lifecycle | — | +| `AppShellContent` | `useShellConnections` | `newTaskConnections` | new-task model choices; transcript and onboarding connections and refresh; executor connection count | legacy `use-shell-connections.ts` | the new-task target's connection snapshot and refresh, falling back to the default Host or onboarding snapshot | application lifecycle | — | +| `AppShellContent` | `useShellConnections` | `defaultHostConnections` | `OnboardingConnectionSeed`; the new-task fallback; the refresh when Settings closes; the connection-event fan-out | legacy `use-shell-connections.ts` | the default Host's connection snapshot and refresh | application lifecycle | — | +| `AppShellContent` | `useShellConnections` | `sessionHostConnections` | owner Session model choices and readiness; transcript connections and refresh; the WorkHub snapshot | legacy `use-shell-connections.ts` | the owner Session Host's connection snapshot and refresh | application lifecycle | — | | `AppShellContent` | `useShellLiveTurn` | live-turn flags | mode-change gating, model switch, pet activity | Conversation reads | derive streaming and settled flags from the owner Session snapshot | — | M3 | -| `AppShellContent` | `useShellMemoryPill` | memory pill | titlebar memory pill | legacy `use-shell-memory-pill.ts` | read and refresh the owner Session's memory state | layout | — | +| `AppShellContent` | `useShellMemoryPill` | memory indicator | the transcript's session-context memory indicator; refreshed by the lifecycle handlers and when Settings closes | legacy `use-shell-memory-pill.ts` | read and refresh the owner Session's memory state, or the default Host's without a Session | layout | — | | `AppShellContent` | `useShellResume` | resume offer | Composer send slot | Conversation | per-Session resume availability | — | M3 | -| `AppShellContent` | `useStableActions` | `createAppShellE2eFixtureActions` | E2E fixture command | AppShell | apply test fixtures across navigation, rail, Workbar and appearance | cross-region command | — | -| `AppShellContent` | `useState` | `petCompletionNonce` | custom pet companion | AppShell | a counter the transcript bumps when the active Turn completes | cross-region command | — | +| `AppShellContent` | `useStableActions` | `createAppShellE2eFixtureActions` | the startup E2E-fixture refresh | legacy `app-shell-e2e-fixture.ts` over `application/contracts/use-stable-actions.ts` | apply test fixtures across Session selection and refresh, navigation, rail, Workbar, Settings, search and appearance | cross-region command | — | +| `AppShellContent` | `useState` | `petCompletionNonce` | custom pet companion | AppShell | a counter `ConversationLifecycle`'s `onTurnCompleted` bumps when the active Turn completes | cross-region command | — | | `AppShellContent` | `useState` | `navigationState` | navigation sections; stored navigation | AppShell | the selected section and each hub's module | navigation | — | -| `AppShellContent` | `useState` | `workHubActive` | WorkHub or Session surface | AppShell | whether the WorkHub surface is shown | navigation | — | -| `AppShellContent` | `useToast` | `toastApi` | toasts of every legacy action | Astryx toast provider | show toasts | cross-region command | — | +| `AppShellContent` | `useState` | `workHubActive` | WorkHub or Session surface; Workbar workspace; the rail entry; the Session-Host connection snapshot; the titlebar identity | AppShell | whether the WorkHub surface is shown; enablement is the WorkHub enablement authority's | navigation | — | +| `AppShellContent` | `useToast` | `toastApi` | toasts and confirm dialogs of every legacy action | `@maka/ui` `ToastProvider` (Astryx) | show toasts and confirmations | cross-region command | — | | `AppShellContent` | `useTurnActionRegistry` | pending turn actions | turn footer disabled mask; the Composer submission owner's Turn branch; bootstrap clears | legacy `use-turn-action-registry.ts` | pending action keys per Session | — | M3 | @@ -317,8 +316,8 @@ following. Exports only tests or Storybook read live in each feature's | Feature | Export | Root consumer | Kind | Stays because / Removal | | --- | --- | --- | --- | --- | -| overlays | `OverlaysConsumer` | `app-shell-overlays.tsx` (Settings modal, palette command list) | render-prop projection of overlay state | M5, with the legacy command actions | -| diagnostics | `ManualDiagnosticReportConsumer` | command palette options in `app-shell.tsx` | render-prop manual report command | M5, with the legacy command actions | +| overlays | `OverlaysConsumer` | `app-shell-overlays.tsx` (Settings modal, palette command list) | render-prop projection of overlay state | stays: root composition of the legacy Settings surface and the palette command list, which a feature cannot import; it moves when the Settings surface migrates, outside R2 | +| diagnostics | `ManualDiagnosticReportConsumer` | command palette options in `app-shell.tsx` | render-prop manual report command | stays: cross-region command (the diagnostics owner's manual report, handed to the shell-built palette options) | | task-entry | `TaskEntryWorkspacePickerConsumer` | Composer region in `app-shell.tsx` | render-prop workspace picker | M3 | | session-collaboration | `GuestTurnRequests` | Composer region in `app-shell.tsx` | render-prop guest composer projection over the Composer ref | M3 | | module-hub | `ModuleHubSkillCatalogRevisionBoundary` | Composer mentions provider | render-prop skill catalog revision | M3 | diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx index cbe2af5a32..c05455fb33 100644 --- a/apps/desktop/src/renderer/app-shell.tsx +++ b/apps/desktop/src/renderer/app-shell.tsx @@ -22,7 +22,6 @@ import { WorkHubEnablementWatch } from './application/contracts/workhub-workspac import { RuntimeHostHandoffOverlay } from './features/runtime-host-management/index.js'; import { useCallback, - useEffect, useLayoutEffect, useMemo, useRef, @@ -85,11 +84,11 @@ import { NEW_TASK_PENDING_KEY } from './pending-items'; import { desktopSlashCommandAvailability } from './application/contracts/desktop-slash-command.js'; import { getOnboardingActivationCandidate, + OnboardingConnectionSeed, OnboardingProjectionRoot, type OnboardingShellProjection, } from './application/contracts/onboarding/onboarding-authority.js'; import { ShellLifecycleSubscriptions } from './application/contracts/shell-lifecycle.js'; -import { ProviderLogo } from './settings/provider-display'; import { ProviderBrandMark } from './settings/provider-brand-marks'; import { RuntimeHostSshTerminalDialog } from './settings/runtime-host-ssh-terminal-dialog.js'; import { @@ -111,7 +110,7 @@ import { deriveDesktopExecutionBoundarySurface } from './desktop-execution-bound import { modelSetupToastCopy } from './model-connection-errors'; import type { AppShellCommandListOptions } from './app-shell-command-actions'; import { AppShellTitlebar } from './app-shell-chrome-actions'; -import { AppShellDetailPanel } from './app-shell-detail-panel'; +import { AppShellDetailPanel } from './shell/detail-panel'; import { appShellFrameStyle } from './shell/frame-style'; import { AppShellOverlays } from './app-shell-overlays'; import type { ArchivedTasksBridge } from './settings/tasks-settings-page'; @@ -185,7 +184,7 @@ export function AppShell() { {(copyManualDiagnosticReport) => ( )} @@ -227,7 +226,6 @@ function AppShellContent({ onboarding, copyManualDiagnosticReport, uiLocale, - uiLocaleOverride, setUiLocaleOverride, setUiLocalePreference, }: { @@ -238,7 +236,6 @@ function AppShellContent({ onboarding: OnboardingShellProjection; copyManualDiagnosticReport: Diagnostics.CopyManualDiagnosticReport; uiLocale: UiLocale; - uiLocaleOverride: UiLocale | null; setUiLocaleOverride: Dispatch>; setUiLocalePreference: Dispatch>; }) { @@ -516,10 +513,7 @@ function AppShellContent({ }); const { chatModelChoices, - activeConnection, - activeConnectionLabel, activeModel, - activeModelLabel, executor, composerModelProps, newChatModel, @@ -721,21 +715,8 @@ function AppShellContent({ // process (PR110a + PR110b contract) and reactively invalidates on // `sessions:changed` + `connections:event`. The hero renders only // when sessions.length === 0; any session (including archived / - // aborted) takes over with the existing chat surface. - useEffect(() => { - const snapshot = onboarding.snapshot; - if (snapshot) { - defaultHostConnections.seedSnapshot({ - connections: snapshot.connections, - defaultConnection: snapshot.defaultSlug, - chatModelChoices: snapshot.chatModelChoices, - }); - } else if (onboarding.failed) { - // Session bootstrap is independent above. If onboarding itself failed, - // retain the previous connection-specific recovery path as well. - void defaultHostConnections.refreshConnections(); - } - }, [onboarding.failed, onboarding.snapshot]); + // aborted) takes over with the existing chat surface. The default Host's + // connections are seeded from the same snapshot by OnboardingConnectionSeed. // Nothing settled to show while the first snapshot pull is in flight. The // flag keeps the composer hidden and — through `data-maka-content-ready` on // .appFrame — holds the launch overlay until a real frame exists: sessions, @@ -789,7 +770,6 @@ function AppShellContent({ const moduleHubCommands = useMemo(ModuleHub.createModuleHubCommandPort, []); const { projectInfo, - projects, projectCapabilities, activeProjectCapabilities, currentProjectId, @@ -919,7 +899,6 @@ function AppShellContent({ const sessionNavigationPorts: SessionNavigationPorts = { sessionsRef, acquireAutomaticQueryBlock: sessionCatalogController.acquireAutomaticQueryBlock, - activateSession: setActiveId, clearSessionRendererState, refreshSessions, toastApi, @@ -1336,6 +1315,7 @@ function AppShellContent({ > + void defaultHostConnections.refreshConnections()} /> { setWorkHubActive(true); setNavSelection({ section: 'sessions' }); }} onDisabled={exitWorkHub} /> } - modelChoices={chatModelChoices} - onModelChange={sharedSessionActive ? undefined : (input) => { - if (activeId) void setSessionModel(activeId, input); - }} userLabel={userLabel} memoryActive={memoryActive} onOpenMemorySettings={sharedSessionActive ? undefined : () => openSettingsSection('memory')} diff --git a/apps/desktop/src/renderer/application/contracts/onboarding/onboarding-authority.ts b/apps/desktop/src/renderer/application/contracts/onboarding/onboarding-authority.ts index 817e722876..5f3a0a19a4 100644 --- a/apps/desktop/src/renderer/application/contracts/onboarding/onboarding-authority.ts +++ b/apps/desktop/src/renderer/application/contracts/onboarding/onboarding-authority.ts @@ -26,7 +26,7 @@ * projections. A failed read is reported as a flag, never as error text. */ -import { createContext, useContext, useSyncExternalStore, type ReactNode } from 'react'; +import { createContext, useContext, useEffect, useEffectEvent, useSyncExternalStore, type ReactNode } from 'react'; import { hasSettledInitialOnboarding } from '@maka/core/onboarding-milestone'; import type { SessionSendProjection } from '@maka/core/session-send-projection'; import { valuesEqual } from '@maka/ui'; @@ -34,6 +34,7 @@ import type { DesktopOnboardingSessionUpdate, OnboardingSnapshot, } from '../../../../shared/onboarding-snapshot.js'; +import type { DesktopConnectionSnapshot } from '../../../../shared/desktop-connection-snapshot.js'; export type { OnboardingSnapshot }; @@ -353,6 +354,33 @@ export function OnboardingProjectionRoot(props: { }); } +/** + * The default Host's connections ride the onboarding snapshot, so each + * accepted snapshot seeds that projection; when onboarding cannot be read, + * the projection refreshes itself instead. + */ +export function OnboardingConnectionSeed(props: { + seed(snapshot: DesktopConnectionSnapshot): void; + refresh(): void; +}) { + const authority = useOnboardingAuthority(); + const { snapshot, failed } = useSyncExternalStore(authority.subscribe, authority.getProjection); + const seed = useEffectEvent(props.seed); + const refresh = useEffectEvent(props.refresh); + useEffect(() => { + if (snapshot) { + seed({ + connections: snapshot.connections, + defaultConnection: snapshot.defaultSlug, + chatModelChoices: snapshot.chatModelChoices, + }); + } else if (failed) { + refresh(); + } + }, [failed, snapshot]); + return null; +} + /** The current snapshot, for readers that check something again whenever onboarding changes. */ export function useCurrentOnboardingSnapshot(): OnboardingSnapshot | null { const authority = useOnboardingAuthority(); diff --git a/apps/desktop/src/renderer/features/session-navigation/ports.ts b/apps/desktop/src/renderer/features/session-navigation/ports.ts index faf9225b5e..37babc6a93 100644 --- a/apps/desktop/src/renderer/features/session-navigation/ports.ts +++ b/apps/desktop/src/renderer/features/session-navigation/ports.ts @@ -152,7 +152,6 @@ export interface SessionNavigationServices { export interface SessionNavigationPorts { sessionsRef: RefObject>; acquireAutomaticQueryBlock(sessionIds: readonly string[]): { release(): void }; - activateSession(sessionId: string | undefined): void; clearSessionRendererState(sessionId: string): void; refreshSessions(): Promise>; toastApi: SessionNavigationToastApi; diff --git a/apps/desktop/src/renderer/features/session-navigation/ui/session-navigation-provider.tsx b/apps/desktop/src/renderer/features/session-navigation/ui/session-navigation-provider.tsx index ab33c14504..4ef3002817 100644 --- a/apps/desktop/src/renderer/features/session-navigation/ui/session-navigation-provider.tsx +++ b/apps/desktop/src/renderer/features/session-navigation/ui/session-navigation-provider.tsx @@ -74,7 +74,7 @@ export interface SessionNavigationChromeInput { moduleMemory?: NavModuleMemory; workHubActive: boolean; /** Shown, and selectable, only while the client WorkHub switch is on. */ - workHubEntry?: { active: boolean; label: string; onSelect(): void }; + onOpenWorkHub?(): void; projectActions?: ProjectRowActions; onSelect(selection: NavSelection): void; onOpenSettings(): void; @@ -325,9 +325,10 @@ export function SessionNavigationProvider(props: SessionNavigationProviderProps) props.onNew(); }, onOpenSettings: props.onOpenSettings, - workHubEntry: workHubEnabled && props.workHubEntry ? { - ...props.workHubEntry, - onSelect: () => { if (workHubEnablement.isEnabled()) props.workHubEntry?.onSelect(); }, + workHubEntry: workHubEnabled && props.onOpenWorkHub ? { + active: props.workHubActive, + label: 'WorkHub', + onSelect: () => { if (workHubEnablement.isEnabled()) props.onOpenWorkHub?.(); }, } : undefined, }; diff --git a/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx b/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx index 07f629d649..3852f43956 100644 --- a/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx +++ b/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx @@ -410,7 +410,6 @@ export function WorkHubRoot() { activeTurn={controller.activeTurn} messageLoading={!transcript.ready} activeSession={session} - activeModel={session?.model} emptyOverride={
diff --git a/apps/desktop/src/renderer/app-shell-detail-panel.tsx b/apps/desktop/src/renderer/shell/detail-panel.tsx similarity index 100% rename from apps/desktop/src/renderer/app-shell-detail-panel.tsx rename to apps/desktop/src/renderer/shell/detail-panel.tsx diff --git a/apps/desktop/src/renderer/use-project-context.ts b/apps/desktop/src/renderer/use-project-context.ts index 2a63115f56..3a42f04f20 100644 --- a/apps/desktop/src/renderer/use-project-context.ts +++ b/apps/desktop/src/renderer/use-project-context.ts @@ -50,9 +50,11 @@ const NO_PROJECT_CAPABILITIES: DesktopProjectCapabilities = { }; /** - * Owns the workspace projection AppShell reads: the default Host's project, - * the active session's project, and the persistent project catalog behind - * them. Project mutations and the open-folder commands belong to Task Entry. + * The read-only project projection the root composes into the titlebar, + * Workbar, Module Hub, command palette and mentions: the owner Session's + * project (path, Git state, current project, capabilities) and, with no + * Session, the default Host's. Project mutations and the open-folder + * commands belong to Task Entry. */ export function useAppShellProjectContext(options: { rendererMountedRef: RefBox; @@ -64,11 +66,8 @@ export function useAppShellProjectContext(options: { /** Re-reads the default Host's project context; resolves to its projects. */ refreshProjects(): Promise; projectInfo: RendererAppInfo | null; - projects: ProjectRecord[]; projectCapabilities: DesktopProjectCapabilities; activeProjectCapabilities: DesktopProjectCapabilities; - localProjects: readonly ProjectRecord[]; - selectedProjectId: string | null | undefined; currentProjectId: string | null | undefined; currentProject: ProjectRecord | undefined; } { @@ -84,7 +83,6 @@ export function useAppShellProjectContext(options: { const [projects, setProjects] = useState([]); const [projectCapabilities, setProjectCapabilities] = useState(NO_PROJECT_CAPABILITIES); - const [localHostProjects, setLocalHostProjects] = useState([]); const [sessionProjectSnapshot, setSessionProjectSnapshot] = useState<{ sessionId: string; projects: ProjectRecord[]; @@ -139,29 +137,6 @@ export function useAppShellProjectContext(options: { }; }, []); - useEffect(() => { - let cancelled = false; - let refreshGeneration = 0; - const refresh = () => { - const generation = ++refreshGeneration; - return window.maka.projects.getLocalSnapshot().then( - (snapshot) => { - if (cancelled || generation !== refreshGeneration) return; - setLocalHostProjects([...snapshot.projects]); - }, - () => { - // The Local Host may be reconnecting; its ready event retries this read. - }, - ); - }; - const unsubscribe = window.maka.projects.subscribeLocalChanges(() => void refresh()); - void refresh(); - return () => { - cancelled = true; - unsubscribe(); - }; - }, []); - useEffect(() => { if (!sessionId) { setSessionProjectSnapshot(null); @@ -240,11 +215,8 @@ export function useAppShellProjectContext(options: { return { refreshProjects, projectInfo, - projects, projectCapabilities, activeProjectCapabilities, - localProjects: localHostProjects, - selectedProjectId, currentProjectId, currentProject, }; diff --git a/apps/desktop/stories/app-shell.stories.tsx b/apps/desktop/stories/app-shell.stories.tsx index 48a9333386..84b3f94719 100644 --- a/apps/desktop/stories/app-shell.stories.tsx +++ b/apps/desktop/stories/app-shell.stories.tsx @@ -61,7 +61,7 @@ import { SESSION_WORKBAR_DEFAULT_WIDTH, type WorkbarLayoutState, } from '../src/renderer/features/workbar/testing'; -import { AppShellDetailPanel } from '../src/renderer/app-shell-detail-panel'; +import { AppShellDetailPanel } from '../src/renderer/shell/detail-panel'; import { deriveChatTurnPresentation } from '../src/renderer/application/contracts/turn-presentation'; import { deriveSessionRail, @@ -219,10 +219,6 @@ const baseChatProps: ChatViewProps = { messages: conversation, scrollBehavior: 'smooth', activeSession, - activeConnectionLabel: 'Anthropic', - activeModel: 'claude-sonnet-4-5', - activeModelLabel: 'Claude Sonnet 4.5', - modelChoices, userLabel: '你', onNew: noop, onPromptSuggestion: noop, diff --git a/apps/desktop/stories/module-hubs.stories.tsx b/apps/desktop/stories/module-hubs.stories.tsx index 413bd3d2d3..4916307410 100644 --- a/apps/desktop/stories/module-hubs.stories.tsx +++ b/apps/desktop/stories/module-hubs.stories.tsx @@ -45,7 +45,7 @@ import { createFakeModuleHubServices, McpPage, } from '../src/renderer/features/module-hub/testing'; -import { AppShellDetailPanel } from '../src/renderer/app-shell-detail-panel'; +import { AppShellDetailPanel } from '../src/renderer/shell/detail-panel'; import { withSkillLocationCounts } from '../src/shared/skill-location-counts'; // Fidelity convention (#1433): every story below names the real app path diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md index 549bbd73d6..7a699d4889 100644 --- a/docs/astryx-surface-file-inventory.md +++ b/docs/astryx-surface-file-inventory.md @@ -30,7 +30,6 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi |------|------|-------------|------------|----------| | `apps/desktop/src/renderer/agent-graph-panel.tsx` | shell-chrome-or-panel | Banner, Button, EmptyState, IconButton, Selector, Spinner | aligned — uses Astryx (Banner, Button, EmptyState, IconButton, Selector, Spinner) | aligned | | `apps/desktop/src/renderer/app-shell-chrome-actions.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned | -| `apps/desktop/src/renderer/app-shell-detail-panel.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `apps/desktop/src/renderer/app-shell-overlays.tsx` | shell-chrome-or-panel | Spinner | aligned — uses Astryx (Spinner) | aligned | | `apps/desktop/src/renderer/app-shell.tsx` | shell-chrome-or-panel | AppShell | aligned — uses Astryx (AppShell) | aligned | | `apps/desktop/src/renderer/app.tsx` | other | Theme | aligned — uses Astryx (Theme) | aligned | @@ -212,6 +211,7 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `apps/desktop/src/renderer/settings/tasks-settings-page.tsx` | settings-page | EmptyState, IconButton, List, ListItem | aligned — uses Astryx (EmptyState, IconButton, List, ListItem) | aligned | | `apps/desktop/src/renderer/settings/usage-settings-page.tsx` | settings-page | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `apps/desktop/src/renderer/settings/web-search-settings-page.tsx` | settings-page | Banner, Button, EmptyState, Link, Selector, StatusDot, Switch, TextInput | aligned — uses Astryx (Banner, Button, EmptyState, Link, Selector, StatusDot, Switch, TextInput) | aligned | +| `apps/desktop/src/renderer/shell/detail-panel.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `apps/desktop/src/renderer/shell/window-titlebar.tsx` | shell-chrome-or-panel | Icon, IconButton, Tooltip | aligned — uses Astryx (Icon, IconButton, Tooltip) | aligned | | `apps/desktop/src/renderer/styles.css` | styles | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | | `apps/desktop/src/renderer/styles/agent-graph.css` | styles | n/a (css) | aligned — no off-rhythm control heights flagged | aligned | diff --git a/docs/astryx-surface-file-inventory.paths b/docs/astryx-surface-file-inventory.paths index 5c15ec9d17..7baaba7b86 100644 --- a/docs/astryx-surface-file-inventory.paths +++ b/docs/astryx-surface-file-inventory.paths @@ -1,6 +1,5 @@ apps/desktop/src/renderer/agent-graph-panel.tsx apps/desktop/src/renderer/app-shell-chrome-actions.tsx -apps/desktop/src/renderer/app-shell-detail-panel.tsx apps/desktop/src/renderer/app-shell-overlays.tsx apps/desktop/src/renderer/app-shell.tsx apps/desktop/src/renderer/app.tsx @@ -182,6 +181,7 @@ apps/desktop/src/renderer/settings/subagent-settings-page.tsx apps/desktop/src/renderer/settings/tasks-settings-page.tsx apps/desktop/src/renderer/settings/usage-settings-page.tsx apps/desktop/src/renderer/settings/web-search-settings-page.tsx +apps/desktop/src/renderer/shell/detail-panel.tsx apps/desktop/src/renderer/shell/window-titlebar.tsx apps/desktop/src/renderer/styles.css apps/desktop/src/renderer/styles/agent-graph.css diff --git a/packages/ui/src/chat-view.tsx b/packages/ui/src/chat-view.tsx index 9f1da9c58a..f59203a215 100644 --- a/packages/ui/src/chat-view.tsx +++ b/packages/ui/src/chat-view.tsx @@ -185,20 +185,6 @@ export function ChatView(props: { */ activeTurn?: { readonly turnId: string; readonly awaitingInput?: boolean; readonly compacting?: boolean }; activeSession?: SessionSummary; - activeConnectionLabel?: string; - activeModel?: string; - activeModelLabel?: string; - /** Renders a provider brand mark next to the model name in the chat tab. */ - activeProviderType?: ProviderType; - /** Optional renderer for the provider mark; supplied by the desktop app to - * avoid bringing the full provider SVG library into @maka/ui. */ - renderProviderMark?(type: ProviderType): ReactNode; - modelChoices?: ChatModelChoice[]; - onModelChange?(input: { - llmConnectionId: string; - llmConnectionSlug: string; - model: string; - }): void | Promise; /** Personalized user label shown on user messages. Falls back to "你". */ userLabel?: string; /** diff --git a/packages/ui/stories/attachment.stories.tsx b/packages/ui/stories/attachment.stories.tsx index 4f8c7531d1..878c33509c 100644 --- a/packages/ui/stories/attachment.stories.tsx +++ b/packages/ui/stories/attachment.stories.tsx @@ -116,10 +116,6 @@ const baseChat: ChatViewProps = { messages: [], scrollBehavior: 'smooth', activeSession: session(), - activeConnectionLabel: 'Anthropic', - activeModel: 'claude-sonnet-4-5', - activeModelLabel: 'Claude Sonnet 4.5', - modelChoices, userLabel: '你', onReadAttachmentBytes: mockReadBytes, onNew: noop, diff --git a/packages/ui/stories/quote-comment.stories.tsx b/packages/ui/stories/quote-comment.stories.tsx index 5338be0eef..1f01beb017 100644 --- a/packages/ui/stories/quote-comment.stories.tsx +++ b/packages/ui/stories/quote-comment.stories.tsx @@ -119,10 +119,6 @@ const baseChat: ChatViewProps = { messages: [], scrollBehavior: 'smooth', activeSession: session(), - activeConnectionLabel: 'Anthropic', - activeModel: 'claude-sonnet-4-5', - activeModelLabel: 'Claude Sonnet 4.5', - modelChoices, userLabel: '你', onNew: noop, onPromptSuggestion: noop, diff --git a/scripts/check-app-shell-hooks.mjs b/scripts/check-app-shell-hooks.mjs index 0dcb8d175c..71f1a1002c 100644 --- a/scripts/check-app-shell-hooks.mjs +++ b/scripts/check-app-shell-hooks.mjs @@ -114,7 +114,6 @@ export const ALLOWED = { useAppShellSessionUiReads: 1, useAppShellSessionWorkspace: 1, useAppShellTurnPresentation: 1, - useEffect: 1, useLayoutEffect: 1, // Replaces `useSessionNavigationController`, which is now called inside // `SessionNavigationProvider`. The entry shrinks rather than disappearing,