Clarify chat reducer state scopes

This commit is contained in:
murashit 2026-06-17 11:12:57 +09:00
parent ca4db5f0cc
commit e8d2a637ab
5 changed files with 100 additions and 137 deletions

View file

@ -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: [],

View file

@ -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",

View file

@ -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,

View file

@ -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[],

View file

@ -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<string>;
readonly activityGroups: ReadonlySet<string>;
readonly agentDetails: ReadonlySet<string>;
readonly textDetails: ReadonlySet<string>;
readonly userMessageExpanded: ReadonlySet<string>;
readonly goalObjectiveExpanded: ReadonlySet<string>;
readonly approvalDetails: ReadonlySet<string>;
}
export type ChatDisclosureBucket = (typeof CHAT_DISCLOSURE_BUCKETS)[number];
export type ChatDisclosureUiState = Readonly<Record<ChatDisclosureBucket, ReadonlySet<string>>>;
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<string>): ChatDisclosureUiState {
const disclosures = {} as Record<ChatDisclosureBucket, ReadonlySet<string>>;
for (const bucket of CHAT_DISCLOSURE_BUCKETS) {
disclosures[bucket] = factory(bucket);
}
return disclosures;
}
function goalObjectiveResetKey(goal: ThreadGoal | null): string {