Classify chat state command selectors

This commit is contained in:
murashit 2026-06-22 20:13:37 +09:00
parent 24b38f8163
commit d38bdaf32c
14 changed files with 74 additions and 75 deletions

View file

@ -1,6 +1,6 @@
import { activeThreadId } from "../state/selectors";
import type { ChatConnectionPhase } from "../state/root-reducer";
import type { ChatStateStore } from "../state/store";
import { activeThreadId } from "../threads/state-selectors";
const STATUS_RECONNECTING = "Reconnecting...";

View file

@ -1,6 +1,6 @@
import type { AppServerClient } from "../../../../app-server/connection/client";
import type { CodexInput } from "../../../../domain/chat/input";
import { submissionStateSnapshot } from "../state/selectors";
import { submissionStateSnapshot } from "./submission-state";
import type { ChatStateStore } from "../state/store";
import { parseSlashCommand } from "../composer/suggestions";
import type { SlashCommandExecutionResult } from "./slash-command-execution";

View file

@ -3,11 +3,12 @@ import type { CodexInput } from "../../../../domain/chat/input";
import type { MessageStreamNoticeSection } from "../../domain/message-stream/items";
import type { LocalIdSource } from "../../../../shared/id/local-id";
import type { ChatRuntimeSettingsActions } from "../runtime/settings-actions";
import { activeThreadId, canImplementPlanItemId } from "../state/selectors";
import type { ChatStateStore } from "../state/store";
import type { ThreadManagementActions } from "../threads/thread-management-actions";
import type { GoalActions } from "../threads/goal-actions";
import { activeThreadId } from "../threads/state-selectors";
import { submitComposer, type ComposerSubmitActions, type ComposerSubmitActionsHost } from "./composer-submit-actions";
import { canImplementPlanItemId } from "./plan-implementation-target";
import { executeSlashCommandWithState, type SlashCommandExecutorHost } from "./slash-command-executor";
import { createTurnSubmissionActions } from "./turn-submission-actions";

View file

@ -0,0 +1,20 @@
import type { ChatActiveThreadState, ChatMessageStreamState, ChatRuntimeState, ChatState, ChatTurnState } from "../state/root-reducer";
import { chatTurnBusy } from "../state/root-reducer";
import { messageStreamItems } from "../state/message-stream";
import { latestImplementablePlanTargetFromItems, type PlanImplementationTarget } from "../../domain/message-stream/selectors";
export function canImplementPlanItemId(state: ChatState, itemId: string): boolean {
return itemId === implementPlanTargetFromState(state)?.itemId;
}
export function implementPlanTargetFromState(state: {
activeThread: Pick<ChatActiveThreadState, "id">;
turn: ChatTurnState;
runtime: Pick<ChatRuntimeState, "selectedCollaborationMode">;
messageStream: Pick<ChatMessageStreamState, "stableItems" | "activeSegment">;
}): PlanImplementationTarget | null {
if (!state.activeThread.id || chatTurnBusy(state) || state.runtime.selectedCollaborationMode !== "plan") {
return null;
}
return latestImplementablePlanTargetFromItems(messageStreamItems(state.messageStream));
}

View file

@ -13,10 +13,10 @@ import {
import type { SlashCommandName } from "../composer/slash-commands";
import type { ReasoningEffort } from "../../../../domain/catalog/metadata";
import { findModelMetadataByIdOrName, supportedEffortsForModelMetadata } from "../../../../domain/catalog/metadata";
import { submissionStateSnapshot } from "../state/selectors";
import type { ChatStateStore } from "../state/store";
import { currentModel, runtimeConfigOrDefault } from "../../domain/runtime/effective";
import { runtimeSnapshotForChatState } from "../runtime/snapshot";
import { submissionStateSnapshot } from "./submission-state";
export interface SlashCommandExecutorHost extends SlashCommandExecutionPorts {
stateStore: ChatStateStore;

View file

@ -0,0 +1,24 @@
import type { Thread } from "../../../../domain/threads/model";
import type { MessageStreamItem } from "../../domain/message-stream/items";
import { activeTurnId, chatTurnBusy, pendingTurnStart, type ChatState, type PendingTurnStart } from "../state/root-reducer";
import { messageStreamItems } from "../state/message-stream";
export interface SubmissionStateSnapshot {
activeThreadId: string | null;
activeTurnId: string | null;
busy: boolean;
listedThreads: readonly Thread[];
items: readonly MessageStreamItem[];
pendingTurnStart: PendingTurnStart | null;
}
export function submissionStateSnapshot(state: ChatState): SubmissionStateSnapshot {
return {
activeThreadId: state.activeThread.id,
activeTurnId: activeTurnId(state),
busy: chatTurnBusy(state),
listedThreads: state.threadList.listedThreads,
items: messageStreamItems(state.messageStream),
pendingTurnStart: pendingTurnStart(state),
};
}

View file

@ -2,7 +2,6 @@ import type { AppServerClient } from "../../../../app-server/connection/client";
import type { CodexInput } from "../../../../domain/chat/input";
import type { ReferencedThreadMetadata } from "../../../../domain/threads/reference";
import type { LocalIdSource } from "../../../../shared/id/local-id";
import { submissionStateSnapshot } from "../state/selectors";
import { STATUS_TURN_RUNNING } from "../state/status-text";
import type { ChatStateStore } from "../state/store";
import {
@ -12,6 +11,7 @@ import {
optimisticTurnStart,
shouldAcknowledgeTurnStart,
} from "./optimistic-turn-start";
import { submissionStateSnapshot } from "./submission-state";
const STATUS_STEERED_CURRENT_TURN = "Steered current turn.";

View file

@ -1,65 +0,0 @@
import { activeTurnId as selectActiveTurnId, chatTurnBusy, pendingTurnStart } from "./root-reducer";
import type {
ChatActiveThreadState,
ChatRuntimeState,
ChatState,
ChatMessageStreamState,
ChatTurnState,
PendingTurnStart,
} from "./root-reducer";
import type { Thread } from "../../../../domain/threads/model";
import type { MessageStreamItem } from "../../domain/message-stream/items";
import { latestImplementablePlanTargetFromItems, type PlanImplementationTarget } from "../../domain/message-stream/selectors";
import { messageStreamItems, messageStreamIsEmpty } from "./message-stream";
export interface SubmissionStateSnapshot {
activeThreadId: string | null;
activeTurnId: string | null;
busy: boolean;
listedThreads: readonly Thread[];
items: readonly MessageStreamItem[];
pendingTurnStart: PendingTurnStart | null;
}
export function activeThreadId(state: ChatState): string | null {
return state.activeThread.id;
}
export function canSwitchToThread(state: ChatState, threadId: string): boolean {
return !chatTurnBusy(state) || threadId === state.activeThread.id;
}
export function listedThreads(state: ChatState): readonly Thread[] {
return state.threadList.listedThreads;
}
export function messageStreamItemsEmpty(state: ChatState): boolean {
return messageStreamIsEmpty(state.messageStream);
}
export function submissionStateSnapshot(state: ChatState): SubmissionStateSnapshot {
return {
activeThreadId: state.activeThread.id,
activeTurnId: selectActiveTurnId(state),
busy: chatTurnBusy(state),
listedThreads: state.threadList.listedThreads,
items: messageStreamItems(state.messageStream),
pendingTurnStart: pendingTurnStart(state),
};
}
export function canImplementPlanItemId(state: ChatState, itemId: string): boolean {
return itemId === implementPlanTargetFromState(state)?.itemId;
}
export function implementPlanTargetFromState(state: {
activeThread: Pick<ChatActiveThreadState, "id">;
turn: ChatTurnState;
runtime: Pick<ChatRuntimeState, "selectedCollaborationMode">;
messageStream: Pick<ChatMessageStreamState, "stableItems" | "activeSegment">;
}): PlanImplementationTarget | null {
if (!state.activeThread.id || chatTurnBusy(state) || state.runtime.selectedCollaborationMode !== "plan") {
return null;
}
return latestImplementablePlanTargetFromItems(messageStreamItems(state.messageStream));
}

View file

@ -1,6 +1,6 @@
import type { RestorationController } from "./restoration-controller";
import { activeThreadId } from "../state/selectors";
import type { ChatStateStore } from "../state/store";
import { activeThreadId } from "./state-selectors";
export interface ActiveThreadIdentitySyncHost {
stateStore: ChatStateStore;

View file

@ -2,11 +2,11 @@ import type { AppServerClient } from "../../../../app-server/connection/client";
import type { ThreadTokenUsage } from "../../../../domain/runtime/metrics";
import { resumeChatThread, type ChatThreadResumeSnapshot } from "../../app-server/threads/resume";
import { resumedThreadAction } from "../state/actions";
import { activeThreadId, canSwitchToThread, listedThreads, messageStreamItemsEmpty } from "../state/selectors";
import type { ChatStateStore } from "../state/store";
import type { RestorationController } from "./restoration-controller";
import type { HistoryController } from "./history-controller";
import type { ChatResumeWorkTracker, ActiveChatResume } from "../lifecycle";
import { activeThreadId, canSwitchToThread, listedThreads, messageStreamItemsEmpty } from "./state-selectors";
export interface ResumeActionsHost {
stateStore: ChatStateStore;

View file

@ -1,5 +1,5 @@
import { canSwitchToThread } from "../state/selectors";
import type { ChatStateStore } from "../state/store";
import { canSwitchToThread } from "./state-selectors";
export interface SelectionActionsHost {
stateStore: ChatStateStore;

View file

@ -0,0 +1,19 @@
import type { Thread } from "../../../../domain/threads/model";
import { chatTurnBusy, type ChatState } from "../state/root-reducer";
import { messageStreamIsEmpty } from "../state/message-stream";
export function activeThreadId(state: ChatState): string | null {
return state.activeThread.id;
}
export function canSwitchToThread(state: ChatState, threadId: string): boolean {
return !chatTurnBusy(state) || threadId === state.activeThread.id;
}
export function listedThreads(state: ChatState): readonly Thread[] {
return state.threadList.listedThreads;
}
export function messageStreamItemsEmpty(state: ChatState): boolean {
return messageStreamIsEmpty(state.messageStream);
}

View file

@ -3,8 +3,8 @@ import { describe, expect, it, vi } from "vitest";
import type { AppServerClient } from "../../../../../src/app-server/connection/client";
import { createChatState } from "../../../../../src/features/chat/application/state/root-reducer";
import { createChatStateStore, type ChatStateStore } from "../../../../../src/features/chat/application/state/store";
import { implementPlanTargetFromState } from "../../../../../src/features/chat/application/state/selectors";
import { implementPlan, type PlanImplementationHost } from "../../../../../src/features/chat/application/conversation/composition";
import { implementPlanTargetFromState } from "../../../../../src/features/chat/application/conversation/plan-implementation-target";
import type { MessageStreamItem } from "../../../../../src/features/chat/domain/message-stream/items";
const planItem = (id: string): MessageStreamItem => ({

View file

@ -5,7 +5,7 @@ import { act } from "preact/test-utils";
import { MarkdownRenderer } from "obsidian";
import type { MessageStreamItem } from "../../../../../src/features/chat/domain/message-stream/items";
import { implementPlanTargetFromState } from "../../../../../src/features/chat/application/state/selectors";
import { implementPlanTargetFromState } from "../../../../../src/features/chat/application/conversation/plan-implementation-target";
import { MESSAGE_CONTENT_RENDERED_EVENT } from "../../../../../src/features/chat/ui/message-stream/content-events";
import { MarkdownMessageRenderer } from "../../../../../src/features/chat/ui/message-stream/markdown-renderer";
import { deferred } from "../../../../support/async";