Rehome chat and shared boundary modules

This commit is contained in:
murashit 2026-06-27 20:39:13 +09:00
parent 19dc423f21
commit 56b4c7d0b9
61 changed files with 338 additions and 325 deletions

View file

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

View file

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

View file

@ -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<readonly Thread[]>;

View file

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

View file

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

View file

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

View file

@ -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<Record<string, MessageStreamExecutionState>>;
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;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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<TurnItem, { type: "userMessage" }>;
@ -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",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.";

View file

@ -4,10 +4,6 @@ export interface RestoredThreadState {
explicitName: string | null;
}
export type ChatResumeLifecycleState = { kind: "idle" } | { kind: "resuming"; threadId: string };
export type ActiveChatResume = Extract<ChatResumeLifecycleState, { kind: "resuming" }>;
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<void> | null };
@ -19,41 +15,6 @@ export type RestoredThreadLifecycleEvent =
| { type: "loading-finished"; loading: Promise<void> }
| { 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,

View file

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

View file

@ -0,0 +1,30 @@
export type ChatResumeLifecycleState = { kind: "idle" } | { kind: "resuming"; threadId: string };
export type ActiveChatResume = Extract<ChatResumeLifecycleState, { kind: "resuming" }>;
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" };
}
}

View file

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

View file

@ -1,83 +1,4 @@
import type { ExecutionState } from "./items";
export type MessageStreamExecutionState = Exclude<ExecutionState, null>;
export type ExecutionStateByStatus = Readonly<Record<string, MessageStreamExecutionState>>;
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;
}

View file

@ -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;
}

View file

@ -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";
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 };

View file

@ -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);

View file

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

View file

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

View file

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

View file

@ -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", () => {

View file

@ -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 }],
},
];

View file

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

View file

@ -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,
});
});
});

View file

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

View file

@ -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);
});
});

View file

@ -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();
});
});

View file

@ -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,
});
});
});

View file

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

View file

@ -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", () => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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();
});
});

View file

@ -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", () => {