From 64057e81678d61637acce99097f34660894df34b Mon Sep 17 00:00:00 2001 From: NekoPunch Date: Tue, 1 Sep 2026 01:45:04 -0700 Subject: [PATCH 1/3] refactor(desktop): centralize navigation ports Generated-by: OpenAI Codex --- .../controller/session-row-actions.ts | 49 +++++-------------- .../use-session-navigation-controller.ts | 42 +--------------- .../features/session-navigation/index.ts | 3 +- .../features/session-navigation/ports.ts | 40 +++++++++++++++ .../features/session-navigation/testing.ts | 2 +- .../ui/session-navigation-provider.tsx | 7 +-- 6 files changed, 56 insertions(+), 87 deletions(-) diff --git a/apps/desktop/src/renderer/features/session-navigation/controller/session-row-actions.ts b/apps/desktop/src/renderer/features/session-navigation/controller/session-row-actions.ts index 8303b9f2c1..0ebc1f57fe 100644 --- a/apps/desktop/src/renderer/features/session-navigation/controller/session-row-actions.ts +++ b/apps/desktop/src/renderer/features/session-navigation/controller/session-row-actions.ts @@ -21,39 +21,12 @@ import type { SessionSummary } from '@maka/core/session'; import type { UiLocale } from '@maka/core/ui-locale'; import { getShellCopy, localizedShellErrorMessage } from '../../../locales/shell-copy.js'; import { revisionFamilySessionIds } from '@maka/core/session-revisions'; -import type { SessionNavigationSessionService } from '../ports.js'; - -type RefBox = { current: T }; - -/** What `sessions.remove` settled on. `restored` means the task is still there. */ -type SessionRemoveDisposition = 'removed' | 'restored'; - -/** - * How a delete settled together with the count the Host actually archived. - * `archivedSubtaskCount` is the Host's executed number — 0 when the delete was - * called off (`restored`) — so the toast reports a fact, not a renderer guess. - */ -type SessionRemoveOutcome = { - disposition: SessionRemoveDisposition; - archivedSubtaskCount: number; -}; - -type ToastApi = { - success(title: string, description?: string): void; - error( - title: string, - description?: string, - diagnosticDetails?: string, - diagnosticTarget?: { sessionId: string }, - ): void; - confirm(options: { - title: string; - description: string; - confirmLabel: string; - cancelLabel: string; - destructive?: boolean; - }): Promise; -}; +import type { + SessionNavigationRef, + SessionNavigationRemoveOutcome, + SessionNavigationSessionService, + SessionNavigationToastApi, +} from '../ports.js'; /** * What a sweep can honestly say afterwards. `verified: false` means the catalog @@ -116,15 +89,15 @@ export interface SessionNavigationRowActions { export function createSessionNavigationRowActions(deps: { uiLocale: UiLocale; - activeIdRef: RefBox; + activeIdRef: SessionNavigationRef; clearActiveMessages: () => void; clearSessionRendererState: (sessionId: string) => void; - pendingSessionRowActionsRef: RefBox>; + pendingSessionRowActionsRef: SessionNavigationRef>; refreshSessions: () => Promise>; service: SessionNavigationSessionService; - sessionsRef: RefBox>; + sessionsRef: SessionNavigationRef>; setActiveId: (sessionId: string | undefined) => void; - toastApi: ToastApi; + toastApi: SessionNavigationToastApi; }): SessionNavigationRowActions { const { uiLocale, @@ -258,7 +231,7 @@ export function createSessionNavigationRowActions(deps: { async function removeSessionFamily( sessionId: string, options: { requireArchived: boolean }, - ): Promise { + ): Promise { // Read before the write: the family comes off the live catalog, which no // longer lists it afterwards. const familyIds = revisionFamilySessionIds(sessionsRef.current, sessionId); diff --git a/apps/desktop/src/renderer/features/session-navigation/controller/use-session-navigation-controller.ts b/apps/desktop/src/renderer/features/session-navigation/controller/use-session-navigation-controller.ts index d676fc0222..654c88601a 100644 --- a/apps/desktop/src/renderer/features/session-navigation/controller/use-session-navigation-controller.ts +++ b/apps/desktop/src/renderer/features/session-navigation/controller/use-session-navigation-controller.ts @@ -35,7 +35,7 @@ import { sessionRailLayoutStore, type SessionRailLayoutState, } from '../model/session-rail-layout-store.js'; -import type { SessionNavigationSession } from '../ports.js'; +import type { SessionNavigationPorts, SessionNavigationSession } from '../ports.js'; import { useSessionNavigationServices } from '../services-context.js'; import { createSessionNavigationRowActions, @@ -43,46 +43,6 @@ import { } from './session-row-actions.js'; import { useSessionSelection } from './use-session-selection.js'; -export type SessionNavigationToastApi = { - success(title: string, description?: string): void; - error( - title: string, - description?: string, - diagnosticDetails?: string, - diagnosticTarget?: { sessionId: string }, - ): void; - confirm(options: { - title: string; - description: string; - confirmLabel: string; - cancelLabel: string; - destructive?: boolean; - }): Promise; -}; - -type RefBox = { current: T }; - -/** - * What the rail asks of the rest of the shell, named one by one. - * - * Switching a session also clears the active messages and leaves the Work Hub. - * Those are commands the shell issues, and they stay commands: the rail calls - * them, it does not subscribe to them. Nothing here has to be identity-stable — - * the controller reads them through a ref published on commit — so the shell - * may build this object inline, and no ordinary `function` declaration upstream - * can quietly put the rail back on every AppShell render (#4109). - */ -export interface SessionNavigationPorts { - activeIdRef: RefBox; - sessionsRef: RefBox>; - pendingSessionRowActionsRef: RefBox>; - activateSession(sessionId: string | undefined): void; - clearActiveMessages(): void; - clearSessionRendererState(sessionId: string): void; - refreshSessions(): Promise>; - toastApi: SessionNavigationToastApi; -} - export interface UseSessionNavigationControllerInput { /** * The rail projection, derived once by its owner. The command palette lists diff --git a/apps/desktop/src/renderer/features/session-navigation/index.ts b/apps/desktop/src/renderer/features/session-navigation/index.ts index 9acddf88cd..21d2153028 100644 --- a/apps/desktop/src/renderer/features/session-navigation/index.ts +++ b/apps/desktop/src/renderer/features/session-navigation/index.ts @@ -21,11 +21,10 @@ export { SessionNavigationServicesProvider } from './services-context.js'; export { SessionNavigationProvider } from './ui/session-navigation-provider.js'; export { createSessionOpenCommand } from './controller/session-open-command.js'; export { useSessionNavigationReads } from './controller/use-session-navigation-reads.js'; -export type { SessionNavigationPorts } from './controller/use-session-navigation-controller.js'; export { deriveSessionRail } from './model/session-rail.js'; export { sessionRailLayoutStore } from './model/session-rail-layout-store.js'; export type { SessionNavigationRowActions, SessionPurgeOutcome, } from './controller/session-row-actions.js'; -export type { SessionNavigationServices } from './ports.js'; +export type { SessionNavigationPorts, SessionNavigationServices } from './ports.js'; diff --git a/apps/desktop/src/renderer/features/session-navigation/ports.ts b/apps/desktop/src/renderer/features/session-navigation/ports.ts index 0b90f29adc..754e41d41d 100644 --- a/apps/desktop/src/renderer/features/session-navigation/ports.ts +++ b/apps/desktop/src/renderer/features/session-navigation/ports.ts @@ -32,6 +32,25 @@ export interface SessionNavigationRemoveOutcome { readonly archivedSubtaskCount: number; } +export type SessionNavigationRef = { current: T }; + +export type SessionNavigationToastApi = { + success(title: string, description?: string): void; + error( + title: string, + description?: string, + diagnosticDetails?: string, + diagnosticTarget?: { sessionId: string }, + ): void; + confirm(options: { + title: string; + description: string; + confirmLabel: string; + cancelLabel: string; + destructive?: boolean; + }): Promise; +}; + export interface SessionNavigationSession extends SessionSummary { readonly profileId: string; readonly profileName: string; @@ -74,3 +93,24 @@ export interface SessionNavigationSessionService { export interface SessionNavigationServices { readonly sessions: SessionNavigationSessionService; } + +/** + * What the rail asks of the rest of the shell, named one by one. + * + * Switching a session also clears the active messages and leaves the Work Hub. + * Those are commands the shell issues, and they stay commands: the rail calls + * them, it does not subscribe to them. Nothing here has to be identity-stable — + * the controller reads them through a ref published on commit — so the shell + * may build this object inline, and no ordinary `function` declaration upstream + * can quietly put the rail back on every AppShell render (#4109). + */ +export interface SessionNavigationPorts { + activeIdRef: SessionNavigationRef; + sessionsRef: SessionNavigationRef>; + pendingSessionRowActionsRef: SessionNavigationRef>; + activateSession(sessionId: string | undefined): void; + clearActiveMessages(): void; + clearSessionRendererState(sessionId: string): void; + refreshSessions(): Promise>; + toastApi: SessionNavigationToastApi; +} diff --git a/apps/desktop/src/renderer/features/session-navigation/testing.ts b/apps/desktop/src/renderer/features/session-navigation/testing.ts index 980c22f9f5..59d17855bb 100644 --- a/apps/desktop/src/renderer/features/session-navigation/testing.ts +++ b/apps/desktop/src/renderer/features/session-navigation/testing.ts @@ -20,6 +20,7 @@ import type { SessionNavigationServices } from './ports.js'; export type { + SessionNavigationPorts, SessionNavigationServices, SessionNavigationSession, SessionNavigationSessionService, @@ -33,7 +34,6 @@ export { createSessionOpenCommand } from './controller/session-open-command.js'; export { useSessionNavigationController, type SessionNavigationController, - type SessionNavigationPorts, type UseSessionNavigationControllerInput, } from './controller/use-session-navigation-controller.js'; export { useSessionSelection } from './controller/use-session-selection.js'; 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 efbcc31235..537a185540 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 @@ -36,10 +36,7 @@ import { type SessionRowActions, type SidebarUpdateReminder, } from '@maka/ui'; -import { - useSessionNavigationController, - type SessionNavigationPorts, -} from '../controller/use-session-navigation-controller.js'; +import { useSessionNavigationController } from '../controller/use-session-navigation-controller.js'; import type { SessionNavigationRowActions } from '../controller/session-row-actions.js'; import { SESSION_LIST_EXPANDED_MAX_WIDTH, @@ -47,7 +44,7 @@ import { } from '../model/session-list-layout.js'; import type { SessionRailProjection } from '../model/session-rail.js'; import { sessionRailLayoutStore } from '../model/session-rail-layout-store.js'; -import type { SessionNavigationSession } from '../ports.js'; +import type { SessionNavigationPorts, SessionNavigationSession } from '../ports.js'; /** The chrome the shell owns and the rail only displays. */ export interface SessionNavigationChromeInput { From a8664585ffbe4741a588bf76fb60159393538497 Mon Sep 17 00:00:00 2001 From: NekoPunch Date: Tue, 1 Sep 2026 02:43:30 -0700 Subject: [PATCH 2/3] refactor(desktop): reuse React's RefObject for port ref boxes The toast port stays a hand-written narrow slice: structural typing already checks it against @maka/ui's ToastApi where the shell wires it in, and deriving it via Pick would leak that API's return types into every test stub. Generated-by: OpenAI Codex --- .../features/session-navigation/README.md | 2 ++ .../controller/session-row-actions.ts | 8 +++---- .../features/session-navigation/index.ts | 6 ++++- .../features/session-navigation/ports.ts | 23 +++++++++++-------- .../features/session-navigation/testing.ts | 1 + 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/renderer/features/session-navigation/README.md b/apps/desktop/src/renderer/features/session-navigation/README.md index 498d7d4c12..d2860ca414 100644 --- a/apps/desktop/src/renderer/features/session-navigation/README.md +++ b/apps/desktop/src/renderer/features/session-navigation/README.md @@ -33,6 +33,8 @@ owns: - Consumers import production APIs from `features/session-navigation`. - Tests may additionally import `features/session-navigation/testing`. +- Contract types the shell fulfills for this feature live in `ports.ts`; + controller files export only what they implement. - Desktop Sessions bridge calls go through `SessionNavigationServices`; only `platform/desktop/create-session-navigation-services.ts` reads that bridge. - Session Navigation may use shared renderer storage/copy, core types, and Maka diff --git a/apps/desktop/src/renderer/features/session-navigation/controller/session-row-actions.ts b/apps/desktop/src/renderer/features/session-navigation/controller/session-row-actions.ts index 0ebc1f57fe..a7fa25efb0 100644 --- a/apps/desktop/src/renderer/features/session-navigation/controller/session-row-actions.ts +++ b/apps/desktop/src/renderer/features/session-navigation/controller/session-row-actions.ts @@ -21,8 +21,8 @@ import type { SessionSummary } from '@maka/core/session'; import type { UiLocale } from '@maka/core/ui-locale'; import { getShellCopy, localizedShellErrorMessage } from '../../../locales/shell-copy.js'; import { revisionFamilySessionIds } from '@maka/core/session-revisions'; +import type { RefObject } from 'react'; import type { - SessionNavigationRef, SessionNavigationRemoveOutcome, SessionNavigationSessionService, SessionNavigationToastApi, @@ -89,13 +89,13 @@ export interface SessionNavigationRowActions { export function createSessionNavigationRowActions(deps: { uiLocale: UiLocale; - activeIdRef: SessionNavigationRef; + activeIdRef: RefObject; clearActiveMessages: () => void; clearSessionRendererState: (sessionId: string) => void; - pendingSessionRowActionsRef: SessionNavigationRef>; + pendingSessionRowActionsRef: RefObject>; refreshSessions: () => Promise>; service: SessionNavigationSessionService; - sessionsRef: SessionNavigationRef>; + sessionsRef: RefObject>; setActiveId: (sessionId: string | undefined) => void; toastApi: SessionNavigationToastApi; }): SessionNavigationRowActions { diff --git a/apps/desktop/src/renderer/features/session-navigation/index.ts b/apps/desktop/src/renderer/features/session-navigation/index.ts index 21d2153028..56c56c1b60 100644 --- a/apps/desktop/src/renderer/features/session-navigation/index.ts +++ b/apps/desktop/src/renderer/features/session-navigation/index.ts @@ -27,4 +27,8 @@ export type { SessionNavigationRowActions, SessionPurgeOutcome, } from './controller/session-row-actions.js'; -export type { SessionNavigationPorts, SessionNavigationServices } from './ports.js'; +export type { + SessionNavigationPorts, + SessionNavigationServices, + SessionNavigationToastApi, +} from './ports.js'; diff --git a/apps/desktop/src/renderer/features/session-navigation/ports.ts b/apps/desktop/src/renderer/features/session-navigation/ports.ts index 754e41d41d..8461fa502d 100644 --- a/apps/desktop/src/renderer/features/session-navigation/ports.ts +++ b/apps/desktop/src/renderer/features/session-navigation/ports.ts @@ -17,6 +17,7 @@ * under the License. */ +import type { RefObject } from 'react'; import type { SessionSummary } from '@maka/core/session'; import type { RuntimeHostProfileKind } from '@maka/runtime-host/profile-kind'; @@ -32,8 +33,10 @@ export interface SessionNavigationRemoveOutcome { readonly archivedSubtaskCount: number; } -export type SessionNavigationRef = { current: T }; - +/** + * The slice of the shell toast surface the rail uses. Structural typing checks + * it against the real `ToastApi` where the shell wires it in. + */ export type SessionNavigationToastApi = { success(title: string, description?: string): void; error( @@ -99,15 +102,17 @@ export interface SessionNavigationServices { * * Switching a session also clears the active messages and leaves the Work Hub. * Those are commands the shell issues, and they stay commands: the rail calls - * them, it does not subscribe to them. Nothing here has to be identity-stable — - * the controller reads them through a ref published on commit — so the shell - * may build this object inline, and no ordinary `function` declaration upstream - * can quietly put the rail back on every AppShell render (#4109). + * them, it does not subscribe to them. The function fields need no identity + * stability — the controller reads them through a ref published on commit — so + * the shell may rebuild this object inline, and no ordinary `function` + * declaration upstream can quietly put the rail back on every AppShell render + * (#4109). The ref boxes themselves must be stable (`useRef` results): row + * actions capture them once and dereference at call time. */ export interface SessionNavigationPorts { - activeIdRef: SessionNavigationRef; - sessionsRef: SessionNavigationRef>; - pendingSessionRowActionsRef: SessionNavigationRef>; + activeIdRef: RefObject; + sessionsRef: RefObject>; + pendingSessionRowActionsRef: RefObject>; activateSession(sessionId: string | undefined): void; clearActiveMessages(): void; clearSessionRendererState(sessionId: string): void; diff --git a/apps/desktop/src/renderer/features/session-navigation/testing.ts b/apps/desktop/src/renderer/features/session-navigation/testing.ts index 59d17855bb..4f39847eaa 100644 --- a/apps/desktop/src/renderer/features/session-navigation/testing.ts +++ b/apps/desktop/src/renderer/features/session-navigation/testing.ts @@ -24,6 +24,7 @@ export type { SessionNavigationServices, SessionNavigationSession, SessionNavigationSessionService, + SessionNavigationToastApi, } from './ports.js'; export { SessionNavigationServicesProvider } from './services-context.js'; From ea56eccdca473fbc803501c17c59ca41fd64ae01 Mon Sep 17 00:00:00 2001 From: NekoPunch Date: Tue, 1 Sep 2026 03:59:00 -0700 Subject: [PATCH 3/3] refactor(desktop): drop unused toast API re-exports Generated-by: OpenAI Codex --- apps/desktop/src/renderer/features/session-navigation/index.ts | 1 - apps/desktop/src/renderer/features/session-navigation/testing.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/apps/desktop/src/renderer/features/session-navigation/index.ts b/apps/desktop/src/renderer/features/session-navigation/index.ts index 56c56c1b60..38cadf9fe5 100644 --- a/apps/desktop/src/renderer/features/session-navigation/index.ts +++ b/apps/desktop/src/renderer/features/session-navigation/index.ts @@ -30,5 +30,4 @@ export type { export type { SessionNavigationPorts, SessionNavigationServices, - SessionNavigationToastApi, } from './ports.js'; diff --git a/apps/desktop/src/renderer/features/session-navigation/testing.ts b/apps/desktop/src/renderer/features/session-navigation/testing.ts index 4f39847eaa..59d17855bb 100644 --- a/apps/desktop/src/renderer/features/session-navigation/testing.ts +++ b/apps/desktop/src/renderer/features/session-navigation/testing.ts @@ -24,7 +24,6 @@ export type { SessionNavigationServices, SessionNavigationSession, SessionNavigationSessionService, - SessionNavigationToastApi, } from './ports.js'; export { SessionNavigationServicesProvider } from './services-context.js';