Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = { 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<boolean>;
};
import type { RefObject } from 'react';
import type {
SessionNavigationRemoveOutcome,
SessionNavigationSessionService,
SessionNavigationToastApi,
} from '../ports.js';

/**
* What a sweep can honestly say afterwards. `verified: false` means the catalog
Expand Down Expand Up @@ -116,15 +89,15 @@ export interface SessionNavigationRowActions {

export function createSessionNavigationRowActions(deps: {
uiLocale: UiLocale;
activeIdRef: RefBox<string | undefined>;
activeIdRef: RefObject<string | undefined>;
clearActiveMessages: () => void;
clearSessionRendererState: (sessionId: string) => void;
pendingSessionRowActionsRef: RefBox<Set<string>>;
pendingSessionRowActionsRef: RefObject<Set<string>>;
refreshSessions: () => Promise<ReadonlyArray<SessionSummary>>;
service: SessionNavigationSessionService;
sessionsRef: RefBox<ReadonlyArray<SessionSummary>>;
sessionsRef: RefObject<ReadonlyArray<SessionSummary>>;
setActiveId: (sessionId: string | undefined) => void;
toastApi: ToastApi;
toastApi: SessionNavigationToastApi;
}): SessionNavigationRowActions {
const {
uiLocale,
Expand Down Expand Up @@ -258,7 +231,7 @@ export function createSessionNavigationRowActions(deps: {
async function removeSessionFamily(
sessionId: string,
options: { requireArchived: boolean },
): Promise<SessionRemoveOutcome> {
): Promise<SessionNavigationRemoveOutcome> {
// Read before the write: the family comes off the live catalog, which no
// longer lists it afterwards.
const familyIds = revisionFamilySessionIds(sessionsRef.current, sessionId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,54 +35,14 @@ 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,
type SessionNavigationRowActions,
} 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<boolean>;
};

type RefBox<T> = { 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<string | undefined>;
sessionsRef: RefBox<ReadonlyArray<SessionSummary>>;
pendingSessionRowActionsRef: RefBox<Set<string>>;
activateSession(sessionId: string | undefined): void;
clearActiveMessages(): void;
clearSessionRendererState(sessionId: string): void;
refreshSessions(): Promise<ReadonlyArray<SessionSummary>>;
toastApi: SessionNavigationToastApi;
}

export interface UseSessionNavigationControllerInput {
/**
* The rail projection, derived once by its owner. The command palette lists
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ 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';
45 changes: 45 additions & 0 deletions apps/desktop/src/renderer/features/session-navigation/ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -32,6 +33,27 @@ export interface SessionNavigationRemoveOutcome {
readonly archivedSubtaskCount: number;
}

/**
* 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(
title: string,
description?: string,
diagnosticDetails?: string,
diagnosticTarget?: { sessionId: string },
): void;
confirm(options: {
title: string;
description: string;
confirmLabel: string;
cancelLabel: string;
destructive?: boolean;
}): Promise<boolean>;
};

export interface SessionNavigationSession extends SessionSummary {
readonly profileId: string;
readonly profileName: string;
Expand Down Expand Up @@ -74,3 +96,26 @@ 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. 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: RefObject<string | undefined>;
sessionsRef: RefObject<ReadonlyArray<SessionSummary>>;
pendingSessionRowActionsRef: RefObject<Set<string>>;
activateSession(sessionId: string | undefined): void;
clearActiveMessages(): void;
clearSessionRendererState(sessionId: string): void;
refreshSessions(): Promise<ReadonlyArray<SessionSummary>>;
toastApi: SessionNavigationToastApi;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import type { SessionNavigationServices } from './ports.js';

export type {
SessionNavigationPorts,
SessionNavigationServices,
SessionNavigationSession,
SessionNavigationSessionService,
Expand All @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,15 @@ 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,
SESSION_LIST_EXPANDED_MIN_WIDTH,
} 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 {
Expand Down