diff --git a/src/features/chat/application/connection/reconnect-actions.ts b/src/features/chat/application/connection/reconnect-actions.ts index 57f99a8b..8c5d7a37 100644 --- a/src/features/chat/application/connection/reconnect-actions.ts +++ b/src/features/chat/application/connection/reconnect-actions.ts @@ -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..."; diff --git a/src/features/chat/application/conversation/composer-submit-actions.ts b/src/features/chat/application/conversation/composer-submit-actions.ts index 8d628fe4..9d338a52 100644 --- a/src/features/chat/application/conversation/composer-submit-actions.ts +++ b/src/features/chat/application/conversation/composer-submit-actions.ts @@ -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"; diff --git a/src/features/chat/application/conversation/composition.ts b/src/features/chat/application/conversation/composition.ts index 17851efb..57a428a0 100644 --- a/src/features/chat/application/conversation/composition.ts +++ b/src/features/chat/application/conversation/composition.ts @@ -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"; diff --git a/src/features/chat/application/conversation/plan-implementation-target.ts b/src/features/chat/application/conversation/plan-implementation-target.ts new file mode 100644 index 00000000..a436a085 --- /dev/null +++ b/src/features/chat/application/conversation/plan-implementation-target.ts @@ -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; + turn: ChatTurnState; + runtime: Pick; + messageStream: Pick; +}): PlanImplementationTarget | null { + if (!state.activeThread.id || chatTurnBusy(state) || state.runtime.selectedCollaborationMode !== "plan") { + return null; + } + return latestImplementablePlanTargetFromItems(messageStreamItems(state.messageStream)); +} diff --git a/src/features/chat/application/conversation/slash-command-executor.ts b/src/features/chat/application/conversation/slash-command-executor.ts index e801079e..2b8e4ac5 100644 --- a/src/features/chat/application/conversation/slash-command-executor.ts +++ b/src/features/chat/application/conversation/slash-command-executor.ts @@ -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; diff --git a/src/features/chat/application/conversation/submission-state.ts b/src/features/chat/application/conversation/submission-state.ts new file mode 100644 index 00000000..6aa572d1 --- /dev/null +++ b/src/features/chat/application/conversation/submission-state.ts @@ -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), + }; +} diff --git a/src/features/chat/application/conversation/turn-submission-actions.ts b/src/features/chat/application/conversation/turn-submission-actions.ts index 7d5536d6..1445516a 100644 --- a/src/features/chat/application/conversation/turn-submission-actions.ts +++ b/src/features/chat/application/conversation/turn-submission-actions.ts @@ -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."; diff --git a/src/features/chat/application/state/selectors.ts b/src/features/chat/application/state/selectors.ts deleted file mode 100644 index 4992d61a..00000000 --- a/src/features/chat/application/state/selectors.ts +++ /dev/null @@ -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; - turn: ChatTurnState; - runtime: Pick; - messageStream: Pick; -}): PlanImplementationTarget | null { - if (!state.activeThread.id || chatTurnBusy(state) || state.runtime.selectedCollaborationMode !== "plan") { - return null; - } - return latestImplementablePlanTargetFromItems(messageStreamItems(state.messageStream)); -} diff --git a/src/features/chat/application/threads/active-thread-identity-sync.ts b/src/features/chat/application/threads/active-thread-identity-sync.ts index 6aba6c36..f9e7bee5 100644 --- a/src/features/chat/application/threads/active-thread-identity-sync.ts +++ b/src/features/chat/application/threads/active-thread-identity-sync.ts @@ -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; diff --git a/src/features/chat/application/threads/resume-actions.ts b/src/features/chat/application/threads/resume-actions.ts index aec4c56a..77ec2e28 100644 --- a/src/features/chat/application/threads/resume-actions.ts +++ b/src/features/chat/application/threads/resume-actions.ts @@ -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; diff --git a/src/features/chat/application/threads/selection-actions.ts b/src/features/chat/application/threads/selection-actions.ts index ed87afc8..bed98b7d 100644 --- a/src/features/chat/application/threads/selection-actions.ts +++ b/src/features/chat/application/threads/selection-actions.ts @@ -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; diff --git a/src/features/chat/application/threads/state-selectors.ts b/src/features/chat/application/threads/state-selectors.ts new file mode 100644 index 00000000..6a52fdff --- /dev/null +++ b/src/features/chat/application/threads/state-selectors.ts @@ -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); +} diff --git a/tests/features/chat/conversation/turns/plan-implementation.test.ts b/tests/features/chat/conversation/turns/plan-implementation.test.ts index 2a6a08fa..00df9430 100644 --- a/tests/features/chat/conversation/turns/plan-implementation.test.ts +++ b/tests/features/chat/conversation/turns/plan-implementation.test.ts @@ -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 => ({ diff --git a/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx b/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx index d0156dae..00914f29 100644 --- a/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx +++ b/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx @@ -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";