From e8d2a637ab1929ad329ad0a490a9eb6385a427af Mon Sep 17 00:00:00 2001 From: murashit Date: Wed, 17 Jun 2026 11:12:57 +0900 Subject: [PATCH] Clarify chat reducer state scopes --- .../application/pending-requests/state.ts | 11 ++ .../chat/application/state/actions.ts | 11 -- .../chat/application/state/message-stream.ts | 21 +++ .../chat/application/state/root-reducer.ts | 137 ++++++------------ .../chat/application/state/ui-state.ts | 57 +++----- 5 files changed, 100 insertions(+), 137 deletions(-) diff --git a/src/features/chat/application/pending-requests/state.ts b/src/features/chat/application/pending-requests/state.ts index 2f9088e5..e453e682 100644 --- a/src/features/chat/application/pending-requests/state.ts +++ b/src/features/chat/application/pending-requests/state.ts @@ -17,6 +17,17 @@ export type RequestAction = | { type: "request/user-input-queued"; input: PendingUserInput } | { type: "request/user-input-draft-set"; key: string; value: string }; +export function isRequestAction(action: { type: string }): action is RequestAction { + switch (action.type) { + case "request/approval-queued": + case "request/user-input-queued": + case "request/user-input-draft-set": + return true; + default: + return false; + } +} + export function initialChatRequestState(): ChatRequestState { return { approvals: [], diff --git a/src/features/chat/application/state/actions.ts b/src/features/chat/application/state/actions.ts index b966c399..6e9a4e0e 100644 --- a/src/features/chat/application/state/actions.ts +++ b/src/features/chat/application/state/actions.ts @@ -88,12 +88,6 @@ export interface DisclosureSetAction { open: boolean; } -export interface UserInputDraftSetAction { - type: "request/user-input-draft-set"; - key: string; - value: string; -} - export interface TurnOptimisticStartedAction { type: "turn/optimistic-started"; item: MessageStreamItem; @@ -111,11 +105,6 @@ export interface TurnStartFailedAction { items: readonly MessageStreamItem[]; } -export interface MessageStreamItemAddedAction { - type: "message-stream/item-added"; - item: MessageStreamItem; -} - export function activeThreadSettingsAppliedAction(settings: ActiveThreadSettingsAppliedActionSettings): ActiveThreadSettingsAppliedAction { return { type: "active-thread/settings-applied", diff --git a/src/features/chat/application/state/message-stream.ts b/src/features/chat/application/state/message-stream.ts index 7ddf94e2..d66c4b29 100644 --- a/src/features/chat/application/state/message-stream.ts +++ b/src/features/chat/application/state/message-stream.ts @@ -70,6 +70,27 @@ export type MessageStreamAction = } | { type: "message-stream/turn-diff-updated"; turnId: string; diff: string }; +export function isMessageStreamAction(action: { type: string }): action is MessageStreamAction { + switch (action.type) { + case "message-stream/item-added": + case "message-stream/system-item-added": + case "message-stream/deduped-log-added": + case "message-stream/history-loading-set": + case "message-stream/items-replaced": + case "message-stream/item-upserted": + case "message-stream/reasoning-completed": + case "message-stream/assistant-delta-appended": + case "message-stream/plan-delta-appended": + case "message-stream/item-text-appended": + case "message-stream/tool-output-appended": + case "message-stream/item-output-appended": + case "message-stream/turn-diff-updated": + return true; + default: + return false; + } +} + export function initialChatMessageStreamState(items: readonly MessageStreamItem[] = []): ChatMessageStreamState { return { stableItems: items, diff --git a/src/features/chat/application/state/root-reducer.ts b/src/features/chat/application/state/root-reducer.ts index 7e102abc..e02ad350 100644 --- a/src/features/chat/application/state/root-reducer.ts +++ b/src/features/chat/application/state/root-reducer.ts @@ -38,14 +38,13 @@ import type { ClearLocalTurnAction, ConnectionInitializedAction, ThreadListAppliedAction, - MessageStreamItemAddedAction, TurnOptimisticStartedAction, TurnStartAcknowledgedAction, TurnStartFailedAction, - UserInputDraftSetAction, } from "./actions"; import { initialChatMessageStreamState, + isMessageStreamAction, messageStreamItems, messageStreamStartActiveSegment, messageStreamWithActiveTurnItems, @@ -57,6 +56,7 @@ import { } from "./message-stream"; import { initialChatRequestState, + isRequestAction, reduceRequestSlice, resolveChatRequest, type ChatRequestState, @@ -246,9 +246,7 @@ type ChatSliceAction = | ActiveThreadAction | RuntimeAction | RequestAction - | UserInputDraftSetAction | MessageStreamAction - | MessageStreamItemAddedAction | ComposerAction | UiAction; @@ -291,9 +289,9 @@ export function chatReducer(state: ChatState, action: ChatAction): ChatState { function reduceChatTransition(state: ChatState, action: ChatTransitionAction): ChatState { switch (action.type) { case "connection/scoped-cleared": - return reduceDisconnectedConnectionStateClearedTransition(state); + return clearConnectionScopedState(state); case "active-thread/cleared": - return reduceActiveThreadClearedTransition(state); + return clearThreadScopedState(state); case "active-thread/resumed": return reduceActiveThreadResumedTransition(state, action); case "active-thread/settings-applied": @@ -307,7 +305,7 @@ function reduceChatTransition(state: ChatState, action: ChatTransitionAction): C case "turn/completed": return reduceTurnCompletedTransition(state, action); case "turn/scoped-cleared": - return reduceTurnScopedClearedTransition(state); + return clearTurnScopedState(state); case "turn/optimistic-started": return reduceTurnOptimisticStartedTransition(state, action); case "turn/start-acknowledged": @@ -321,17 +319,10 @@ function reduceChatTransition(state: ChatState, action: ChatTransitionAction): C } } -function reduceDisconnectedConnectionStateClearedTransition(state: ChatState): ChatState { - return clearDisconnectedConnectionState(state); -} - -function reduceActiveThreadClearedTransition(state: ChatState): ChatState { - return clearActiveThreadState(state); -} - function reduceActiveThreadResumedTransition(state: ChatState, action: ActiveThreadResumedAction): ChatState { const runtimeBase = action.preserveRequestedRuntimeSettings ? state.runtime : initialChatRuntimeState(); - return patchChatState(clearActiveTurnState(state), { + const turnScopedState = clearTurnScopedState(state); + return patchChatState(turnScopedState, { connection: { ...state.connection, statusText: action.status ?? state.connection.statusText, @@ -385,7 +376,7 @@ function reduceActiveThreadSettingsAppliedTransition(state: ChatState, action: A } function reduceActiveThreadRestoredPlaceholderTransition(state: ChatState, action: ActiveThreadRestoredPlaceholderAction): ChatState { - return clearActiveTurnState( + return clearTurnScopedState( patchChatState(state, { activeThread: { id: action.threadId, @@ -432,10 +423,6 @@ function reduceTurnCompletedTransition(state: ChatState, action: TurnCompletedAc }); } -function reduceTurnScopedClearedTransition(state: ChatState): ChatState { - return clearActiveTurnState(state); -} - function reduceTurnOptimisticStartedTransition(state: ChatState, action: TurnOptimisticStartedAction): ChatState { const lifecycle = transitionChatTurnLifecycleState(state.turn.lifecycle, { type: "optimistic-started", @@ -492,6 +479,44 @@ function reduceRequestResolvedTransition(state: ChatState, action: RequestResolv }); } +function clearTurnScopedState(state: ChatState): ChatState { + return patchChatState(state, { + turn: { + lifecycle: transitionChatTurnLifecycleState(state.turn.lifecycle, { type: "cleared" }), + }, + messageStream: messageStreamWithItems(state.messageStream, messageStreamItems(state.messageStream)), + requests: initialRequestState(), + ui: clearAllRequestDisclosures(state.ui), + }); +} + +function clearThreadScopedState(state: ChatState): ChatState { + return clearTurnScopedState( + patchChatState(state, { + activeThread: initialActiveThreadState(), + runtime: initialChatRuntimeState(), + messageStream: initialMessageStreamState(), + composer: initialComposerState(), + ui: initialUiState(), + }), + ); +} + +function clearConnectionScopedState(state: ChatState): ChatState { + return patchChatState(clearTurnScopedState(state), { + activeThread: initialActiveThreadState(), + runtime: initialChatRuntimeState(), + connection: { + ...state.connection, + serverDiagnostics: createServerDiagnostics(), + rateLimit: null, + availableModels: [], + availableSkills: [], + }, + threadList: initialThreadListState(), + }); +} + function reduceChatSlices(state: ChatState, action: ChatSliceAction): ChatState { return patchChatState(state, { connection: reduceConnectionSlice(state.connection, action), @@ -601,44 +626,6 @@ function reduceComposerSlice(state: ChatComposerState, action: ChatSliceAction): } } -function clearActiveTurnState(state: ChatState): ChatState { - return patchChatState(state, { - turn: { - lifecycle: transitionChatTurnLifecycleState(state.turn.lifecycle, { type: "cleared" }), - }, - messageStream: messageStreamWithItems(state.messageStream, messageStreamItems(state.messageStream)), - requests: initialRequestState(), - ui: clearAllRequestDisclosures(state.ui), - }); -} - -function clearActiveThreadState(state: ChatState): ChatState { - return clearActiveTurnState( - patchChatState(state, { - activeThread: initialActiveThreadState(), - runtime: initialChatRuntimeState(), - messageStream: initialMessageStreamState(), - composer: initialComposerState(), - ui: initialUiState(), - }), - ); -} - -function clearDisconnectedConnectionState(state: ChatState): ChatState { - return patchChatState(clearActiveTurnState(state), { - activeThread: initialActiveThreadState(), - runtime: initialChatRuntimeState(), - connection: { - ...state.connection, - serverDiagnostics: createServerDiagnostics(), - rateLimit: null, - availableModels: [], - availableSkills: [], - }, - threadList: initialThreadListState(), - }); -} - function initialConnectionState(): ChatConnectionState { return { phase: { kind: "idle" }, @@ -749,38 +736,6 @@ function cloneActiveSegment(segment: ChatMessageStreamActiveSegment | null): Cha }; } -function isMessageStreamAction(action: ChatSliceAction): action is MessageStreamAction { - switch (action.type) { - case "message-stream/item-added": - case "message-stream/system-item-added": - case "message-stream/deduped-log-added": - case "message-stream/history-loading-set": - case "message-stream/items-replaced": - case "message-stream/item-upserted": - case "message-stream/reasoning-completed": - case "message-stream/assistant-delta-appended": - case "message-stream/plan-delta-appended": - case "message-stream/item-text-appended": - case "message-stream/tool-output-appended": - case "message-stream/item-output-appended": - case "message-stream/turn-diff-updated": - return true; - default: - return false; - } -} - -function isRequestAction(action: ChatSliceAction): action is RequestAction { - switch (action.type) { - case "request/approval-queued": - case "request/user-input-queued": - case "request/user-input-draft-set": - return true; - default: - return false; - } -} - function setComposerSuggestionsSlice( state: ChatComposerState, suggestions: readonly ComposerSuggestion[], diff --git a/src/features/chat/application/state/ui-state.ts b/src/features/chat/application/state/ui-state.ts index 8cca6219..39bb0368 100644 --- a/src/features/chat/application/state/ui-state.ts +++ b/src/features/chat/application/state/ui-state.ts @@ -28,24 +28,19 @@ interface ChatMessageActionsUiState { readonly forkActionsItemId: string | null; } -export type ChatDisclosureBucket = - | "toolResults" - | "activityGroups" - | "agentDetails" - | "textDetails" - | "userMessageExpanded" - | "goalObjectiveExpanded" - | "approvalDetails"; +const CHAT_DISCLOSURE_BUCKETS = [ + "toolResults", + "activityGroups", + "agentDetails", + "textDetails", + "userMessageExpanded", + "goalObjectiveExpanded", + "approvalDetails", +] as const; -export interface ChatDisclosureUiState { - readonly toolResults: ReadonlySet; - readonly activityGroups: ReadonlySet; - readonly agentDetails: ReadonlySet; - readonly textDetails: ReadonlySet; - readonly userMessageExpanded: ReadonlySet; - readonly goalObjectiveExpanded: ReadonlySet; - readonly approvalDetails: ReadonlySet; -} +export type ChatDisclosureBucket = (typeof CHAT_DISCLOSURE_BUCKETS)[number]; + +export type ChatDisclosureUiState = Readonly>>; export interface ChatUiState { readonly toolbarPanel: "history" | "chat-actions" | "status-panel" | null; @@ -152,15 +147,7 @@ export function reduceUiSlice(state: ChatUiState, action: UiAction): ChatUiState } export function cloneDisclosureUiState(state: ChatDisclosureUiState): ChatDisclosureUiState { - return { - toolResults: new Set(state.toolResults), - activityGroups: new Set(state.activityGroups), - agentDetails: new Set(state.agentDetails), - textDetails: new Set(state.textDetails), - userMessageExpanded: new Set(state.userMessageExpanded), - goalObjectiveExpanded: new Set(state.goalObjectiveExpanded), - approvalDetails: new Set(state.approvalDetails), - }; + return disclosureUiStateFrom((bucket) => new Set(state[bucket])); } export function maybeClearGoalObjectiveExpansion( @@ -247,15 +234,15 @@ function initialMessageActionsUiState(): ChatMessageActionsUiState { } function initialDisclosureUiState(): ChatDisclosureUiState { - return { - toolResults: new Set(), - activityGroups: new Set(), - agentDetails: new Set(), - textDetails: new Set(), - userMessageExpanded: new Set(), - goalObjectiveExpanded: new Set(), - approvalDetails: new Set(), - }; + return disclosureUiStateFrom(() => new Set()); +} + +function disclosureUiStateFrom(factory: (bucket: ChatDisclosureBucket) => ReadonlySet): ChatDisclosureUiState { + const disclosures = {} as Record>; + for (const bucket of CHAT_DISCLOSURE_BUCKETS) { + disclosures[bucket] = factory(bucket); + } + return disclosures; } function goalObjectiveResetKey(goal: ThreadGoal | null): string {