// Generated from types/*.ts — do not edit. // Regenerate with: npm run generate:typescript /** * Session Channel Reducer — Pure reducer for `SessionState`. * * @module channels-session/reducer */ import { ActionType } from '../common/actions.js'; import type { SessionState, SessionInputRequest, ChildCustomization, Customization, CustomizationEnablement, McpServerCustomization, } from './state.js'; import { SessionLifecycle, SessionStatus, SessionInputRequestKind, CustomizationType, McpServerStatus, } from './state.js'; import type { SessionAction } from '../action-origin.generated.js'; import { softAssertNever } from '../common/reducer-helpers.js'; // ─── Helpers ───────────────────────────────────────────────────────────────── /** Bitmask covering the mutually-exclusive activity bits (bits 0–4). */ const STATUS_ACTIVITY_MASK = (1 << 5) - 1; /** Sets or clears a metadata flag on a status value. */ function withStatusFlag(status: SessionStatus, flag: SessionStatus, set: boolean): SessionStatus { return set ? status | flag : status & ~flag; } /** * Whether an entry blocks on the *user*. * * {@link SessionInputRequestKind.ToolClientExecution} is work delegated to a * client, not a prompt: the call has already cleared its confirmation gate and * is simply running somewhere else. Counting it would report a session as * awaiting the user for the entire duration of every client tool call. */ function awaitsUser(request: SessionInputRequest): boolean { return request.kind !== SessionInputRequestKind.ToolClientExecution; } /** * Reflects the session-level {@link SessionState.inputNeeded | input queue} * into the activity bits of `status`. A queue holding any user-blocking entry * promotes the activity to {@link SessionStatus.InputNeeded}; draining those * entries clears the input-needed-specific bit. Since `InputNeeded` implies * {@link SessionStatus.InProgress}, an unblocked turn falls back to * `InProgress` while an already-idle session stays idle. Orthogonal flags * (`IsRead` / `IsArchived`) are preserved. */ function withInputNeededStatus(status: SessionStatus, inputNeeded: readonly SessionInputRequest[]): SessionStatus { if (inputNeeded.some(awaitsUser)) { return (status & ~STATUS_ACTIVITY_MASK) | SessionStatus.InputNeeded; } return status & ~(SessionStatus.InputNeeded & ~SessionStatus.InProgress); } function updateMcpServerCustomization( state: SessionState, id: string, update: (entry: McpServerCustomization) => McpServerCustomization, ): SessionState { const list = state.customizations; if (!list) { return state; } const topIdx = list.findIndex(c => c.id === id); if (topIdx >= 0) { const entry = list[topIdx]; if (entry.type !== CustomizationType.McpServer) { return state; } const updated = list.slice(); updated[topIdx] = update(entry); return { ...state, customizations: updated }; } let changed = false; const updated = list.map(container => { if (container.type === CustomizationType.McpServer) { return container; } const children = container.children; if (!children) { return container; } const childIdx = children.findIndex(c => c.id === id); if (childIdx < 0) { return container; } const child = children[childIdx]; if (child.type !== CustomizationType.McpServer) { return container; } changed = true; const newChildren = children.slice(); newChildren[childIdx] = update(child); return { ...container, children: newChildren }; }); if (!changed) { return state; } return { ...state, customizations: updated }; } /** * Replaces explicit decisions for plugins and MCP servers; other customizations * retain their legacy `enabled` field, derived from the incoming decisions. */ function applyCustomizationEnablement(customization: Customization, enablement: readonly CustomizationEnablement[]): Customization; function applyCustomizationEnablement(customization: ChildCustomization, enablement: readonly CustomizationEnablement[]): ChildCustomization; function applyCustomizationEnablement(customization: Customization | ChildCustomization, enablement: readonly CustomizationEnablement[]): Customization | ChildCustomization { switch (customization.type) { case CustomizationType.Plugin: case CustomizationType.McpServer: { if (enablement.length > 0) { return { ...customization, enablement: [...enablement] }; } const { enablement: _enablement, ...withoutEnablement } = customization; return withoutEnablement; } default: return { ...customization, enabled: enablement[0]?.enabled ?? true }; } } // ─── Session Reducer ───────────────────────────────────────────────────────── /** * Pure reducer for session state. Handles all {@link SessionAction} variants. */ export function sessionReducer(state: SessionState, action: SessionAction, log?: (msg: string) => void): SessionState { switch (action.type) { // ── Lifecycle ────────────────────────────────────────────────────────── case ActionType.SessionReady: // `SessionReady` is purely a lifecycle transition (Creating -> // Ready). It must not touch `status`: for provisional sessions the // first turn can start before materialization completes, so an // `activeTurn` may already be set when this action is dispatched // (e.g. from a materialize-session handler). Other reducers keep // `status` in sync with the activity state, so leaving it alone here // is correct. return { ...state, lifecycle: SessionLifecycle.Ready }; case ActionType.SessionCreationFailed: return { ...state, lifecycle: SessionLifecycle.Failed, creationError: action.error, }; case ActionType.SessionChatAdded: { const list = state.chats; const idx = list.findIndex(c => c.resource === action.summary.resource); if (idx < 0) { return { ...state, chats: [...list, action.summary] }; } const updated = list.slice(); updated[idx] = action.summary; return { ...state, chats: updated }; } case ActionType.SessionChatRemoved: { const list = state.chats; const idx = list.findIndex(c => c.resource === action.chat); if (idx < 0) { return state; } const updated = list.slice(); updated.splice(idx, 1); const next: SessionState = { ...state, chats: updated }; if (state.defaultChat === action.chat) { delete next.defaultChat; } return next; } case ActionType.SessionChatUpdated: { const list = state.chats; const idx = list.findIndex(c => c.resource === action.chat); if (idx < 0) { return state; } const { resource: _ignored, ...changes } = action.changes; const updated = list.slice(); updated[idx] = { ...list[idx], ...changes }; return { ...state, chats: updated }; } case ActionType.SessionDefaultChatChanged: return { ...state, defaultChat: action.defaultChat }; // ── Metadata ────────────────────────────────────────────────────────── case ActionType.SessionTitleChanged: return { ...state, title: action.title }; case ActionType.SessionIsReadChanged: return { ...state, status: withStatusFlag(state.status, SessionStatus.IsRead, action.isRead), }; case ActionType.SessionIsArchivedChanged: return { ...state, status: withStatusFlag(state.status, SessionStatus.IsArchived, action.isArchived), }; case ActionType.SessionActivityChanged: return { ...state, activity: action.activity }; case ActionType.SessionChangesetsChanged: { const { changesets: _omit, ...stateWithoutChangesets } = state; return action.changesets ? { ...stateWithoutChangesets, changesets: action.changesets } : stateWithoutChangesets; } case ActionType.SessionConfigChanged: if (!state.config) { return state; } return { ...state, config: { ...state.config, values: action.replace ? { ...action.config } : { ...state.config.values, ...action.config }, }, }; case ActionType.SessionMetaChanged: return { ...state, _meta: action._meta }; case ActionType.SessionServerToolsChanged: return { ...state, serverTools: action.tools }; case ActionType.SessionActiveClientSet: { const list = state.activeClients; const idx = list.findIndex(c => c.clientId === action.activeClient.clientId); if (idx < 0) { return { ...state, activeClients: [...list, action.activeClient] }; } const updated = list.slice(); updated[idx] = action.activeClient; return { ...state, activeClients: updated }; } case ActionType.SessionActiveClientRemoved: { const list = state.activeClients; const idx = list.findIndex(c => c.clientId === action.clientId); if (idx < 0) { return state; } const updated = list.slice(); updated.splice(idx, 1); return { ...state, activeClients: updated }; } // ── Working Directories ───────────────────────────────────────────── case ActionType.SessionWorkingDirectorySet: { const list = state.workingDirectories ?? []; if (list.includes(action.directory)) { return state; } return { ...state, workingDirectories: [...list, action.directory] }; } case ActionType.SessionWorkingDirectoryRemoved: { const list = state.workingDirectories; if (!list) { return state; } const idx = list.indexOf(action.directory); if (idx < 0) { return state; } const updated = list.slice(); updated.splice(idx, 1); return { ...state, workingDirectories: updated }; } case ActionType.SessionWorkingDirectoryReplaced: { const list = state.workingDirectories; if (!list) { return state; } const idx = list.indexOf(action.directory); if (idx < 0) { return state; } const replacementIdx = list.indexOf(action.replacement); if (replacementIdx >= 0 && replacementIdx < idx) { return { ...state, workingDirectories: list.filter((_, index) => index !== idx), }; } return { ...state, workingDirectories: list .map((directory, index) => (index === idx ? action.replacement : directory)) .filter((directory, index) => index === idx || directory !== action.replacement), }; } // ── Input Needed ──────────────────────────────────────────────────── case ActionType.SessionInputNeededSet: { const list = state.inputNeeded ?? []; const idx = list.findIndex(r => r.id === action.request.id); const inputNeeded = idx < 0 ? [...list, action.request] : list.slice(); if (idx >= 0) { inputNeeded[idx] = action.request; } return { ...state, inputNeeded, status: withInputNeededStatus(state.status, inputNeeded) }; } case ActionType.SessionInputNeededRemoved: { const list = state.inputNeeded; if (!list) { return state; } const idx = list.findIndex(r => r.id === action.id); if (idx < 0) { return state; } const remaining = list.slice(); remaining.splice(idx, 1); const next: SessionState = { ...state, status: withInputNeededStatus(state.status, remaining) }; if (remaining.length > 0) { next.inputNeeded = remaining; } else { delete next.inputNeeded; } return next; } // ── Customizations ────────────────────────────────────────────────── case ActionType.SessionCustomizationsChanged: return { ...state, customizations: action.customizations }; case ActionType.SessionCustomizationToggled: { const list = state.customizations; if (!list) { return state; } const topIdx = list.findIndex(c => c.id === action.id); if (topIdx >= 0) { const updated = list.slice(); updated[topIdx] = applyCustomizationEnablement(list[topIdx], action.enablement); return { ...state, customizations: updated }; } for (let i = 0; i < list.length; i++) { const container = list[i]; if (container.type === CustomizationType.McpServer) { continue; } const children = container.children; if (!children) { continue; } const childIdx = children.findIndex(c => c.id === action.id); if (childIdx < 0) { continue; } const newChildren = children.slice(); newChildren[childIdx] = applyCustomizationEnablement(children[childIdx], action.enablement); const updated = list.slice(); updated[i] = { ...container, children: newChildren }; return { ...state, customizations: updated }; } return state; } case ActionType.SessionCustomizationUpdated: { const list = state.customizations ?? []; const idx = list.findIndex(c => c.id === action.customization.id); if (idx < 0) { return { ...state, customizations: [...list, action.customization] }; } const updated = [...list]; updated[idx] = action.customization; return { ...state, customizations: updated }; } case ActionType.SessionCustomizationRemoved: { const list = state.customizations; if (!list) { return state; } const topIdx = list.findIndex(c => c.id === action.id); if (topIdx >= 0) { const updated = list.slice(); updated.splice(topIdx, 1); return { ...state, customizations: updated }; } let changed = false; const updated = list.map(container => { if (container.type === CustomizationType.McpServer) { return container; } const children = container.children; if (!children) { return container; } const childIdx = children.findIndex(c => c.id === action.id); if (childIdx < 0) { return container; } changed = true; const newChildren = children.slice(); newChildren.splice(childIdx, 1); return { ...container, children: newChildren }; }); if (!changed) { return state; } return { ...state, customizations: updated }; } case ActionType.SessionMcpServerStateChanged: { return updateMcpServerCustomization(state, action.id, entry => ({ ...entry, state: action.state, channel: action.channel, })); } case ActionType.SessionMcpServerStartRequested: { return updateMcpServerCustomization(state, action.id, entry => ({ ...entry, state: { kind: McpServerStatus.Starting }, channel: undefined, })); } case ActionType.SessionMcpServerStopRequested: { return updateMcpServerCustomization(state, action.id, entry => ({ ...entry, state: { kind: McpServerStatus.Stopped }, channel: undefined, })); } default: softAssertNever(action, log); return state; } }