diff --git a/src/app-server/query/cache.ts b/src/app-server/query/cache.ts index 62fc9d39..ac8edce8 100644 --- a/src/app-server/query/cache.ts +++ b/src/app-server/query/cache.ts @@ -1,9 +1,9 @@ import { QueryClient, QueryObserver, type QueryObserverResult } from "@tanstack/query-core"; import type { ModelMetadata } from "../../domain/catalog/metadata"; -import type { ObservedResult, ObservedResultListener } from "../../domain/observed-result"; import { createServerDiagnostics, diagnosticProbeError, diagnosticProbeOk, diagnosticsWithProbe } from "../../domain/server/diagnostics"; import type { SharedServerMetadata } from "../../domain/server/metadata"; import type { Thread } from "../../domain/threads/model"; +import type { ObservedResult, ObservedResultListener } from "../../shared/query/observed-result"; import type { AppServerClient } from "../connection/client"; import type { AppServerClientAccessOptions } from "../connection/client-access"; import { runtimeConfigSnapshotFromAppServerConfig } from "../protocol/runtime-config"; diff --git a/src/app-server/query/shared-queries.ts b/src/app-server/query/shared-queries.ts index 5e283ed9..68382a45 100644 --- a/src/app-server/query/shared-queries.ts +++ b/src/app-server/query/shared-queries.ts @@ -1,7 +1,7 @@ import type { ModelMetadata } from "../../domain/catalog/metadata"; -import type { ObservedResultListener } from "../../domain/observed-result"; import type { SharedServerMetadata } from "../../domain/server/metadata"; import type { Thread } from "../../domain/threads/model"; +import type { ObservedResultListener } from "../../shared/query/observed-result"; import type { AppServerQueryCache } from "./cache"; import { type AppServerQueryContext, diff --git a/src/app-server/query/thread-catalog.ts b/src/app-server/query/thread-catalog.ts index f4ac08b4..f468c28e 100644 --- a/src/app-server/query/thread-catalog.ts +++ b/src/app-server/query/thread-catalog.ts @@ -1,5 +1,5 @@ -import type { ObservedResultListener } from "../../domain/observed-result"; import type { Thread } from "../../domain/threads/model"; +import type { ObservedResultListener } from "../../shared/query/observed-result"; type ThreadListObserver = ObservedResultListener; diff --git a/src/features/chat/app-server/client-scope.ts b/src/features/chat/app-server/connection/client-scope.ts similarity index 93% rename from src/features/chat/app-server/client-scope.ts rename to src/features/chat/app-server/connection/client-scope.ts index ab60692a..7f127a53 100644 --- a/src/features/chat/app-server/client-scope.ts +++ b/src/features/chat/app-server/connection/client-scope.ts @@ -1,4 +1,4 @@ -import type { AppServerClient } from "../../../app-server/connection/client"; +import type { AppServerClient } from "../../../../app-server/connection/client"; export interface CurrentChatAppServerClientHost { currentClient(): AppServerClient | null; diff --git a/src/features/chat/app-server/goals/transport.ts b/src/features/chat/app-server/goals/transport.ts index ec4182c6..1919f5bc 100644 --- a/src/features/chat/app-server/goals/transport.ts +++ b/src/features/chat/app-server/goals/transport.ts @@ -1,7 +1,7 @@ import { readThreadGoal, recordThreadGoalUserMessage, setThreadGoal } from "../../../../app-server/services/threads"; import type { ThreadGoalReadTransport, ThreadGoalTransport } from "../../application/threads/goal-transport"; -import type { ConnectedChatAppServerClientHost, CurrentChatAppServerClientHost } from "../client-scope"; -import { chatAppServerClientIsStale, withConnectedChatAppServerClient, withCurrentChatAppServerClient } from "../client-scope"; +import type { ConnectedChatAppServerClientHost, CurrentChatAppServerClientHost } from "../connection/client-scope"; +import { chatAppServerClientIsStale, withConnectedChatAppServerClient, withCurrentChatAppServerClient } from "../connection/client-scope"; export function createChatThreadGoalReadTransport(host: CurrentChatAppServerClientHost): ThreadGoalReadTransport { return { diff --git a/src/features/chat/app-server/mappers/message-stream/agent-items.ts b/src/features/chat/app-server/mappers/message-stream/agent-items.ts index b33e14cb..b3040f01 100644 --- a/src/features/chat/app-server/mappers/message-stream/agent-items.ts +++ b/src/features/chat/app-server/mappers/message-stream/agent-items.ts @@ -1,10 +1,10 @@ +import type { AgentMessageStreamItem, AgentStateSummary, ExecutionState } from "../../../domain/message-stream/items"; import { collabAgentStateExecutionState, type ExecutionStateByStatus, executionStateFromStatus, RUNNING_EXECUTION_STATE, -} from "../../../domain/message-stream/execution-state"; -import type { AgentMessageStreamItem, AgentStateSummary, ExecutionState } from "../../../domain/message-stream/items"; +} from "./execution-state"; const STANDARD_TOOL_STATES: ExecutionStateByStatus = { inProgress: RUNNING_EXECUTION_STATE, @@ -51,19 +51,23 @@ export function agentMessageStreamItem(item: MessageStreamCollabAgentToolCall, t function agentStatesDisplay(states: MessageStreamCollabAgentToolCall["agentsStates"]): AgentStateSummary[] { return Object.entries(states) - .map(([threadId, state]) => ({ - threadId, - status: state?.status ?? "unknown", - message: state?.message ?? null, - })) + .map(([threadId, state]) => { + const status = state?.status ?? "unknown"; + return { + threadId, + status, + executionState: collabAgentStateExecutionState(status), + message: state?.message ?? null, + }; + }) .sort((a, b) => a.threadId.localeCompare(b.threadId)); } function collabAgentExecutionState(tool: string, status: string, receiverThreadIds: string[], agents: AgentStateSummary[]): ExecutionState { if (tool === "spawnAgent") return collabAgentToolCallExecutionState(status); - if (agents.some((agent) => collabAgentStateExecutionState(agent.status) === "failed")) return "failed"; - if (agents.some((agent) => collabAgentStateExecutionState(agent.status) === "running")) return "running"; - if (agents.length > 0 && agents.every((agent) => collabAgentStateExecutionState(agent.status) === "completed")) { + if (agents.some((agent) => agent.executionState === "failed")) return "failed"; + if (agents.some((agent) => agent.executionState === "running")) return "running"; + if (agents.length > 0 && agents.every((agent) => agent.executionState === "completed")) { return "completed"; } if (receiverThreadIds.length > 0 && collabAgentToolCallExecutionState(status) === "completed") return "running"; diff --git a/src/features/chat/app-server/mappers/message-stream/execution-state.ts b/src/features/chat/app-server/mappers/message-stream/execution-state.ts new file mode 100644 index 00000000..ebe093ff --- /dev/null +++ b/src/features/chat/app-server/mappers/message-stream/execution-state.ts @@ -0,0 +1,83 @@ +import { type MessageStreamExecutionState, RUNNING_EXECUTION_STATE } from "../../../domain/message-stream/execution-state"; +import type { ExecutionState } from "../../../domain/message-stream/items"; + +export { RUNNING_EXECUTION_STATE }; + +export type ExecutionStateByStatus = Readonly>; + +const COMMAND_STATES = { + inProgress: RUNNING_EXECUTION_STATE, + completed: "completed", + failed: "failed", + declined: "failed", +} as const satisfies ExecutionStateByStatus; + +const PATCH_STATES = { + inProgress: RUNNING_EXECUTION_STATE, + completed: "completed", + failed: "failed", + declined: "failed", +} as const satisfies ExecutionStateByStatus; + +const STANDARD_TOOL_STATES = { + inProgress: RUNNING_EXECUTION_STATE, + completed: "completed", + failed: "failed", +} as const satisfies ExecutionStateByStatus; + +const COLLAB_AGENT_STATES = { + pendingInit: RUNNING_EXECUTION_STATE, + running: RUNNING_EXECUTION_STATE, + inProgress: RUNNING_EXECUTION_STATE, + completed: "completed", + shutdown: "completed", + interrupted: "failed", + errored: "failed", + notFound: "failed", + failed: "failed", +} as const satisfies ExecutionStateByStatus; + +export function commandExecutionState(status: string, exitCode?: number): ExecutionState { + if (typeof exitCode === "number" && exitCode !== 0) return "failed"; + const state = executionStateFromStatus(status, COMMAND_STATES); + if (state) return state; + if (typeof exitCode === "number") return "completed"; + return null; +} + +export function patchApplyExecutionState(status: string): ExecutionState { + return executionStateFromStatus(status, PATCH_STATES); +} + +export function mcpToolCallExecutionState(status: string): ExecutionState { + return standardToolCallExecutionState(status); +} + +export function dynamicToolCallExecutionState(status: string, success?: boolean | null): ExecutionState { + if (success === false) return "failed"; + const state = standardToolCallExecutionState(status); + if (state) return state; + return success === true ? "completed" : null; +} + +export function imageGenerationExecutionState(status: string): ExecutionState { + return standardToolCallExecutionState(status); +} + +export function collabAgentStateExecutionState(status: string): ExecutionState { + return executionStateFromStatus(status, COLLAB_AGENT_STATES); +} + +export function appServerFailedStatusLabel(status: unknown): string | null { + if (status === "failed") return "failed"; + if (status === "declined") return "declined"; + return null; +} + +function standardToolCallExecutionState(status: string): ExecutionState { + return executionStateFromStatus(status, STANDARD_TOOL_STATES); +} + +export function executionStateFromStatus(status: string, states: ExecutionStateByStatus): ExecutionState { + return states[status] ?? null; +} diff --git a/src/features/chat/app-server/mappers/message-stream/file-changes.ts b/src/features/chat/app-server/mappers/message-stream/file-changes.ts index 2ec217af..b199991d 100644 --- a/src/features/chat/app-server/mappers/message-stream/file-changes.ts +++ b/src/features/chat/app-server/mappers/message-stream/file-changes.ts @@ -1,5 +1,5 @@ -import { patchApplyExecutionState } from "../../../domain/message-stream/execution-state"; import type { MessageStreamFileChange, MessageStreamItem } from "../../../domain/message-stream/items"; +import { patchApplyExecutionState } from "./execution-state"; export interface AppServerFileChange { readonly path: string; diff --git a/src/features/chat/app-server/mappers/message-stream/hook-run-items.ts b/src/features/chat/app-server/mappers/message-stream/hook-run-items.ts index 9cfc8afa..6d2057fd 100644 --- a/src/features/chat/app-server/mappers/message-stream/hook-run-items.ts +++ b/src/features/chat/app-server/mappers/message-stream/hook-run-items.ts @@ -1,9 +1,5 @@ -import { - type ExecutionStateByStatus, - executionStateFromStatus, - RUNNING_EXECUTION_STATE, -} from "../../../domain/message-stream/execution-state"; import type { HookMessageStreamItem } from "../../../domain/message-stream/items"; +import { type ExecutionStateByStatus, executionStateFromStatus, RUNNING_EXECUTION_STATE } from "./execution-state"; interface MessageStreamHookRun { id: string; diff --git a/src/features/chat/app-server/mappers/message-stream/review-result-items.ts b/src/features/chat/app-server/mappers/message-stream/review-result-items.ts index 12908493..7737ece9 100644 --- a/src/features/chat/app-server/mappers/message-stream/review-result-items.ts +++ b/src/features/chat/app-server/mappers/message-stream/review-result-items.ts @@ -1,11 +1,7 @@ import { pathRelativeToRoot } from "../../../../../shared/path/file-paths"; -import { - type ExecutionStateByStatus, - executionStateFromStatus, - RUNNING_EXECUTION_STATE, -} from "../../../domain/message-stream/execution-state"; import { permissionRows } from "../../../domain/message-stream/format/permission-rows"; import type { ExecutionState, MessageStreamAuditFact, MessageStreamItem } from "../../../domain/message-stream/items"; +import { type ExecutionStateByStatus, executionStateFromStatus, RUNNING_EXECUTION_STATE } from "./execution-state"; const AUTO_REVIEW_STATES: ExecutionStateByStatus = { inProgress: RUNNING_EXECUTION_STATE, diff --git a/src/features/chat/app-server/mappers/message-stream/task-progress.ts b/src/features/chat/app-server/mappers/message-stream/task-progress.ts index 42609797..8e8a6bd5 100644 --- a/src/features/chat/app-server/mappers/message-stream/task-progress.ts +++ b/src/features/chat/app-server/mappers/message-stream/task-progress.ts @@ -1,9 +1,5 @@ -import { - type ExecutionStateByStatus, - executionStateFromStatus, - RUNNING_EXECUTION_STATE, -} from "../../../domain/message-stream/execution-state"; import type { MessageStreamItem } from "../../../domain/message-stream/items"; +import { type ExecutionStateByStatus, executionStateFromStatus, RUNNING_EXECUTION_STATE } from "./execution-state"; const TASK_STATES = { pending: RUNNING_EXECUTION_STATE, diff --git a/src/features/chat/app-server/mappers/message-stream/turn-items.ts b/src/features/chat/app-server/mappers/message-stream/turn-items.ts index fc6cf327..0880b974 100644 --- a/src/features/chat/app-server/mappers/message-stream/turn-items.ts +++ b/src/features/chat/app-server/mappers/message-stream/turn-items.ts @@ -8,20 +8,20 @@ import type { HistoricalTurn } from "../../../../../domain/threads/history"; import { referencedThreadMetadataFromPrompt } from "../../../../../domain/threads/reference"; import type { ThreadConversationSummary } from "../../../../../domain/threads/transcript"; import { jsonPreview } from "../../../../../shared/text/preview"; -import { - commandExecutionState, - dynamicToolCallExecutionState, - failedStatusLabel, - imageGenerationExecutionState, - mcpToolCallExecutionState, - patchApplyExecutionState, -} from "../../../domain/message-stream/execution-state"; import { fileMentionsFromInput } from "../../../domain/message-stream/format/file-mentions"; import { normalizeProposedPlanMarkdown } from "../../../domain/message-stream/format/proposed-plan"; import { userMessageDisplayText } from "../../../domain/message-stream/format/user-message-text"; import type { CommandMessageStreamTarget, MessageStreamDiagnosticSection, MessageStreamItem } from "../../../domain/message-stream/items"; import type { MessageStreamItemProvenance } from "../../../domain/message-stream/provenance"; import { agentMessageStreamItem } from "./agent-items"; +import { + appServerFailedStatusLabel, + commandExecutionState, + dynamicToolCallExecutionState, + imageGenerationExecutionState, + mcpToolCallExecutionState, + patchApplyExecutionState, +} from "./execution-state"; import { normalizeFileChanges } from "./file-changes"; type UserMessageItem = Extract; @@ -228,7 +228,7 @@ function mcpToolCallMessageStreamItem(item: McpToolCallItem, turnId?: string): M function dynamicToolCallMessageStreamItem(item: DynamicToolCallItem, turnId?: string): MessageStreamItem { const name = `${item.namespace ? `${item.namespace}.` : ""}${item.tool}`; const target = jsonTargetLabel(item.arguments); - const failure = item.success === false ? "failed" : failedStatusLabel(item.status); + const failure = item.success === false ? "failed" : appServerFailedStatusLabel(item.status); return { ...turnItemSourceFields(item, turnId), kind: "tool", @@ -282,7 +282,7 @@ function sleepMessageStreamItem(item: SleepItem, turnId?: string): MessageStream function imageGenerationMessageStreamItem(item: ImageGenerationItem, turnId?: string): MessageStreamItem { const target = item.savedPath ?? item.result; - const failureReason = failedStatusLabel(item.status); + const failureReason = appServerFailedStatusLabel(item.status); return { ...turnItemSourceFields(item, turnId), kind: "tool", diff --git a/src/features/chat/app-server/runtime/thread-settings-transport.ts b/src/features/chat/app-server/runtime/thread-settings-transport.ts index 52f8d3c4..5f7a3ec2 100644 --- a/src/features/chat/app-server/runtime/thread-settings-transport.ts +++ b/src/features/chat/app-server/runtime/thread-settings-transport.ts @@ -1,7 +1,7 @@ import type { RuntimeSettingsPatch } from "../../../../domain/runtime/thread-settings"; import type { RuntimeSettingsTransport } from "../../application/runtime/settings-transport"; -import type { CurrentChatAppServerClientHost } from "../client-scope"; -import { withCurrentChatAppServerClient } from "../client-scope"; +import type { CurrentChatAppServerClientHost } from "../connection/client-scope"; +import { withCurrentChatAppServerClient } from "../connection/client-scope"; export function createChatRuntimeSettingsTransport(host: CurrentChatAppServerClientHost): RuntimeSettingsTransport { return { diff --git a/src/features/chat/app-server/threads/loading-transport.ts b/src/features/chat/app-server/threads/loading-transport.ts index c9929fb4..155dcab0 100644 --- a/src/features/chat/app-server/threads/loading-transport.ts +++ b/src/features/chat/app-server/threads/loading-transport.ts @@ -4,8 +4,8 @@ import type { ThreadResumeSnapshot, ThreadResumeTransport, } from "../../application/threads/thread-loading-transport"; -import type { ConnectedChatAppServerClientHost, CurrentChatAppServerClientHost } from "../client-scope"; -import { withConnectedChatAppServerClient, withCurrentChatAppServerClient } from "../client-scope"; +import type { ConnectedChatAppServerClientHost, CurrentChatAppServerClientHost } from "../connection/client-scope"; +import { withConnectedChatAppServerClient, withCurrentChatAppServerClient } from "../connection/client-scope"; import { readChatThreadHistoryPage, resumeChatThread } from "./projection"; interface ChatThreadResumeTransportHost extends ConnectedChatAppServerClientHost { diff --git a/src/features/chat/app-server/threads/transport.ts b/src/features/chat/app-server/threads/transport.ts index dbb1b3c9..5d34f65d 100644 --- a/src/features/chat/app-server/threads/transport.ts +++ b/src/features/chat/app-server/threads/transport.ts @@ -1,7 +1,7 @@ import { compactThread, forkThread, rollbackThread } from "../../../../app-server/services/threads"; import type { ThreadMutationTransport, ThreadRollbackSnapshot } from "../../application/threads/thread-mutation-transport"; -import type { ConnectedChatAppServerClientHost } from "../client-scope"; -import { withConnectedChatAppServerClient } from "../client-scope"; +import type { ConnectedChatAppServerClientHost } from "../connection/client-scope"; +import { withConnectedChatAppServerClient } from "../connection/client-scope"; import { messageStreamItemsFromTurns } from "../mappers/message-stream/turn-items"; interface ChatThreadMutationTransportHost extends ConnectedChatAppServerClientHost { diff --git a/src/features/chat/app-server/turns/transport.ts b/src/features/chat/app-server/turns/transport.ts index 2dcf38d0..3de61457 100644 --- a/src/features/chat/app-server/turns/transport.ts +++ b/src/features/chat/app-server/turns/transport.ts @@ -1,6 +1,6 @@ import type { ChatTurnTransport } from "../../application/conversation/turn-transport"; -import type { ConnectedChatAppServerClientHost } from "../client-scope"; -import { withCurrentChatAppServerClient } from "../client-scope"; +import type { ConnectedChatAppServerClientHost } from "../connection/client-scope"; +import { withCurrentChatAppServerClient } from "../connection/client-scope"; interface ChatTurnTransportHost extends ConnectedChatAppServerClientHost { vaultPath: string; diff --git a/src/features/chat/application/conversation/optimistic-turn-start.ts b/src/features/chat/application/conversation/optimistic-turn-start.ts index 44cf16e8..d67d3545 100644 --- a/src/features/chat/application/conversation/optimistic-turn-start.ts +++ b/src/features/chat/application/conversation/optimistic-turn-start.ts @@ -1,8 +1,8 @@ import type { CodexInput } from "../../../../domain/chat/input"; -import { isLocalSteerMessageClientId } from "../../domain/local-message-ids"; import { fileMentionsFromInput } from "../../domain/message-stream/format/file-mentions"; import { userMessageDisplayText } from "../../domain/message-stream/format/user-message-text"; import type { MessageStreamFileMention, MessageStreamItem, MessageStreamMessageItem } from "../../domain/message-stream/items"; +import { isLocalSteerMessageClientId } from "../../domain/message-stream/local-message-ids"; import type { MessageStreamItemProvenance } from "../../domain/message-stream/provenance"; import { attachHookRunsToTurn } from "../../domain/message-stream/updates"; import type { PendingTurnStart } from "./turn-state"; diff --git a/src/features/chat/application/threads/lifecycle-parts.ts b/src/features/chat/application/threads/lifecycle-parts.ts index dd691c13..08c7d12a 100644 --- a/src/features/chat/application/threads/lifecycle-parts.ts +++ b/src/features/chat/application/threads/lifecycle-parts.ts @@ -1,11 +1,11 @@ import type { ThreadTokenUsage } from "../../../../domain/runtime/metrics"; -import type { ChatResumeWorkTracker } from "../lifecycle"; import type { ChatStateStore } from "../state/store"; import { createActiveThreadIdentitySync } from "./active-thread-identity-sync"; import type { GoalActions } from "./goal-actions"; import type { HistoryController } from "./history-controller"; import { RestorationController } from "./restoration-controller"; import { createResumeActions, type ResumeActions } from "./resume-actions"; +import type { ChatResumeWorkTracker } from "./resume-work"; import type { ThreadResumeTransport } from "./thread-loading-transport"; export interface ThreadLifecyclePartsContext { diff --git a/src/features/chat/application/threads/restoration-controller.ts b/src/features/chat/application/threads/restoration-controller.ts index a2c4a683..dce34a04 100644 --- a/src/features/chat/application/threads/restoration-controller.ts +++ b/src/features/chat/application/threads/restoration-controller.ts @@ -3,7 +3,7 @@ import { type RestoredThreadPlaceholderState, type RestoredThreadState, transitionRestoredThreadLifecycle, -} from "../lifecycle"; +} from "./restored-thread-lifecycle"; const STATUS_THREAD_READY_TO_RESUME = "Thread ready to resume."; diff --git a/src/features/chat/application/lifecycle.ts b/src/features/chat/application/threads/restored-thread-lifecycle.ts similarity index 54% rename from src/features/chat/application/lifecycle.ts rename to src/features/chat/application/threads/restored-thread-lifecycle.ts index e11a0e4d..8217a505 100644 --- a/src/features/chat/application/lifecycle.ts +++ b/src/features/chat/application/threads/restored-thread-lifecycle.ts @@ -4,10 +4,6 @@ export interface RestoredThreadState { explicitName: string | null; } -export type ChatResumeLifecycleState = { kind: "idle" } | { kind: "resuming"; threadId: string }; -export type ActiveChatResume = Extract; -type ChatResumeLifecycleEvent = { type: "started"; resume: ActiveChatResume } | { type: "invalidated" }; - export type RestoredThreadLifecycleState = | { kind: "idle" } | { kind: "placeholder"; threadId: string; title: string | null; explicitName: string | null; loading: Promise | null }; @@ -19,41 +15,6 @@ export type RestoredThreadLifecycleEvent = | { type: "loading-finished"; loading: Promise } | { type: "cleared" }; -export interface ChatViewDeferredTasks { - scheduleDiagnostics(callback: () => void): void; - clearDiagnostics(): void; - scheduleAppServerWarmup(callback: () => void): void; - clearAppServerWarmup(): void; - clearAll(): void; -} - -export class ChatResumeWorkTracker { - private state: ChatResumeLifecycleState = { kind: "idle" }; - - begin(threadId: string): ActiveChatResume { - const resume: ActiveChatResume = { kind: "resuming", threadId }; - this.state = transitionChatResumeLifecycle(this.state, { type: "started", resume }); - return resume; - } - - invalidate(): void { - this.state = transitionChatResumeLifecycle(this.state, { type: "invalidated" }); - } - - isStale(resume: ActiveChatResume): boolean { - return this.state !== resume; - } -} - -function transitionChatResumeLifecycle(state: ChatResumeLifecycleState, event: ChatResumeLifecycleEvent): ChatResumeLifecycleState { - switch (event.type) { - case "started": - return event.resume; - case "invalidated": - return state.kind === "idle" ? state : { kind: "idle" }; - } -} - export function transitionRestoredThreadLifecycle( state: RestoredThreadLifecycleState, event: RestoredThreadLifecycleEvent, diff --git a/src/features/chat/application/threads/resume-actions.ts b/src/features/chat/application/threads/resume-actions.ts index da8d915f..7f1cedfb 100644 --- a/src/features/chat/application/threads/resume-actions.ts +++ b/src/features/chat/application/threads/resume-actions.ts @@ -1,10 +1,10 @@ import type { ThreadTokenUsage } from "../../../../domain/runtime/metrics"; -import type { ActiveChatResume, ChatResumeWorkTracker } from "../lifecycle"; import { resumedThreadAction } from "../state/actions"; import { messageStreamIsEmpty } from "../state/message-stream"; import type { ChatStateStore } from "../state/store"; import type { HistoryController } from "./history-controller"; import type { RestorationController } from "./restoration-controller"; +import type { ActiveChatResume, ChatResumeWorkTracker } from "./resume-work"; import type { ThreadResumeSnapshot, ThreadResumeTransport } from "./thread-loading-transport"; import { canSwitchToThread } from "./thread-switching"; diff --git a/src/features/chat/application/threads/resume-work.ts b/src/features/chat/application/threads/resume-work.ts new file mode 100644 index 00000000..1b9710b9 --- /dev/null +++ b/src/features/chat/application/threads/resume-work.ts @@ -0,0 +1,30 @@ +export type ChatResumeLifecycleState = { kind: "idle" } | { kind: "resuming"; threadId: string }; +export type ActiveChatResume = Extract; +type ChatResumeLifecycleEvent = { type: "started"; resume: ActiveChatResume } | { type: "invalidated" }; + +export class ChatResumeWorkTracker { + private state: ChatResumeLifecycleState = { kind: "idle" }; + + begin(threadId: string): ActiveChatResume { + const resume: ActiveChatResume = { kind: "resuming", threadId }; + this.state = transitionChatResumeLifecycle(this.state, { type: "started", resume }); + return resume; + } + + invalidate(): void { + this.state = transitionChatResumeLifecycle(this.state, { type: "invalidated" }); + } + + isStale(resume: ActiveChatResume): boolean { + return this.state !== resume; + } +} + +function transitionChatResumeLifecycle(state: ChatResumeLifecycleState, event: ChatResumeLifecycleEvent): ChatResumeLifecycleState { + switch (event.type) { + case "started": + return event.resume; + case "invalidated": + return state.kind === "idle" ? state : { kind: "idle" }; + } +} diff --git a/src/features/chat/domain/message-stream/completed-turn-reconciliation.ts b/src/features/chat/domain/message-stream/completed-turn-reconciliation.ts index a35dab82..482971ad 100644 --- a/src/features/chat/domain/message-stream/completed-turn-reconciliation.ts +++ b/src/features/chat/domain/message-stream/completed-turn-reconciliation.ts @@ -1,5 +1,5 @@ -import { isLocalUserMessageId } from "../local-message-ids"; import type { MessageStreamItem, MessageStreamMessageItem } from "./items"; +import { isLocalUserMessageId } from "./local-message-ids"; import { upsertMessageStreamItemById } from "./updates"; export interface CompletedTurnReconciliationInput { diff --git a/src/features/chat/domain/message-stream/execution-state.ts b/src/features/chat/domain/message-stream/execution-state.ts index e9890cd0..66300b52 100644 --- a/src/features/chat/domain/message-stream/execution-state.ts +++ b/src/features/chat/domain/message-stream/execution-state.ts @@ -1,83 +1,4 @@ import type { ExecutionState } from "./items"; export type MessageStreamExecutionState = Exclude; -export type ExecutionStateByStatus = Readonly>; - export const RUNNING_EXECUTION_STATE: MessageStreamExecutionState = "running"; - -const COMMAND_STATES = { - inProgress: RUNNING_EXECUTION_STATE, - completed: "completed", - failed: "failed", - declined: "failed", -} as const satisfies ExecutionStateByStatus; - -const PATCH_STATES = { - inProgress: RUNNING_EXECUTION_STATE, - completed: "completed", - failed: "failed", - declined: "failed", -} as const satisfies ExecutionStateByStatus; - -const STANDARD_TOOL_STATES = { - inProgress: RUNNING_EXECUTION_STATE, - completed: "completed", - failed: "failed", -} as const satisfies ExecutionStateByStatus; - -const COLLAB_AGENT_STATES = { - pendingInit: RUNNING_EXECUTION_STATE, - running: RUNNING_EXECUTION_STATE, - inProgress: RUNNING_EXECUTION_STATE, - completed: "completed", - shutdown: "completed", - interrupted: "failed", - errored: "failed", - notFound: "failed", - failed: "failed", -} as const satisfies ExecutionStateByStatus; - -export function commandExecutionState(status: string, exitCode?: number): ExecutionState { - if (typeof exitCode === "number" && exitCode !== 0) return "failed"; - const state = executionStateFromStatus(status, COMMAND_STATES); - if (state) return state; - if (typeof exitCode === "number") return "completed"; - return null; -} - -export function patchApplyExecutionState(status: string): ExecutionState { - return executionStateFromStatus(status, PATCH_STATES); -} - -export function mcpToolCallExecutionState(status: string): ExecutionState { - return standardToolCallExecutionState(status); -} - -export function dynamicToolCallExecutionState(status: string, success?: boolean | null): ExecutionState { - if (success === false) return "failed"; - const state = standardToolCallExecutionState(status); - if (state) return state; - return success === true ? "completed" : null; -} - -export function imageGenerationExecutionState(status: string): ExecutionState { - return standardToolCallExecutionState(status); -} - -export function collabAgentStateExecutionState(status: string): ExecutionState { - return executionStateFromStatus(status, COLLAB_AGENT_STATES); -} - -export function failedStatusLabel(status: unknown): string | null { - if (status === "failed") return "failed"; - if (status === "declined") return "declined"; - return null; -} - -function standardToolCallExecutionState(status: string): ExecutionState { - return executionStateFromStatus(status, STANDARD_TOOL_STATES); -} - -export function executionStateFromStatus(status: string, states: ExecutionStateByStatus): ExecutionState { - return states[status] ?? null; -} diff --git a/src/features/chat/domain/message-stream/items.ts b/src/features/chat/domain/message-stream/items.ts index 03bc6fb0..609fbbcb 100644 --- a/src/features/chat/domain/message-stream/items.ts +++ b/src/features/chat/domain/message-stream/items.ts @@ -269,6 +269,7 @@ export interface TaskProgressMessageStreamItem extends MessageStreamBase { export interface AgentStateSummary { readonly threadId: string; readonly status: string; + readonly executionState: ExecutionState; readonly message: string | null; } diff --git a/src/features/chat/domain/local-message-ids.ts b/src/features/chat/domain/message-stream/local-message-ids.ts similarity index 100% rename from src/features/chat/domain/local-message-ids.ts rename to src/features/chat/domain/message-stream/local-message-ids.ts diff --git a/src/features/chat/domain/message-stream/semantics/active-turn.ts b/src/features/chat/domain/message-stream/semantics/active-turn.ts index 44ffca9c..7edda3dc 100644 --- a/src/features/chat/domain/message-stream/semantics/active-turn.ts +++ b/src/features/chat/domain/message-stream/semantics/active-turn.ts @@ -1,5 +1,4 @@ import { truncate } from "../../../../../shared/text/preview"; -import { collabAgentStateExecutionState } from "../execution-state"; import type { AgentRunSummary, AgentRunSummaryAgent, @@ -72,7 +71,7 @@ function activeAgentRunSummary(items: readonly MessageStreamItem[], activeTurnId } } else { for (const threadId of item.receiverThreadIds) { - agentStatuses.set(threadId, { threadId, status: item.status, message: null }); + agentStatuses.set(threadId, { threadId, status: item.status, executionState: item.executionState ?? "running", message: null }); } } } @@ -82,14 +81,14 @@ function activeAgentRunSummary(items: readonly MessageStreamItem[], activeTurnId const summary = { running: 0, completed: 0, failed: 0, agents: [] as AgentRunSummaryAgent[], additionalAgents: 0 }; const agents = [...agentStatuses.values()]; for (const agent of agents) { - const state = agentRunState(agent.status); + const state = agentRunState(agent); summary[state] += 1; } if (summary.running === 0 && summary.failed === 0) return null; summary.agents = agents - .filter((agent) => agentRunState(agent.status) === "running") + .filter((agent) => agentRunState(agent) === "running") .sort((a, b) => a.threadId.localeCompare(b.threadId)) .map((agent) => ({ threadId: agent.threadId, @@ -129,6 +128,6 @@ function agentMessagePreview(message: string | null, maxLength: number): string return truncate(firstLine.replace(/\s+/g, " "), maxLength); } -function agentRunState(status: string): AgentRunState { - return collabAgentStateExecutionState(status) ?? "running"; +function agentRunState(agent: AgentStateSummary): AgentRunState { + return agent.executionState ?? "running"; } diff --git a/src/features/chat/domain/message-stream/semantics/classify.ts b/src/features/chat/domain/message-stream/semantics/classify.ts index c9fb5675..17452449 100644 --- a/src/features/chat/domain/message-stream/semantics/classify.ts +++ b/src/features/chat/domain/message-stream/semantics/classify.ts @@ -1,5 +1,5 @@ -import { isLocalSteerMessageClientId } from "../../local-message-ids"; import type { MessageStreamItem } from "../items"; +import { isLocalSteerMessageClientId } from "../local-message-ids"; import type { MessageStreamLifecycle, MessageStreamMeaning, diff --git a/src/features/chat/host/composer-bundle.ts b/src/features/chat/host/composer-bundle.ts index 6caac3a0..8c1fc8a4 100644 --- a/src/features/chat/host/composer-bundle.ts +++ b/src/features/chat/host/composer-bundle.ts @@ -5,10 +5,10 @@ import { resolveRuntimeControls } from "../domain/runtime/resolution"; import { ChatComposerController } from "../panel/composer-controller"; import { type ChatPanelComposerSurface, chatPanelComposerProjection } from "../panel/surface/composer-projection"; import type { ChatMessageScrollController } from "../panel/surface/message-stream-scroll"; -import { VaultNoteCandidateProvider } from "../panel/vault-note-candidate-provider"; import type { ChatPanelEnvironment } from "./contracts"; import type { ChatPanelRuntimeSettingsActions } from "./runtime-bundle"; import type { ChatPanelThreadLifecycle } from "./thread-bundle"; +import { VaultNoteCandidateProvider } from "./vault-note-candidate-provider.obsidian"; interface ChatPanelComposerHost { environment: ChatPanelEnvironment; diff --git a/src/features/chat/host/connection-bundle.ts b/src/features/chat/host/connection-bundle.ts index 909aa473..7e535380 100644 --- a/src/features/chat/host/connection-bundle.ts +++ b/src/features/chat/host/connection-bundle.ts @@ -14,13 +14,13 @@ import { createChatConnectionController, handleChatConnectionExit, } from "../application/connection/connection-controller"; -import type { ChatViewDeferredTasks } from "../application/lifecycle"; import { runtimeSnapshotForChatState } from "../application/runtime/snapshot"; import type { ChatConnectionPhase } from "../application/state/root-reducer"; import type { ChatStateStore } from "../application/state/store"; import type { AutoTitleCoordinator } from "../application/threads/auto-title-coordinator"; import type { createThreadGoalSyncActions } from "../application/threads/goal-actions"; import type { ChatPanelEnvironment } from "./contracts"; +import type { ChatViewDeferredTasks } from "./deferred-work"; export type CurrentAppServerClient = () => AppServerClient | null; diff --git a/src/features/chat/host/contracts.ts b/src/features/chat/host/contracts.ts index 8f775753..aa4547a1 100644 --- a/src/features/chat/host/contracts.ts +++ b/src/features/chat/host/contracts.ts @@ -5,9 +5,9 @@ import type { AppServerQueryContext } from "../../../app-server/query/keys"; import type { ThreadCatalogActiveReader, ThreadCatalogEventSink } from "../../../app-server/query/thread-catalog"; import type { ArchiveExportDestination } from "../../../app-server/services/thread-archive-markdown"; import type { ModelMetadata } from "../../../domain/catalog/metadata"; -import type { ObservedResultListener } from "../../../domain/observed-result"; import type { SharedServerMetadata } from "../../../domain/server/metadata"; import type { CodexPanelSettings } from "../../../settings/model"; +import type { ObservedResultListener } from "../../../shared/query/observed-result"; import type { ChatTurnDiffViewState } from "../domain/turn-diff"; import type { ChatPanelSnapshot } from "../panel/snapshot"; diff --git a/src/features/chat/host/deferred-work.ts b/src/features/chat/host/deferred-work.ts index 48784b94..74d8cde0 100644 --- a/src/features/chat/host/deferred-work.ts +++ b/src/features/chat/host/deferred-work.ts @@ -1,5 +1,12 @@ import { DeferredTask, type DeferredTaskWindow } from "../../../shared/lifecycle/deferred-task"; -import type { ChatViewDeferredTasks } from "../application/lifecycle"; + +export interface ChatViewDeferredTasks { + scheduleDiagnostics(callback: () => void): void; + clearDiagnostics(): void; + scheduleAppServerWarmup(callback: () => void): void; + clearAppServerWarmup(): void; + clearAll(): void; +} export function createChatViewDeferredTasks(getWindow: () => DeferredTaskWindow): ChatViewDeferredTasks { const diagnosticsTask = new DeferredTask(getWindow, 1_000); diff --git a/src/features/chat/host/session-graph.ts b/src/features/chat/host/session-graph.ts index f8965c37..fbd455b4 100644 --- a/src/features/chat/host/session-graph.ts +++ b/src/features/chat/host/session-graph.ts @@ -2,12 +2,12 @@ import { ConnectionManager } from "../../../app-server/connection/connection-man import { isStaleAppServerSharedQueryContextError } from "../../../app-server/query/shared-queries"; import { createLocalIdSource, type LocalIdSource } from "../../../shared/id/local-id"; import type { ConnectionWorkTracker } from "../../../shared/lifecycle/connection-work"; -import type { ChatResumeWorkTracker, ChatViewDeferredTasks } from "../application/lifecycle"; import type { ChatAction, ChatConnectionPhase } from "../application/state/root-reducer"; import type { ChatStateStore } from "../application/state/store"; import type { ActiveThreadIdentitySync } from "../application/threads/active-thread-identity-sync"; import type { RestorationController } from "../application/threads/restoration-controller"; import type { ResumeActions } from "../application/threads/resume-actions"; +import type { ChatResumeWorkTracker } from "../application/threads/resume-work"; import { createStructuredSystemItem, createSystemItem } from "../domain/message-stream/factories/system-items"; import type { MessageStreamNoticeSection } from "../domain/message-stream/items"; import type { ChatComposerController } from "../panel/composer-controller"; @@ -15,6 +15,7 @@ import type { ChatMessageScrollController } from "../panel/surface/message-strea import { createComposerBundle } from "./composer-bundle"; import { type ChatPanelConnectionBundle, createConnectionBundle } from "./connection-bundle"; import type { ChatPanelEnvironment } from "./contracts"; +import type { ChatViewDeferredTasks } from "./deferred-work"; import { createRuntimeBundle } from "./runtime-bundle"; import { type ChatPanelSharedStateBinding, createChatPanelSharedStateBinding } from "./shared-state-binding"; import { type ChatPanelShellBundle, createShellBundle } from "./shell-bundle"; diff --git a/src/features/chat/host/session.ts b/src/features/chat/host/session.ts index 7c6079c7..7fc2242d 100644 --- a/src/features/chat/host/session.ts +++ b/src/features/chat/host/session.ts @@ -3,14 +3,15 @@ import { type AppServerQueryContext, appServerQueryContextMatches, appServerQuer import { pendingRequestCountsFromQueues } from "../../../domain/pending-requests/aggregate"; import { threadMeaningfulTitle, threadWindowTitle } from "../../../domain/threads/title"; import { ConnectionWorkTracker } from "../../../shared/lifecycle/connection-work"; -import { ChatResumeWorkTracker, type ChatViewDeferredTasks, type RestoredThreadPlaceholderState } from "../application/lifecycle"; import type { ChatState } from "../application/state/root-reducer"; import { type ChatStateStore, createChatStateStore } from "../application/state/store"; +import type { RestoredThreadPlaceholderState } from "../application/threads/restored-thread-lifecycle"; +import { ChatResumeWorkTracker } from "../application/threads/resume-work"; import { renderChatPanelShell, unmountChatPanelShell } from "../panel/shell.dom"; import { type ChatPanelSnapshot, openPanelTurnLifecycle, parseRestoredThreadState } from "../panel/snapshot"; import { type ChatMessageScrollController, createChatMessageScrollController } from "../panel/surface/message-stream-scroll"; import type { ChatPanelEnvironment, ChatPanelHandle } from "./contracts"; -import { createChatViewDeferredTasks } from "./deferred-work"; +import { type ChatViewDeferredTasks, createChatViewDeferredTasks } from "./deferred-work"; import { type ChatPanelSessionGraph, createChatPanelSessionGraph } from "./session-graph"; export class ChatPanelSession implements ChatPanelHandle { diff --git a/src/features/chat/host/shared-state-binding.ts b/src/features/chat/host/shared-state-binding.ts index 4ef4badb..b8d6dcfc 100644 --- a/src/features/chat/host/shared-state-binding.ts +++ b/src/features/chat/host/shared-state-binding.ts @@ -1,8 +1,8 @@ import type { ModelMetadata } from "../../../domain/catalog/metadata"; -import type { ObservedResult } from "../../../domain/observed-result"; -import { observedValue } from "../../../domain/observed-result"; import type { SharedServerMetadata } from "../../../domain/server/metadata"; import type { Thread } from "../../../domain/threads/model"; +import type { ObservedResult } from "../../../shared/query/observed-result"; +import { observedValue } from "../../../shared/query/observed-result"; import type { ChatStateStore } from "../application/state/store"; import type { ChatPanelConnectionBundle } from "./connection-bundle"; diff --git a/src/features/chat/host/thread-bundle.ts b/src/features/chat/host/thread-bundle.ts index 840ec3be..0898ae38 100644 --- a/src/features/chat/host/thread-bundle.ts +++ b/src/features/chat/host/thread-bundle.ts @@ -10,7 +10,6 @@ import type { ChatServerThreadActions } from "../app-server/actions/threads"; import { createChatThreadGoalReadTransport, createChatThreadGoalTransport } from "../app-server/goals/transport"; import { createChatThreadHistoryTransport, createChatThreadResumeTransport } from "../app-server/threads/loading-transport"; import { createChatThreadMutationTransport } from "../app-server/threads/transport"; -import type { ChatResumeWorkTracker } from "../application/lifecycle"; import { messageStreamItems } from "../application/state/message-stream"; import type { ChatStateStore } from "../application/state/store"; import type { ActiveThreadIdentitySync } from "../application/threads/active-thread-identity-sync"; @@ -25,6 +24,7 @@ import { } from "../application/threads/rename-editor-actions"; import type { RestorationController } from "../application/threads/restoration-controller"; import type { ResumeActions } from "../application/threads/resume-actions"; +import type { ChatResumeWorkTracker } from "../application/threads/resume-work"; import { createThreadManagementActions, type ThreadManagementActionsHost } from "../application/threads/thread-management-actions"; import { createThreadNavigationActions } from "../application/threads/thread-navigation-actions"; import { threadTitleContextFromMessageStreamItems } from "../application/threads/title-context"; diff --git a/src/features/chat/host/turn-bundle.ts b/src/features/chat/host/turn-bundle.ts index 04800f07..3b0b2c63 100644 --- a/src/features/chat/host/turn-bundle.ts +++ b/src/features/chat/host/turn-bundle.ts @@ -10,7 +10,6 @@ import { type ConversationTurnActions as ChatPanelConversationTurnActions, createConversationTurnActions, } from "../application/conversation/composition"; -import type { ChatViewDeferredTasks } from "../application/lifecycle"; import { createPendingRequestActions, type PendingRequestActions } from "../application/pending-requests/pending-request-actions"; import type { ChatConnectionPhase } from "../application/state/root-reducer"; import type { ChatStateStore } from "../application/state/store"; @@ -19,6 +18,7 @@ import type { MessageStreamNoticeSection } from "../domain/message-stream/items" import type { ChatComposerController } from "../panel/composer-controller"; import type { CurrentAppServerClient } from "./connection-bundle"; import type { ChatPanelEnvironment } from "./contracts"; +import type { ChatViewDeferredTasks } from "./deferred-work"; import type { ChatPanelRuntimeProjection, ChatPanelRuntimeSettingsActions } from "./runtime-bundle"; import type { ChatPanelGoalActions, diff --git a/src/features/chat/panel/vault-note-candidate-provider.ts b/src/features/chat/host/vault-note-candidate-provider.obsidian.ts similarity index 100% rename from src/features/chat/panel/vault-note-candidate-provider.ts rename to src/features/chat/host/vault-note-candidate-provider.obsidian.ts diff --git a/src/features/chat/panel/snapshot.ts b/src/features/chat/panel/snapshot.ts index e06da864..a0ea7635 100644 --- a/src/features/chat/panel/snapshot.ts +++ b/src/features/chat/panel/snapshot.ts @@ -1,5 +1,5 @@ -import type { RestoredThreadState } from "../application/lifecycle"; import type { ChatState } from "../application/state/root-reducer"; +import type { RestoredThreadState } from "../application/threads/restored-thread-lifecycle"; type OpenCodexPanelTurnLifecycle = { kind: "idle" } | { kind: "starting" } | { kind: "running"; turnId: string }; diff --git a/src/features/chat/presentation/message-stream/detail-view.ts b/src/features/chat/presentation/message-stream/detail-view.ts index c6d5edf7..0105535c 100644 --- a/src/features/chat/presentation/message-stream/detail-view.ts +++ b/src/features/chat/presentation/message-stream/detail-view.ts @@ -1,7 +1,6 @@ import { shortThreadId } from "../../../../shared/id/thread-id"; import { pathRelativeToRoot } from "../../../../shared/path/file-paths"; import { truncate } from "../../../../shared/text/preview"; -import { failedStatusLabel } from "../../domain/message-stream/execution-state"; import type { AgentMessageStreamItem, ApprovalResultMessageStreamItem, @@ -418,6 +417,12 @@ function fileChangeSummary(item: FileChangeMessageStreamItem, changes: (MessageS return compactSummary(null, target, statusQualifier(item.status, failedStatusLabel(item.status))); } +function failedStatusLabel(status: unknown): string | null { + if (status === "failed") return "failed"; + if (status === "declined") return "declined"; + return null; +} + function agentSummaryText(item: AgentMessageStreamItem): string { const target = item.receiverThreadIds.length === 0 ? "" : ` ${item.receiverThreadIds.map(shortThreadId).join(", ")}`; const promptPreview = agentPromptPreview(item.prompt); diff --git a/src/features/threads-view/session.ts b/src/features/threads-view/session.ts index f3c325b5..dec83d31 100644 --- a/src/features/threads-view/session.ts +++ b/src/features/threads-view/session.ts @@ -5,10 +5,10 @@ import { isStaleAppServerSharedQueryContextError } from "../../app-server/query/ import type { ThreadCatalogActiveReader, ThreadCatalogEventSink } from "../../app-server/query/thread-catalog"; import type { ArchiveExportDestination } from "../../app-server/services/thread-archive-markdown"; import type { ReasoningEffort } from "../../domain/catalog/metadata"; -import type { ObservedResult } from "../../domain/observed-result"; -import { observedInitialError, observedInitialLoading, observedValue } from "../../domain/observed-result"; import type { ArchiveExportSettings } from "../../domain/threads/archive-markdown"; import type { Thread } from "../../domain/threads/model"; +import type { ObservedResult } from "../../shared/query/observed-result"; +import { observedInitialError, observedInitialLoading, observedValue } from "../../shared/query/observed-result"; import type { OpenCodexPanelSnapshot } from "../../workspace/panel-coordinator"; import { createThreadOperations, type ThreadOperations } from "../threads/thread-operations"; import { createThreadTitleService, type ThreadTitleService } from "../threads/thread-title-service"; diff --git a/src/settings/dynamic-sections-controller.ts b/src/settings/dynamic-sections-controller.ts index be32a6c1..9ea8c574 100644 --- a/src/settings/dynamic-sections-controller.ts +++ b/src/settings/dynamic-sections-controller.ts @@ -4,10 +4,10 @@ import { type HookCatalog, listHookCatalog, setHookItemEnabled, trustHookItem } import { restoreArchivedThread as restoreArchivedThreadOnAppServer } from "../app-server/services/threads"; import type { HookItem, ModelMetadata, ReasoningEffort } from "../domain/catalog/metadata"; import { findModelMetadataByIdOrName, sortedModelMetadata, supportedEffortsForModelMetadata } from "../domain/catalog/metadata"; -import type { ObservedResult } from "../domain/observed-result"; -import { observedValue } from "../domain/observed-result"; import type { Thread } from "../domain/threads/model"; import { threadArchiveDisplayTitle } from "../domain/threads/title"; +import type { ObservedResult } from "../shared/query/observed-result"; +import { observedValue } from "../shared/query/observed-result"; import type { SettingsDynamicSectionsHost } from "./host"; import { createSettingsDynamicSectionLifecycle, diff --git a/src/settings/host.ts b/src/settings/host.ts index c9ef5d74..371831e4 100644 --- a/src/settings/host.ts +++ b/src/settings/host.ts @@ -1,7 +1,7 @@ import type { AppServerClientAccess } from "../app-server/connection/client-access"; import type { ThreadCatalogArchivedReader, ThreadCatalogEventSink } from "../app-server/query/thread-catalog"; import type { ModelMetadata } from "../domain/catalog/metadata"; -import type { ObservedResultListener } from "../domain/observed-result"; +import type { ObservedResultListener } from "../shared/query/observed-result"; import type { CodexPanelSettings } from "./model"; interface SettingsAppServerQueries { diff --git a/src/domain/observed-result.ts b/src/shared/query/observed-result.ts similarity index 100% rename from src/domain/observed-result.ts rename to src/shared/query/observed-result.ts diff --git a/tests/app-server/shared-queries.test.ts b/tests/app-server/shared-queries.test.ts index d11d3568..fca11fd4 100644 --- a/tests/app-server/shared-queries.test.ts +++ b/tests/app-server/shared-queries.test.ts @@ -3,10 +3,10 @@ import { describe, expect, it, vi } from "vitest"; import type { AppServerQueryCache } from "../../src/app-server/query/cache"; import { AppServerSharedQueries, StaleAppServerSharedQueryContextError } from "../../src/app-server/query/shared-queries"; import type { ModelMetadata } from "../../src/domain/catalog/metadata"; -import type { ObservedResult } from "../../src/domain/observed-result"; import { createServerDiagnostics, diagnosticProbeOk, diagnosticsWithProbe } from "../../src/domain/server/diagnostics"; import type { SharedServerMetadata } from "../../src/domain/server/metadata"; import type { Thread } from "../../src/domain/threads/model"; +import type { ObservedResult } from "../../src/shared/query/observed-result"; import { deferred } from "../support/async"; describe("AppServerSharedQueries", () => { diff --git a/tests/features/chat/app-server/mappers/message-stream.test.ts b/tests/features/chat/app-server/mappers/message-stream.test.ts index 79f88cdc..a6629514 100644 --- a/tests/features/chat/app-server/mappers/message-stream.test.ts +++ b/tests/features/chat/app-server/mappers/message-stream.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import type { TurnItem, TurnRecord } from "../../../../../src/app-server/protocol/turn"; import type { Thread } from "../../../../../src/domain/threads/model"; import { referencedThreadPromptBundle } from "../../../../../src/domain/threads/reference"; +import { collabAgentStateExecutionState } from "../../../../../src/features/chat/app-server/mappers/message-stream/execution-state"; import { hookRunMessageStreamItem } from "../../../../../src/features/chat/app-server/mappers/message-stream/hook-run-items"; import { createAutoReviewResultItem, @@ -12,7 +13,6 @@ import { messageStreamItemFromTurnItem, messageStreamItemsFromTurns, } from "../../../../../src/features/chat/app-server/mappers/message-stream/turn-items"; -import { collabAgentStateExecutionState } from "../../../../../src/features/chat/domain/message-stream/execution-state"; import { permissionRows } from "../../../../../src/features/chat/domain/message-stream/format/permission-rows"; import type { MessageStreamItem } from "../../../../../src/features/chat/domain/message-stream/items"; import { activeTurnLiveItems } from "../../../../../src/features/chat/domain/message-stream/semantics/active-turn"; @@ -258,7 +258,7 @@ describe("turn item conversion preserves app-server semantics", () => { prompt: "Inspect the renderer.", model: "gpt-5.5", reasoningEffort: "high", - agents: [{ threadId: "child-thread", status: "completed", message: "Done" }], + agents: [{ threadId: "child-thread", status: "completed", executionState: "completed", message: "Done" }], executionState: "completed", }); }); @@ -1069,7 +1069,7 @@ describe("display block grouping keeps message stream details subordinate to con prompt: null, model: null, reasoningEffort: null, - agents: [{ threadId: "child", status: "completed", message: null }], + agents: [{ threadId: "child", status: "completed", executionState: "completed", message: null }], }, { id: "a1", @@ -1105,9 +1105,9 @@ describe("display block grouping keeps message stream details subordinate to con model: null, reasoningEffort: null, agents: [ - { threadId: "done", status: "completed", message: null }, - { threadId: "running", status: "running", message: null }, - { threadId: "failed", status: "errored", message: null }, + { threadId: "done", status: "completed", executionState: "completed", message: null }, + { threadId: "running", status: "running", executionState: "running", message: null }, + { threadId: "failed", status: "errored", executionState: "failed", message: null }, ], }, ]; @@ -1153,11 +1153,11 @@ describe("display block grouping keeps message stream details subordinate to con model: null, reasoningEffort: null, agents: [ - { threadId: "a", status: "running", message: "\n Inspecting renderer tests \nmore details" }, - { threadId: "b", status: "failed", message: "Could not reproduce" }, - { threadId: "c", status: "running", message: null }, - { threadId: "d", status: "running", message: "Reviewing details" }, - { threadId: "e", status: "running", message: "Checking scroll behavior" }, + { threadId: "a", status: "running", executionState: "running", message: "\n Inspecting renderer tests \nmore details" }, + { threadId: "b", status: "failed", executionState: "failed", message: "Could not reproduce" }, + { threadId: "c", status: "running", executionState: "running", message: null }, + { threadId: "d", status: "running", executionState: "running", message: "Reviewing details" }, + { threadId: "e", status: "running", executionState: "running", message: "Checking scroll behavior" }, ], }, ]; @@ -1192,7 +1192,7 @@ describe("display block grouping keeps message stream details subordinate to con prompt: null, model: null, reasoningEffort: null, - agents: [{ threadId: "done", status: "completed", message: null }], + agents: [{ threadId: "done", status: "completed", executionState: "completed", message: null }], }, ]; diff --git a/tests/features/chat/application/threads/active-thread-identity-sync.test.ts b/tests/features/chat/application/threads/active-thread-identity-sync.test.ts index 545504b5..9d0f5412 100644 --- a/tests/features/chat/application/threads/active-thread-identity-sync.test.ts +++ b/tests/features/chat/application/threads/active-thread-identity-sync.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it, vi } from "vitest"; import type { Thread } from "../../../../../src/domain/threads/model"; -import type { RestoredThreadPlaceholderState } from "../../../../../src/features/chat/application/lifecycle"; import { createChatState } from "../../../../../src/features/chat/application/state/root-reducer"; import { createChatStateStore } from "../../../../../src/features/chat/application/state/store"; import { createActiveThreadIdentitySync } from "../../../../../src/features/chat/application/threads/active-thread-identity-sync"; import type { RestorationController } from "../../../../../src/features/chat/application/threads/restoration-controller"; +import type { RestoredThreadPlaceholderState } from "../../../../../src/features/chat/application/threads/restored-thread-lifecycle"; function thread(id: string, name: string | null = null): Thread { return { diff --git a/tests/features/chat/application/threads/restored-thread-lifecycle.test.ts b/tests/features/chat/application/threads/restored-thread-lifecycle.test.ts new file mode 100644 index 00000000..ec453421 --- /dev/null +++ b/tests/features/chat/application/threads/restored-thread-lifecycle.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; + +import { transitionRestoredThreadLifecycle } from "../../../../../src/features/chat/application/threads/restored-thread-lifecycle"; + +describe("transitionRestoredThreadLifecycle", () => { + it("clears restored-thread loading only for the active loading promise", () => { + const firstLoading = Promise.resolve(); + const secondLoading = Promise.resolve(); + const placeholder = transitionRestoredThreadLifecycle( + { kind: "idle" }, + { type: "placeholder-restored", restoredThread: { threadId: "thread", title: "Old", explicitName: null } }, + ); + const loading = transitionRestoredThreadLifecycle(placeholder, { type: "loading-started", loading: secondLoading }); + + expect(transitionRestoredThreadLifecycle(loading, { type: "loading-finished", loading: firstLoading })).toBe(loading); + expect(transitionRestoredThreadLifecycle(loading, { type: "renamed", threadId: "thread", name: "New" })).toMatchObject({ + title: "New", + explicitName: "New", + loading: secondLoading, + }); + expect(transitionRestoredThreadLifecycle(loading, { type: "loading-finished", loading: secondLoading })).toMatchObject({ + kind: "placeholder", + loading: null, + }); + }); +}); diff --git a/tests/features/chat/application/threads/resume-actions.test.ts b/tests/features/chat/application/threads/resume-actions.test.ts index 84178d4f..26db5c8b 100644 --- a/tests/features/chat/application/threads/resume-actions.test.ts +++ b/tests/features/chat/application/threads/resume-actions.test.ts @@ -2,12 +2,12 @@ import { describe, expect, it, vi } from "vitest"; import type { ThreadTokenUsage } from "../../../../../src/domain/runtime/metrics"; import type { Thread as PanelThread } from "../../../../../src/domain/threads/model"; -import { ChatResumeWorkTracker } from "../../../../../src/features/chat/application/lifecycle"; import { createChatState } from "../../../../../src/features/chat/application/state/root-reducer"; import { createChatStateStore } from "../../../../../src/features/chat/application/state/store"; import type { HistoryController } from "../../../../../src/features/chat/application/threads/history-controller"; import type { RestorationController } from "../../../../../src/features/chat/application/threads/restoration-controller"; import { createResumeActions, type ResumeActionsHost } from "../../../../../src/features/chat/application/threads/resume-actions"; +import { ChatResumeWorkTracker } from "../../../../../src/features/chat/application/threads/resume-work"; import type { ThreadHistoryPage, ThreadResumeSnapshot, diff --git a/tests/features/chat/application/threads/resume-work.test.ts b/tests/features/chat/application/threads/resume-work.test.ts new file mode 100644 index 00000000..0f7e8aef --- /dev/null +++ b/tests/features/chat/application/threads/resume-work.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; + +import { ChatResumeWorkTracker } from "../../../../../src/features/chat/application/threads/resume-work"; + +describe("ChatResumeWorkTracker", () => { + it("tracks resume work by identity", () => { + const tracker = new ChatResumeWorkTracker(); + const resume = tracker.begin("thread"); + + expect(tracker.isStale(resume)).toBe(false); + tracker.invalidate(); + expect(tracker.isStale(resume)).toBe(true); + }); +}); diff --git a/tests/features/chat/host/deferred-work.test.ts b/tests/features/chat/host/deferred-work.test.ts new file mode 100644 index 00000000..e8feb2d3 --- /dev/null +++ b/tests/features/chat/host/deferred-work.test.ts @@ -0,0 +1,26 @@ +// @vitest-environment jsdom + +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { createChatViewDeferredTasks } from "../../../../src/features/chat/host/deferred-work"; + +describe("createChatViewDeferredTasks", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + it("clears scheduled deferred work", async () => { + const tasks = createChatViewDeferredTasks(() => window); + const diagnostics = vi.fn(); + const warmup = vi.fn(); + + tasks.scheduleDiagnostics(diagnostics); + tasks.scheduleAppServerWarmup(warmup); + tasks.clearAll(); + + await vi.advanceTimersByTimeAsync(1_500); + + expect(diagnostics).not.toHaveBeenCalled(); + expect(warmup).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/features/chat/host/lifecycle.test.ts b/tests/features/chat/host/lifecycle.test.ts deleted file mode 100644 index 9690b9ea..00000000 --- a/tests/features/chat/host/lifecycle.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -// @vitest-environment jsdom - -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { ChatResumeWorkTracker, transitionRestoredThreadLifecycle } from "../../../../src/features/chat/application/lifecycle"; -import { createChatViewDeferredTasks } from "../../../../src/features/chat/host/deferred-work"; -import { ConnectionWorkTracker } from "../../../../src/shared/lifecycle/connection-work"; - -describe("createChatViewDeferredTasks", () => { - beforeEach(() => { - vi.useFakeTimers(); - }); - - it("clears scheduled deferred work", async () => { - const tasks = createChatViewDeferredTasks(() => window); - const diagnostics = vi.fn(); - const warmup = vi.fn(); - - tasks.scheduleDiagnostics(diagnostics); - tasks.scheduleAppServerWarmup(warmup); - tasks.clearAll(); - - await vi.advanceTimersByTimeAsync(1_500); - - expect(diagnostics).not.toHaveBeenCalled(); - expect(warmup).not.toHaveBeenCalled(); - }); -}); - -describe("chat view lifecycle transitions", () => { - it("tracks active connection work by identity", () => { - const tracker = new ConnectionWorkTracker(); - const connection = tracker.begin(); - const stale = { kind: "connecting" as const, promise: Promise.resolve() }; - - expect(tracker.active()).toBe(connection); - expect(tracker.isStale(connection)).toBe(false); - expect(tracker.isStale(stale)).toBe(true); - - const promise = Promise.resolve(); - connection.promise = promise; - tracker.finish(connection, Promise.resolve()); - expect(tracker.active()).toBe(connection); - tracker.finish(connection, promise); - expect(tracker.active()).toBeNull(); - }); - - it("tracks resume work by identity", () => { - const tracker = new ChatResumeWorkTracker(); - const resume = tracker.begin("thread"); - - expect(tracker.isStale(resume)).toBe(false); - tracker.invalidate(); - expect(tracker.isStale(resume)).toBe(true); - }); - - it("keeps stale connection completions from clearing the active connection", () => { - const firstPromise = Promise.resolve(); - const secondPromise = Promise.resolve(); - const tracker = new ConnectionWorkTracker(); - const first = tracker.begin(); - first.promise = firstPromise; - const second = tracker.begin(); - second.promise = secondPromise; - - tracker.finish(first, firstPromise); - expect(tracker.active()).toBe(second); - tracker.finish(second, firstPromise); - expect(tracker.active()).toBe(second); - tracker.finish(second, secondPromise); - expect(tracker.active()).toBeNull(); - }); - - it("clears restored-thread loading only for the active loading promise", () => { - const firstLoading = Promise.resolve(); - const secondLoading = Promise.resolve(); - const placeholder = transitionRestoredThreadLifecycle( - { kind: "idle" }, - { type: "placeholder-restored", restoredThread: { threadId: "thread", title: "Old", explicitName: null } }, - ); - const loading = transitionRestoredThreadLifecycle(placeholder, { type: "loading-started", loading: secondLoading }); - - expect(transitionRestoredThreadLifecycle(loading, { type: "loading-finished", loading: firstLoading })).toBe(loading); - expect(transitionRestoredThreadLifecycle(loading, { type: "renamed", threadId: "thread", name: "New" })).toMatchObject({ - title: "New", - explicitName: "New", - loading: secondLoading, - }); - expect(transitionRestoredThreadLifecycle(loading, { type: "loading-finished", loading: secondLoading })).toMatchObject({ - kind: "placeholder", - loading: null, - }); - }); -}); diff --git a/tests/features/chat/host/session-graph.test.ts b/tests/features/chat/host/session-graph.test.ts index af3fd79f..83f09639 100644 --- a/tests/features/chat/host/session-graph.test.ts +++ b/tests/features/chat/host/session-graph.test.ts @@ -5,9 +5,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { StaleAppServerSharedQueryContextError } from "../../../../src/app-server/query/shared-queries"; import type { ModelMetadata } from "../../../../src/domain/catalog/metadata"; import type { Thread } from "../../../../src/domain/threads/model"; -import { ChatResumeWorkTracker } from "../../../../src/features/chat/application/lifecycle"; import { type ChatStateStore, createChatStateStore } from "../../../../src/features/chat/application/state/store"; import { HistoryController } from "../../../../src/features/chat/application/threads/history-controller"; +import { ChatResumeWorkTracker } from "../../../../src/features/chat/application/threads/resume-work"; import type { ChatPanelEnvironment } from "../../../../src/features/chat/host/contracts"; import { createChatViewDeferredTasks } from "../../../../src/features/chat/host/deferred-work"; import { createChatPanelSessionGraph } from "../../../../src/features/chat/host/session-graph"; diff --git a/tests/features/chat/panel/vault-note-candidate-provider.test.ts b/tests/features/chat/host/vault-note-candidate-provider.test.ts similarity index 99% rename from tests/features/chat/panel/vault-note-candidate-provider.test.ts rename to tests/features/chat/host/vault-note-candidate-provider.test.ts index efe05089..eadcc723 100644 --- a/tests/features/chat/panel/vault-note-candidate-provider.test.ts +++ b/tests/features/chat/host/vault-note-candidate-provider.test.ts @@ -1,7 +1,7 @@ import { type App, type EventRef, TFile } from "obsidian"; import { describe, expect, it, vi } from "vitest"; -import { VaultNoteCandidateProvider } from "../../../../src/features/chat/panel/vault-note-candidate-provider"; +import { VaultNoteCandidateProvider } from "../../../../src/features/chat/host/vault-note-candidate-provider.obsidian"; describe("VaultNoteCandidateProvider", () => { it("builds note candidates from markdown files", () => { diff --git a/tests/features/chat/host/view-connection.test.ts b/tests/features/chat/host/view-connection.test.ts index 820dd82b..b2d2e9fa 100644 --- a/tests/features/chat/host/view-connection.test.ts +++ b/tests/features/chat/host/view-connection.test.ts @@ -6,13 +6,13 @@ import { modelMetadataFromCatalogModels } from "../../../../src/app-server/proto import type { ThreadRecord } from "../../../../src/app-server/protocol/thread"; import type { ThreadCatalogEvent } from "../../../../src/app-server/query/thread-catalog"; import type { ModelMetadata } from "../../../../src/domain/catalog/metadata"; -import type { ObservedResult } from "../../../../src/domain/observed-result"; import { emptyRuntimeConfigSnapshot } from "../../../../src/domain/runtime/config"; import { createServerDiagnostics } from "../../../../src/domain/server/diagnostics"; import type { SharedServerMetadata } from "../../../../src/domain/server/metadata"; import type { Thread } from "../../../../src/domain/threads/model"; import type { CodexChatHost } from "../../../../src/features/chat/host/contracts"; import { DEFAULT_SETTINGS } from "../../../../src/settings/model"; +import type { ObservedResult } from "../../../../src/shared/query/observed-result"; import { notices } from "../../../mocks/obsidian"; import { deferred, waitForAsyncWork } from "../../../support/async"; import { installObsidianDomShims } from "../../../support/dom"; diff --git a/tests/features/chat/presentation/message-stream/view-model.test.ts b/tests/features/chat/presentation/message-stream/view-model.test.ts index 8ece5c28..326f8671 100644 --- a/tests/features/chat/presentation/message-stream/view-model.test.ts +++ b/tests/features/chat/presentation/message-stream/view-model.test.ts @@ -103,7 +103,7 @@ function agentItem(id: string, turnId: string): MessageStreamItem { tool: "spawnAgent", senderThreadId: "sender", receiverThreadIds: ["receiver"], - agents: [{ threadId: "receiver", status: "running", message: "Still working" }], + agents: [{ threadId: "receiver", status: "running", executionState: "running", message: "Still working" }], status: "running", prompt: null, model: null, diff --git a/tests/features/chat/ui/message-stream/stream-items.test.tsx b/tests/features/chat/ui/message-stream/stream-items.test.tsx index 5037c7f7..71f22c2c 100644 --- a/tests/features/chat/ui/message-stream/stream-items.test.tsx +++ b/tests/features/chat/ui/message-stream/stream-items.test.tsx @@ -337,7 +337,7 @@ describe("message stream item renderer decisions", () => { prompt: null, model: null, reasoningEffort: null, - agents: [{ threadId: "running", status: "running", message: "Inspecting renderer" }], + agents: [{ threadId: "running", status: "running", executionState: "running", message: "Inspecting renderer" }], }, ], pendingRequests: { @@ -390,7 +390,7 @@ describe("message stream item renderer decisions", () => { prompt: null, model: null, reasoningEffort: null, - agents: [{ threadId: "running", status: "running", message: null }], + agents: [{ threadId: "running", status: "running", executionState: "running", message: null }], }, { id: "plan-progress-turn", @@ -426,7 +426,7 @@ describe("message stream item renderer decisions", () => { prompt: "Inspect the renderer.", model: null, reasoningEffort: null, - agents: [{ threadId: "child", status: "completed", message: null }], + agents: [{ threadId: "child", status: "completed", executionState: "completed", message: null }], }, { id: "plan-progress-turn", @@ -451,7 +451,7 @@ describe("message stream item renderer decisions", () => { prompt: null, model: null, reasoningEffort: null, - agents: [{ threadId: "child", status: "running", message: "Inspecting renderer" }], + agents: [{ threadId: "child", status: "running", executionState: "running", message: "Inspecting renderer" }], }, ], }); @@ -482,7 +482,7 @@ describe("message stream item renderer decisions", () => { prompt: "Inspect the renderer.", model: "gpt-5.5", reasoningEffort: "high", - agents: [{ threadId: "child", status: "completed", message: "Done" }], + agents: [{ threadId: "child", status: "completed", executionState: "completed", message: "Done" }], }, ], })[0]; @@ -548,7 +548,7 @@ describe("message stream item renderer decisions", () => { prompt: null, model: null, reasoningEffort: null, - agents: [{ threadId, status: "completed", message: longMessage }], + agents: [{ threadId, status: "completed", executionState: "completed", message: longMessage }], }, ], onDisclosureToggle, @@ -588,8 +588,8 @@ describe("message stream item renderer decisions", () => { model: null, reasoningEffort: null, agents: [ - { threadId: "done", status: "completed", message: null }, - { threadId: "running", status: "running", message: "Inspecting renderer" }, + { threadId: "done", status: "completed", executionState: "completed", message: null }, + { threadId: "running", status: "running", executionState: "running", message: "Inspecting renderer" }, ], }, ], @@ -665,7 +665,7 @@ describe("message stream item renderer decisions", () => { prompt: null, model: null, reasoningEffort: null, - agents: [{ threadId: "done", status: "completed", message: null }], + agents: [{ threadId: "done", status: "completed", executionState: "completed", message: null }], }, ], }); @@ -691,8 +691,8 @@ describe("message stream item renderer decisions", () => { model: null, reasoningEffort: null, agents: [ - { threadId: "failed", status: "errored", message: "Failed" }, - { threadId: "running", status: "running", message: null }, + { threadId: "failed", status: "errored", executionState: "failed", message: "Failed" }, + { threadId: "running", status: "running", executionState: "running", message: null }, ], }, ], diff --git a/tests/features/threads-view/view.test.ts b/tests/features/threads-view/view.test.ts index 1b6eff5a..7afba607 100644 --- a/tests/features/threads-view/view.test.ts +++ b/tests/features/threads-view/view.test.ts @@ -3,9 +3,9 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { TurnRecord } from "../../../src/app-server/protocol/turn"; import type * as ThreadTitleGeneratorModule from "../../../src/app-server/services/thread-title-generation"; -import type { ObservedResult } from "../../../src/domain/observed-result"; import type { Thread } from "../../../src/domain/threads/model"; import { DEFAULT_SETTINGS } from "../../../src/settings/model"; +import type { ObservedResult } from "../../../src/shared/query/observed-result"; import { deferred, waitForAsyncWork } from "../../support/async"; import { changeInputValue, installObsidianDomShims } from "../../support/dom"; diff --git a/tests/settings/settings-tab.test.ts b/tests/settings/settings-tab.test.ts index fdf57f29..0851ade9 100644 --- a/tests/settings/settings-tab.test.ts +++ b/tests/settings/settings-tab.test.ts @@ -6,11 +6,11 @@ import type { CatalogHookMetadata, CatalogModel } from "../../src/app-server/pro import { modelMetadataFromCatalogModels } from "../../src/app-server/protocol/catalog"; import type { ThreadRecord } from "../../src/app-server/protocol/thread"; import type { ModelMetadata, ReasoningEffort } from "../../src/domain/catalog/metadata"; -import type { ObservedResult } from "../../src/domain/observed-result"; import type { Thread } from "../../src/domain/threads/model"; import { SettingsDynamicSectionsController, type SettingsDynamicSectionsSnapshot } from "../../src/settings/dynamic-sections-controller"; import type { CodexPanelSettingTabHost } from "../../src/settings/host"; import { CodexPanelSettingTab } from "../../src/settings/tab.obsidian"; +import type { ObservedResult } from "../../src/shared/query/observed-result"; import { notices } from "../mocks/obsidian"; import { deferred } from "../support/async"; import { installObsidianDomShims } from "../support/dom"; diff --git a/tests/shared/lifecycle/connection-work.test.ts b/tests/shared/lifecycle/connection-work.test.ts new file mode 100644 index 00000000..890f1ff4 --- /dev/null +++ b/tests/shared/lifecycle/connection-work.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; + +import { ConnectionWorkTracker } from "../../../src/shared/lifecycle/connection-work"; + +describe("ConnectionWorkTracker", () => { + it("tracks active connection work by identity", () => { + const tracker = new ConnectionWorkTracker(); + const connection = tracker.begin(); + const stale = { kind: "connecting" as const, promise: Promise.resolve() }; + + expect(tracker.active()).toBe(connection); + expect(tracker.isStale(connection)).toBe(false); + expect(tracker.isStale(stale)).toBe(true); + + const promise = Promise.resolve(); + connection.promise = promise; + tracker.finish(connection, Promise.resolve()); + expect(tracker.active()).toBe(connection); + tracker.finish(connection, promise); + expect(tracker.active()).toBeNull(); + }); + + it("keeps stale connection completions from clearing the active connection", () => { + const firstPromise = Promise.resolve(); + const secondPromise = Promise.resolve(); + const tracker = new ConnectionWorkTracker(); + const first = tracker.begin(); + first.promise = firstPromise; + const second = tracker.begin(); + second.promise = secondPromise; + + tracker.finish(first, firstPromise); + expect(tracker.active()).toBe(second); + tracker.finish(second, firstPromise); + expect(tracker.active()).toBe(second); + tracker.finish(second, secondPromise); + expect(tracker.active()).toBeNull(); + }); +}); diff --git a/tests/app-server/observed-result.test.ts b/tests/shared/query/observed-result.test.ts similarity index 90% rename from tests/app-server/observed-result.test.ts rename to tests/shared/query/observed-result.test.ts index 14769dbd..3312526f 100644 --- a/tests/app-server/observed-result.test.ts +++ b/tests/shared/query/observed-result.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import type { ObservedResult } from "../../src/domain/observed-result"; -import { observedInitialError, observedInitialLoading, observedValue } from "../../src/domain/observed-result"; +import type { ObservedResult } from "../../../src/shared/query/observed-result"; +import { observedInitialError, observedInitialLoading, observedValue } from "../../../src/shared/query/observed-result"; describe("observed query result helpers", () => { it("treats successful empty arrays as current values", () => {