From c7ecbf65bcea96e2be0cc717259fc10a29eeab02 Mon Sep 17 00:00:00 2001 From: murashit Date: Fri, 12 Jun 2026 10:28:07 +0900 Subject: [PATCH] Organize chat display and panel view-model modules --- eslint.config.mjs | 2 +- src/features/chat/chat-host.ts | 2 +- .../chat/conversation/composer/controller.ts | 2 +- .../chat/conversation/turns/composition.ts | 4 +- src/features/chat/display/agent.ts | 72 ++++++++++- src/features/chat/display/blocks.ts | 3 +- src/features/chat/display/labels.ts | 46 ------- ...rmission-details.ts => permission-rows.ts} | 8 +- src/features/chat/display/plan.ts | 29 ++++- src/features/chat/display/review.ts | 24 +++- .../runtime-status.ts} | 65 +++++++++- src/features/chat/display/state.ts | 105 --------------- src/features/chat/display/tool-view.ts | 3 +- src/features/chat/panel/controller-ports.ts | 4 +- .../chat/{ui => panel}/region-view-models.ts | 14 +- src/features/chat/panel/ui-ports.ts | 4 +- .../chat/panel/view-model/composer.ts | 88 ++++++++++++- src/features/chat/panel/view-model/runtime.ts | 111 ---------------- .../chat/panel/view-model/thread-title.ts | 7 +- src/features/chat/panel/view-model/toolbar.ts | 63 ++++++++- src/features/chat/panel/view-model/types.ts | 122 ------------------ src/features/chat/protocol/display-items.ts | 66 ++++++++-- .../chat/protocol/requests/approval.ts | 23 +++- src/features/chat/turn-diff/model.ts | 30 +++++ .../turn-diff.tsx => turn-diff/render.tsx} | 32 +---- .../view.ts} | 10 +- src/features/chat/ui/composer.tsx | 2 +- .../chat/ui/message-stream/context.ts | 2 +- .../chat/ui/message-stream/message-item.tsx | 7 +- .../{context-port.ts => ports.ts} | 2 +- .../{render-state.ts => render-model.ts} | 4 +- .../chat/ui/message-stream/renderer.ts | 6 +- .../{blocks.tsx => stream-blocks.tsx} | 0 .../{context-builder.ts => view-model.ts} | 2 +- src/features/chat/ui/regions.tsx | 2 +- src/features/chat/ui/toolbar.tsx | 4 +- src/features/chat/ui/work-items.tsx | 18 ++- src/features/chat/view.ts | 9 +- src/main.ts | 4 +- .../chat/display/display-model.test.ts | 41 +++--- .../{context-port.test.ts => ports.test.ts} | 2 +- .../chat/ui/message-stream/test-helpers.tsx | 2 +- .../chat/ui/renderers/composer.test.ts | 2 +- .../chat/ui/renderers/toolbar.test.ts | 2 +- .../chat/ui/renderers/turn-diff.test.ts | 5 +- tests/features/chat/view-model.test.ts | 12 +- tests/runtime/runtime-settings.test.ts | 4 +- 47 files changed, 534 insertions(+), 537 deletions(-) delete mode 100644 src/features/chat/display/labels.ts rename src/features/chat/display/{permission-details.ts => permission-rows.ts} (91%) rename src/features/chat/{runtime/status-summary.ts => display/runtime-status.ts} (85%) delete mode 100644 src/features/chat/display/state.ts rename src/features/chat/{ui => panel}/region-view-models.ts (88%) delete mode 100644 src/features/chat/panel/view-model/runtime.ts delete mode 100644 src/features/chat/panel/view-model/types.ts create mode 100644 src/features/chat/turn-diff/model.ts rename src/features/chat/{ui/turn-diff.tsx => turn-diff/render.tsx} (77%) rename src/features/chat/{chat-turn-diff-view.ts => turn-diff/view.ts} (87%) rename src/features/chat/ui/message-stream/{context-port.ts => ports.ts} (95%) rename src/features/chat/ui/message-stream/{render-state.ts => render-model.ts} (91%) rename src/features/chat/ui/message-stream/{blocks.tsx => stream-blocks.tsx} (100%) rename src/features/chat/ui/message-stream/{context-builder.ts => view-model.ts} (98%) rename tests/features/chat/ui/message-stream/{context-port.test.ts => ports.test.ts} (98%) diff --git a/eslint.config.mjs b/eslint.config.mjs index cbed7a09..182dacd2 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -110,7 +110,7 @@ const chatPreactDomBridgeFiles = [ "src/features/chat/ui/goal-banner.tsx", "src/features/chat/ui/shell.tsx", "src/features/chat/ui/tool-result.tsx", - "src/features/chat/ui/turn-diff.tsx", + "src/features/chat/turn-diff/render.tsx", ]; const chatImperativeDomBridgeFiles = [...chatExternalDomBridgeFiles, ...chatPreactDomBridgeFiles]; const nonChatImperativeDomBridgeFiles = [ diff --git a/src/features/chat/chat-host.ts b/src/features/chat/chat-host.ts index 74682458..d74486c2 100644 --- a/src/features/chat/chat-host.ts +++ b/src/features/chat/chat-host.ts @@ -1,7 +1,7 @@ import type { Thread } from "../../domain/threads/model"; import type { SharedAppServerMetadata } from "../../app-server/shared-cache-state"; import type { CodexPanelSettings } from "../../settings/model"; -import type { ChatTurnDiffViewState } from "./ui/turn-diff"; +import type { ChatTurnDiffViewState } from "./turn-diff/model"; export interface CodexChatHost { readonly settings: CodexPanelSettings; diff --git a/src/features/chat/conversation/composer/controller.ts b/src/features/chat/conversation/composer/controller.ts index b3124f09..8a4c4233 100644 --- a/src/features/chat/conversation/composer/controller.ts +++ b/src/features/chat/conversation/composer/controller.ts @@ -5,7 +5,7 @@ import type { CodexInput } from "../../../../app-server/request-input"; import { isComposerSendKey, type SendShortcut } from "../../../../shared/ui/keyboard"; import { textareaCursorAtVisualBoundary } from "../../../../shared/ui/textarea-caret"; import { chatTurnBusy, type ChatAction, type ChatState, type ChatStateStore } from "../../state/reducer"; -import type { ComposerMetaViewModel } from "../../panel/view-model/types"; +import type { ComposerMetaViewModel } from "../../panel/view-model/composer"; import { composerShellNode, syncComposerHeight, type ComposerCallbacks } from "../../ui/composer"; import { composerBoundaryScrollDirection, type ComposerBoundaryScrollAction } from "./boundary-scroll"; import { noteCandidates as appNoteCandidates, resolveWikiLinkMention as resolveAppWikiLinkMention } from "./obsidian-context"; diff --git a/src/features/chat/conversation/turns/composition.ts b/src/features/chat/conversation/turns/composition.ts index 1ad436d7..5fa29494 100644 --- a/src/features/chat/conversation/turns/composition.ts +++ b/src/features/chat/conversation/turns/composition.ts @@ -21,8 +21,8 @@ import type { RuntimeSnapshot } from "../../runtime/model"; import { ChatMessageRenderer } from "../../ui/message-stream/renderer"; import type { DisplayDetailSection } from "../../display/types"; import type { ChatMessageScrollIntentController } from "../../panel/message-scroll-intent-controller"; -import type { ChatTurnDiffViewState } from "../../ui/turn-diff"; -import type { ComposerMetaViewModel } from "../../panel/view-model/types"; +import type { ChatTurnDiffViewState } from "../../turn-diff/model"; +import type { ComposerMetaViewModel } from "../../panel/view-model/composer"; import type { CodexPanelSettings } from "../../../../settings/model"; interface ConversationSurfaceControllerGroupPorts { diff --git a/src/features/chat/display/agent.ts b/src/features/chat/display/agent.ts index 5f83b635..6fb999e3 100644 --- a/src/features/chat/display/agent.ts +++ b/src/features/chat/display/agent.ts @@ -1,10 +1,28 @@ import type { AgentDisplayItem, AgentRunSummary, AgentRunSummaryAgent, AgentStateDisplay, DisplayItem, ExecutionState } from "./types"; -import { definedProp } from "../../../utils"; -import { agentActivitySummaryLabel, agentMessagePreview } from "./labels"; -import { collabAgentStateExecutionState, collabAgentToolCallExecutionState } from "./state"; +import { definedProp, truncate } from "../../../utils"; const ACTIVE_AGENT_PREVIEW_LIMIT = 96; type AgentRunState = "running" | "completed" | "failed"; +type DisplayExecutionState = Exclude; +type ExecutionStateByStatus = Readonly>; + +const AGENT_STATES = { + pendingInit: "running", + running: "running", + inProgress: "running", + completed: "completed", + shutdown: "completed", + interrupted: "failed", + errored: "failed", + notFound: "failed", + failed: "failed", +} as const satisfies ExecutionStateByStatus; + +const STANDARD_TOOL_STATES = { + inProgress: "running", + completed: "completed", + failed: "failed", +} as const satisfies ExecutionStateByStatus; interface DisplayCollabAgentToolCall { id: string; @@ -46,6 +64,24 @@ export function agentDisplayItem(item: DisplayCollabAgentToolCall, turnId?: stri }; } +export function agentActivitySummaryLabel(tool: string): string { + if (tool === "spawnAgent") return "Spawn agent"; + if (tool === "sendInput") return "Send input to agent"; + if (tool === "resumeAgent") return "Resume agent"; + if (tool === "wait") return "Wait for agent"; + if (tool === "closeAgent") return "Close agent"; + return `Agent ${tool}`; +} + +export function agentActivityMetaLabel(tool: string): string { + if (tool === "spawnAgent") return "spawn"; + if (tool === "sendInput") return "send input"; + if (tool === "resumeAgent") return "resume"; + if (tool === "wait") return "wait"; + if (tool === "closeAgent") return "close"; + return tool; +} + export function activeAgentRunSummary(items: readonly DisplayItem[], activeTurnId: string | null): AgentRunSummary | null { if (!activeTurnId) return null; @@ -86,6 +122,24 @@ export function activeAgentRunSummary(items: readonly DisplayItem[], activeTurnI return summary; } +export function agentRunSummaryLabel(summary: AgentRunSummary): string { + const parts: string[] = []; + if (summary.failed > 0) parts.push(`${String(summary.failed)} failed`); + if (summary.running > 0) parts.push(`${String(summary.running)} running`); + if (summary.completed > 0) parts.push(`${String(summary.completed)} done`); + return `Agents ${parts.join(", ")}`; +} + +export function agentMessagePreview(message: string | null, maxLength: number): string | null { + if (!message) return null; + const firstLine = message + .split(/\r?\n/) + .map((line) => line.trim()) + .find((line) => line.length > 0); + if (!firstLine) return null; + return truncate(firstLine.replace(/\s+/g, " "), maxLength); +} + function agentStatesDisplay(states: DisplayCollabAgentToolCall["agentsStates"]): AgentStateDisplay[] { return Object.entries(states) .map(([threadId, state]) => ({ @@ -112,3 +166,15 @@ function collabAgentExecutionState(tool: string, status: string, receiverThreadI function agentRunState(status: string): AgentRunState { return collabAgentStateExecutionState(status) ?? "running"; } + +export function collabAgentStateExecutionState(status: string): ExecutionState { + return executionStateFromStatus(status, AGENT_STATES); +} + +function collabAgentToolCallExecutionState(status: string): ExecutionState { + return executionStateFromStatus(status, STANDARD_TOOL_STATES); +} + +function executionStateFromStatus(status: string, states: ExecutionStateByStatus): ExecutionState { + return states[status] ?? null; +} diff --git a/src/features/chat/display/blocks.ts b/src/features/chat/display/blocks.ts index d8c9c1e7..32ed7c56 100644 --- a/src/features/chat/display/blocks.ts +++ b/src/features/chat/display/blocks.ts @@ -1,7 +1,6 @@ import type { DisplayBlock, DisplayItem, DisplayKind } from "./types"; import { isCompletedTurnOutcomeMessage } from "./turn-outcome-message"; import { pathRelativeToRoot } from "./paths"; -import { executionState } from "./state"; const STEERING_ACTIVITY_LABEL = "steer"; const STEERING_ACTIVITY_KIND = "userSteered"; @@ -62,7 +61,7 @@ export function displayBlocksForItems( } function shouldShowDisplayItem(item: DisplayItem): boolean { - return item.kind !== "reasoning" || executionState(item) !== "completed" || item.text.trim().length > 0; + return item.kind !== "reasoning" || item.executionState !== "completed" || item.text.trim().length > 0; } function isSteeringUserMessage(item: DisplayItem, seenUserMessagesByTurn: Map): boolean { diff --git a/src/features/chat/display/labels.ts b/src/features/chat/display/labels.ts deleted file mode 100644 index bff676a6..00000000 --- a/src/features/chat/display/labels.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { truncate } from "../../../utils"; -import type { AgentRunSummary } from "./types"; - -export type TaskStepStatus = "pending" | "inProgress" | "completed"; - -export function taskStatusMarker(status: TaskStepStatus): string { - if (status === "completed") return "[x]"; - if (status === "inProgress") return "[>]"; - return "[ ]"; -} - -export function agentActivitySummaryLabel(tool: string): string { - if (tool === "spawnAgent") return "Spawn agent"; - if (tool === "sendInput") return "Send input to agent"; - if (tool === "resumeAgent") return "Resume agent"; - if (tool === "wait") return "Wait for agent"; - if (tool === "closeAgent") return "Close agent"; - return `Agent ${tool}`; -} - -export function agentActivityMetaLabel(tool: string): string { - if (tool === "spawnAgent") return "spawn"; - if (tool === "sendInput") return "send input"; - if (tool === "resumeAgent") return "resume"; - if (tool === "wait") return "wait"; - if (tool === "closeAgent") return "close"; - return tool; -} - -export function agentRunSummaryLabel(summary: AgentRunSummary): string { - const parts: string[] = []; - if (summary.failed > 0) parts.push(`${String(summary.failed)} failed`); - if (summary.running > 0) parts.push(`${String(summary.running)} running`); - if (summary.completed > 0) parts.push(`${String(summary.completed)} done`); - return `Agents ${parts.join(", ")}`; -} - -export function agentMessagePreview(message: string | null, maxLength: number): string | null { - if (!message) return null; - const firstLine = message - .split(/\r?\n/) - .map((line) => line.trim()) - .find((line) => line.length > 0); - if (!firstLine) return null; - return truncate(firstLine.replace(/\s+/g, " "), maxLength); -} diff --git a/src/features/chat/display/permission-details.ts b/src/features/chat/display/permission-rows.ts similarity index 91% rename from src/features/chat/display/permission-details.ts rename to src/features/chat/display/permission-rows.ts index 85c2b0a7..64e4c32d 100644 --- a/src/features/chat/display/permission-details.ts +++ b/src/features/chat/display/permission-rows.ts @@ -1,6 +1,6 @@ import { jsonPreview } from "../../../utils"; -export interface DetailRow { +interface DetailRow { key: string; value: string; } @@ -49,7 +49,7 @@ export function permissionRows(permissions: DisplayPermissionProfile): DetailRow return rows; } -export function addOptional(rows: DetailRow[], key: string, value: unknown): void { +function addOptional(rows: DetailRow[], key: string, value: unknown): void { if (value === null || value === undefined) return; if (Array.isArray(value) && value.length === 0) return; rows.push({ key, value: stringValue(value) }); @@ -65,10 +65,6 @@ function stringValue(value: unknown, fallback = ""): string { return jsonPreview(value); } -export function nonEmptyString(value: unknown): string | null { - return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; -} - function fileSystemPathLabel(path: DisplayFileSystemPath): string { if (path.type === "path") return path.path; if (path.type === "glob_pattern") return path.pattern; diff --git a/src/features/chat/display/plan.ts b/src/features/chat/display/plan.ts index e390ee3b..ef625d94 100644 --- a/src/features/chat/display/plan.ts +++ b/src/features/chat/display/plan.ts @@ -1,12 +1,31 @@ -import type { DisplayItem } from "./types"; -import { taskStatusMarker, type TaskStepStatus } from "./labels"; -import { taskProgressExecutionState } from "./state"; +import type { DisplayItem, ExecutionState } from "./types"; + +type DisplayExecutionState = Exclude; +type ExecutionStateByStatus = Readonly>; + +const TASK_STATES = { + pending: "running", + inProgress: "running", + completed: "completed", +} as const satisfies ExecutionStateByStatus; + +export type TaskStepStatus = "pending" | "inProgress" | "completed"; export interface TaskPlanStep { step: string; status: TaskStepStatus; } +export function taskStatusMarker(status: TaskStepStatus): string { + if (status === "completed") return "[x]"; + if (status === "inProgress") return "[>]"; + return "[ ]"; +} + +export function taskProgressExecutionState(status: string): ExecutionState { + return executionStateFromStatus(status, TASK_STATES); +} + export function normalizeProposedPlanMarkdown(text: string): string { return text .replace(/^\s*\s*\n?/i, "") @@ -32,3 +51,7 @@ export function planProgressDisplayItem(turnId: string, explanation: string | nu executionState: taskProgressExecutionState(status), }; } + +function executionStateFromStatus(status: string, states: ExecutionStateByStatus): ExecutionState { + return states[status] ?? null; +} diff --git a/src/features/chat/display/review.ts b/src/features/chat/display/review.ts index b0b6260a..d3f89380 100644 --- a/src/features/chat/display/review.ts +++ b/src/features/chat/display/review.ts @@ -1,7 +1,17 @@ -import { permissionRows } from "./permission-details"; -import type { DisplayItem } from "./types"; +import { permissionRows } from "./permission-rows"; +import type { DisplayItem, ExecutionState } from "./types"; import { pathsRelativeToRoot } from "./paths"; -import { autoReviewExecutionState } from "./state"; + +type DisplayExecutionState = Exclude; +type ExecutionStateByStatus = Readonly>; + +const AUTO_REVIEW_STATES = { + inProgress: "running", + approved: "completed", + denied: "failed", + timedOut: "failed", + aborted: "failed", +} as const satisfies ExecutionStateByStatus; type AutoReviewNotification = AutoReviewStartedNotification | AutoReviewCompletedNotification; @@ -177,3 +187,11 @@ function autoReviewActionLabel(action: AutoReviewAction): string { if (action.type === "mcpToolCall") return `${action.server}.${action.toolName}`; return action.reason ?? "permission request"; } + +export function autoReviewExecutionState(status: string): ExecutionState { + return executionStateFromStatus(status, AUTO_REVIEW_STATES); +} + +function executionStateFromStatus(status: string, states: ExecutionStateByStatus): ExecutionState { + return states[status] ?? null; +} diff --git a/src/features/chat/runtime/status-summary.ts b/src/features/chat/display/runtime-status.ts similarity index 85% rename from src/features/chat/runtime/status-summary.ts rename to src/features/chat/display/runtime-status.ts index a6f79c2e..799f0006 100644 --- a/src/features/chat/runtime/status-summary.ts +++ b/src/features/chat/display/runtime-status.ts @@ -3,6 +3,7 @@ import type { RuntimeConfigSnapshot } from "../../../app-server/runtime-config"; import { jsonPreview } from "../../../utils"; import { sortedModelMetadata } from "../../../domain/catalog/metadata"; import { defaultEffortForModelMetadata } from "../../../domain/catalog/metadata"; +import type { ChatState } from "../state/reducer"; import { currentApprovalsReviewer, currentApprovalPolicy, @@ -12,8 +13,9 @@ import { fastModeLabel, runtimeConfigOrDefault, serviceTierLabel, -} from "./effective-settings"; -import { collaborationModeLabel, pendingRuntimeSettingLabel, type RuntimeSnapshot } from "./model"; + supportedReasoningEfforts, +} from "../runtime/effective-settings"; +import { collaborationModeLabel, pendingRuntimeSettingLabel, type RuntimeSnapshot } from "../runtime/model"; export interface ContextSummary { label: string; @@ -43,6 +45,25 @@ export interface RuntimeConfigSection { rows: { key: string; value: string }[]; } +export interface StatusSummaryLinesInput { + activeThreadId: ChatState["activeThread"]["id"]; + snapshot: RuntimeSnapshot; + nowMs: number; +} + +export interface ModelStatusLinesInput { + runtimeConfig: ChatState["connection"]["runtimeConfig"]; + requestedModel: ChatState["runtime"]["requestedModel"]; + snapshot: RuntimeSnapshot; + collaborationModeLabel: string; +} + +export interface EffortStatusLinesInput { + runtimeConfig: ChatState["connection"]["runtimeConfig"]; + requestedReasoningEffort: ChatState["runtime"]["requestedReasoningEffort"]; + snapshot: RuntimeSnapshot; +} + const CODEX_DEFAULT_LABEL = "(Codex default)"; const NOT_REPORTED_LABEL = "(not reported)"; @@ -86,7 +107,7 @@ export function contextSummary(snapshot: RuntimeSnapshot): ContextSummary | null }; } -export function rateLimitSummary(snapshot: RuntimeSnapshot, nowMs = Date.now()): RateLimitSummary | null { +export function rateLimitSummary(snapshot: RuntimeSnapshot, nowMs: number): RateLimitSummary | null { const rateLimit = snapshot.rateLimit; if (!rateLimit) return null; @@ -176,6 +197,38 @@ export function runtimeConfigSections(snapshot: RuntimeSnapshot, vaultPath: stri ]; } +export function statusSummaryLines(input: StatusSummaryLinesInput): string[] { + const context = contextSummary(input.snapshot); + const limit = rateLimitSummary(input.snapshot, input.nowMs); + return [ + "Thread status", + `Thread: ${input.activeThreadId ?? "(none)"}`, + context ? context.title : "Context: not available", + ...(limit ? usageLimitStatusLines(limit) : ["Usage limits: not available"]), + ]; +} + +export function modelStatusLines(input: ModelStatusLinesInput): string[] { + const config = runtimeConfigOrDefault(input.runtimeConfig); + return [ + `Model: ${currentModel(input.snapshot, config) ?? CODEX_DEFAULT_LABEL}`, + `Override: ${pendingRuntimeSettingLabel(input.requestedModel)}`, + `Provider: ${stringValue(config.modelProvider, CODEX_DEFAULT_LABEL)}`, + `Effort: ${currentReasoningEffort(input.snapshot, config) ?? CODEX_DEFAULT_LABEL}`, + `Mode: ${input.collaborationModeLabel}`, + `Service tier: ${serviceTierLabel(input.snapshot, config)}`, + ]; +} + +export function effortStatusLines(input: EffortStatusLinesInput): string[] { + const config = runtimeConfigOrDefault(input.runtimeConfig); + return [ + `Effort: ${currentReasoningEffort(input.snapshot, config) ?? CODEX_DEFAULT_LABEL}`, + `Override: ${pendingRuntimeSettingLabel(input.requestedReasoningEffort)}`, + `Supported: ${supportedReasoningEfforts(input.snapshot, config).join(", ")}`, + ]; +} + function contextUsageTokens(usage: ThreadTokenUsage): number { return usage.last.inputTokens > 0 ? usage.last.inputTokens : usage.last.totalTokens; } @@ -202,6 +255,10 @@ function activePermissionProfileLabel(profile: RuntimeSnapshot["activePermission return profile.extends ? `${profile.id} extends ${profile.extends}` : profile.id; } +function usageLimitStatusLines(limit: RateLimitSummary): string[] { + return ["Usage limits", ...limit.rows.map((row) => `${row.label}: ${row.value}${row.resetLabel ? ` (${row.resetLabel})` : ""}`)]; +} + function rateLimitWindowSummary( fallbackLabel: string, window: RateLimitWindow | null, @@ -262,7 +319,7 @@ function formatRateLimitDuration(minutes: number): string { } function formatRateLimitReset(resetsAt: number): string { - return new Date(resetsAt * 1000).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" }); + return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(resetsAt * 1000); } function formatRateLimitRemaining(resetsAt: number, nowMs: number): string { diff --git a/src/features/chat/display/state.ts b/src/features/chat/display/state.ts deleted file mode 100644 index e34280bd..00000000 --- a/src/features/chat/display/state.ts +++ /dev/null @@ -1,105 +0,0 @@ -import type { DisplayItem, ExecutionState } from "./types"; - -type DisplayExecutionState = Exclude; -type ExecutionStateByStatus = Readonly>; - -const COMMAND_STATES = { - inProgress: "running", - completed: "completed", - failed: "failed", - declined: "failed", -} as const satisfies ExecutionStateByStatus; - -const PATCH_STATES = { - inProgress: "running", - completed: "completed", - failed: "failed", - declined: "failed", -} as const satisfies ExecutionStateByStatus; - -const STANDARD_TOOL_STATES = { - inProgress: "running", - completed: "completed", - failed: "failed", -} as const satisfies ExecutionStateByStatus; - -const TASK_STATES = { - pending: "running", - inProgress: "running", - completed: "completed", -} as const satisfies ExecutionStateByStatus; - -const AUTO_REVIEW_STATES = { - inProgress: "running", - approved: "completed", - denied: "failed", - timedOut: "failed", - aborted: "failed", -} as const satisfies ExecutionStateByStatus; - -const AGENT_STATES = { - pendingInit: "running", - running: "running", - inProgress: "running", - completed: "completed", - shutdown: "completed", - interrupted: "failed", - errored: "failed", - notFound: "failed", - failed: "failed", -} as const satisfies ExecutionStateByStatus; - -export function executionState(item: DisplayItem): ExecutionState { - return item.executionState ?? null; -} - -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 taskProgressExecutionState(status: string): ExecutionState { - return executionStateFromStatus(status, TASK_STATES); -} - -export function autoReviewExecutionState(status: string): ExecutionState { - return executionStateFromStatus(status, AUTO_REVIEW_STATES); -} - -export function collabAgentToolCallExecutionState(status: string): ExecutionState { - return standardToolCallExecutionState(status); -} - -export function collabAgentStateExecutionState(status: string): ExecutionState { - return executionStateFromStatus(status, AGENT_STATES); -} - -function standardToolCallExecutionState(status: string): ExecutionState { - return executionStateFromStatus(status, STANDARD_TOOL_STATES); -} - -function executionStateFromStatus(status: string, states: ExecutionStateByStatus): ExecutionState { - return states[status] ?? null; -} diff --git a/src/features/chat/display/tool-view.ts b/src/features/chat/display/tool-view.ts index b746e34a..d5ac6772 100644 --- a/src/features/chat/display/tool-view.ts +++ b/src/features/chat/display/tool-view.ts @@ -1,5 +1,4 @@ import { pathRelativeToRoot } from "./paths"; -import { executionState } from "./state"; import { definedProp } from "../../../utils"; import type { ApprovalResultDisplayItem, @@ -157,7 +156,7 @@ function toolView( summary, detailsKey, details, - state: executionState(item), + state: item.executionState ?? null, }; } diff --git a/src/features/chat/panel/controller-ports.ts b/src/features/chat/panel/controller-ports.ts index 824764e8..7cbb30b5 100644 --- a/src/features/chat/panel/controller-ports.ts +++ b/src/features/chat/panel/controller-ports.ts @@ -8,11 +8,11 @@ import type { Thread } from "../../../domain/threads/model"; import type { CodexPanelSettings } from "../../../settings/model"; import type { RuntimeSnapshot } from "../runtime/model"; import type { ChatState, ChatStateStore } from "../state/reducer"; -import type { ChatTurnDiffViewState } from "../ui/turn-diff"; +import type { ChatTurnDiffViewState } from "../turn-diff/model"; import type { ChatMessageScrollIntentController } from "../panel/message-scroll-intent-controller"; import type { DisplayDetailSection, DisplayItem } from "../display/types"; import type { ChatConnectionWorkTracker, ChatResumeWorkTracker, ChatViewDeferredTasks } from "./lifecycle"; -import type { ComposerMetaViewModel } from "./view-model/types"; +import type { ComposerMetaViewModel } from "./view-model/composer"; export interface ChatControllerCompositionPorts { obsidian: ChatPanelObsidianContext; diff --git a/src/features/chat/ui/region-view-models.ts b/src/features/chat/panel/region-view-models.ts similarity index 88% rename from src/features/chat/ui/region-view-models.ts rename to src/features/chat/panel/region-view-models.ts index 9346c0a5..5d0ac6ef 100644 --- a/src/features/chat/ui/region-view-models.ts +++ b/src/features/chat/panel/region-view-models.ts @@ -2,13 +2,13 @@ import { pendingRequestsSignature as requestStateSignature } from "../pending-re import { composerMetaViewModel as buildComposerMetaViewModel, composerPlaceholder as buildComposerPlaceholder, -} from "../panel/view-model/composer"; -import { runtimeComposerChoices } from "../panel/view-model/runtime"; -import { activeComposerThreadName as buildActiveComposerThreadName } from "../panel/view-model/thread-title"; -import { toolbarViewModel as buildToolbarViewModel } from "../panel/view-model/toolbar"; -import type { GoalBannerActions, GoalBannerOptions } from "./goal-banner"; -import type { ChatPanelComposerPorts, ChatPanelGoalPorts, ChatPanelStatePort, ChatPanelToolbarPorts } from "../panel/ui-ports"; -import type { ChatPanelShellState } from "./shell"; + runtimeComposerChoices, +} from "./view-model/composer"; +import { activeComposerThreadName as buildActiveComposerThreadName } from "./view-model/thread-title"; +import { toolbarViewModel as buildToolbarViewModel } from "./view-model/toolbar"; +import type { GoalBannerActions, GoalBannerOptions } from "../ui/goal-banner"; +import type { ChatPanelComposerPorts, ChatPanelGoalPorts, ChatPanelStatePort, ChatPanelToolbarPorts } from "./ui-ports"; +import type { ChatPanelShellState } from "../ui/shell"; export function chatPanelToolbarViewModel(ports: ChatPanelToolbarPorts, shellState: ChatPanelShellState) { const latestState = shellState.latestState(); diff --git a/src/features/chat/panel/ui-ports.ts b/src/features/chat/panel/ui-ports.ts index 00a5874c..aa95cc74 100644 --- a/src/features/chat/panel/ui-ports.ts +++ b/src/features/chat/panel/ui-ports.ts @@ -3,8 +3,8 @@ import type { ReasoningEffort } from "../../../domain/catalog/metadata"; import type { RuntimeSnapshot } from "../runtime/model"; import type { SendShortcut } from "../../../shared/ui/keyboard"; import type { ToolbarActions } from "../ui/toolbar"; -import type { ToolbarThreadRow } from "./view-model/types"; -import type { RestoredThreadTitleSnapshot } from "./view-model/types"; +import type { RestoredThreadTitleSnapshot } from "./view-model/thread-title"; +import type { ToolbarThreadRow } from "./view-model/toolbar"; interface ChatPanelToolbarState { archiveConfirmId: () => string | null; diff --git a/src/features/chat/panel/view-model/composer.ts b/src/features/chat/panel/view-model/composer.ts index 97902889..615ab674 100644 --- a/src/features/chat/panel/view-model/composer.ts +++ b/src/features/chat/panel/view-model/composer.ts @@ -4,12 +4,53 @@ import { currentReasoningEffort, fastModeActive, runtimeConfigOrDefault, + supportedReasoningEfforts, } from "../../runtime/effective-settings"; import { compactReasoningEffortLabel } from "../../conversation/turns/runtime-overrides"; -import { contextSummary } from "../../runtime/status-summary"; +import { contextSummary } from "../../display/runtime-status"; +import { sortedModelMetadata } from "../../../../domain/catalog/metadata"; +import type { ReasoningEffort } from "../../../../domain/catalog/metadata"; import type { RuntimeSnapshot } from "../../runtime/model"; import type { ChatState } from "../../state/reducer"; -import type { ComposerContextMeterCellViewModel, ComposerContextMeterViewModel, ComposerMetaViewModel } from "./types"; + +export interface ComposerMetaViewModel { + fatal: string | null; + context: ComposerContextMeterViewModel; + statusSummary: string; + model: string; + effort: string | null; + planActive: boolean; + autoReviewActive: boolean; + fastActive: boolean; + modelChoices?: RuntimeChoice[]; + effortChoices?: RuntimeChoice[]; +} + +export interface RuntimeChoice { + label: string; + selected?: boolean; + disabled?: boolean; + meta?: string; + onClick: () => void; +} + +export interface ComposerContextMeterCellViewModel { + text: string; + placeholder: boolean; +} + +export interface ComposerContextMeterViewModel { + cells: ComposerContextMeterCellViewModel[]; + percent: string; +} + +export interface RuntimeComposerChoicesInput { + state: ChatState; + snapshot: RuntimeSnapshot; + requestModel: (model: string) => void; + requestReasoningEffort: (effort: ReasoningEffort) => void; + resetReasoningEffortToConfig: () => void; +} export function composerPlaceholder(threadName: string | null): string { return threadName ? `Ask Codex to work on “${threadName}”...` : "Ask Codex to work on this task..."; @@ -57,6 +98,49 @@ export function composerMetaViewModel(state: ChatState, snapshot: RuntimeSnapsho }; } +export function runtimeComposerChoices(input: RuntimeComposerChoicesInput): { + modelChoices: RuntimeChoice[]; + effortChoices: RuntimeChoice[]; +} { + const config = runtimeConfigOrDefault(input.state.connection.runtimeConfig); + const activeModel = currentModel(input.snapshot, config); + const models = sortedModelMetadata(input.state.connection.availableModels); + const modelChoices: RuntimeChoice[] = models.slice(0, 12).map((model) => ({ + label: model.model, + selected: activeModel === model.model, + onClick: () => { + input.requestModel(model.model); + }, + })); + if (models.length === 0) { + modelChoices.push({ + label: "No model list available.", + disabled: true, + onClick: () => undefined, + }); + } + + const activeEffort = currentReasoningEffort(input.snapshot, config); + const effortChoices: RuntimeChoice[] = [ + { + label: "Codex default", + selected: activeEffort === null, + onClick: () => { + input.resetReasoningEffortToConfig(); + }, + }, + ...supportedReasoningEfforts(input.snapshot, config).map((effort) => ({ + label: effort, + selected: activeEffort === effort, + onClick: () => { + input.requestReasoningEffort(effort); + }, + })), + ]; + + return { modelChoices, effortChoices }; +} + function composerStatusSummary(input: { context: ComposerContextMeterViewModel; model: string; diff --git a/src/features/chat/panel/view-model/runtime.ts b/src/features/chat/panel/view-model/runtime.ts deleted file mode 100644 index 289950c6..00000000 --- a/src/features/chat/panel/view-model/runtime.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { - currentModel, - currentReasoningEffort, - runtimeConfigOrDefault, - serviceTierLabel, - supportedReasoningEfforts, -} from "../../runtime/effective-settings"; -import { pendingRuntimeSettingLabel } from "../../runtime/model"; -import { sortedModelMetadata } from "../../../../domain/catalog/metadata"; -import { contextSummary, rateLimitSummary, type RateLimitSummary } from "../../runtime/status-summary"; -import type { - EffortStatusLinesInput, - ModelStatusLinesInput, - RuntimeChoice, - RuntimeComposerChoicesInput, - StatusSummaryLinesInput, -} from "./types"; - -export function runtimeComposerChoices(input: RuntimeComposerChoicesInput): { - modelChoices: RuntimeChoice[]; - effortChoices: RuntimeChoice[]; -} { - const config = runtimeConfigOrDefault(input.state.connection.runtimeConfig); - const activeModel = currentModel(input.snapshot, config); - const models = sortedModelMetadata(input.state.connection.availableModels); - const modelChoices: RuntimeChoice[] = models.slice(0, 12).map((model) => ({ - label: model.model, - selected: activeModel === model.model, - onClick: () => { - input.requestModel(model.model); - }, - })); - if (models.length === 0) { - modelChoices.push({ - label: "No model list available.", - disabled: true, - onClick: () => undefined, - }); - } - - const activeEffort = currentReasoningEffort(input.snapshot, config); - const effortChoices: RuntimeChoice[] = [ - { - label: "Codex default", - selected: activeEffort === null, - onClick: () => { - input.resetReasoningEffortToConfig(); - }, - }, - ...supportedReasoningEfforts(input.snapshot, config).map((effort) => ({ - label: effort, - selected: activeEffort === effort, - onClick: () => { - input.requestReasoningEffort(effort); - }, - })), - ]; - - return { modelChoices, effortChoices }; -} - -export function statusSummaryLines(input: StatusSummaryLinesInput): string[] { - const context = contextSummary(input.snapshot); - const limit = rateLimitSummary(input.snapshot); - return [ - "Thread status", - `Thread: ${input.activeThreadId ?? "(none)"}`, - context ? context.title : "Context: not available", - ...(limit ? usageLimitStatusLines(limit) : ["Usage limits: not available"]), - ]; -} - -export function modelStatusLines(input: ModelStatusLinesInput): string[] { - const config = runtimeConfigOrDefault(input.runtimeConfig); - return [ - `Model: ${currentModel(input.snapshot, config) ?? "(Codex default)"}`, - `Override: ${pendingRuntimeSettingLabel(input.requestedModel)}`, - `Provider: ${statusValue(config.modelProvider, "(Codex default)")}`, - `Effort: ${currentReasoningEffort(input.snapshot, config) ?? "(Codex default)"}`, - `Mode: ${input.collaborationModeLabel}`, - `Service tier: ${serviceTierLabel(input.snapshot, config)}`, - ]; -} - -export function effortStatusLines(input: EffortStatusLinesInput): string[] { - const config = runtimeConfigOrDefault(input.runtimeConfig); - return [ - `Effort: ${currentReasoningEffort(input.snapshot, config) ?? "(Codex default)"}`, - `Override: ${pendingRuntimeSettingLabel(input.requestedReasoningEffort)}`, - `Supported: ${supportedReasoningEfforts(input.snapshot, config).join(", ")}`, - ]; -} - -function statusValue(value: unknown, fallback: string): string { - if (typeof value === "string") return value; - if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value); - if (value === null || value === undefined) return fallback; - return jsonPreview(value, fallback); -} - -function usageLimitStatusLines(limit: RateLimitSummary): string[] { - return ["Usage limits", ...limit.rows.map((row) => `${row.label}: ${row.value}${row.resetLabel ? ` (${row.resetLabel})` : ""}`)]; -} - -function jsonPreview(value: unknown, fallback: string): string { - try { - return JSON.stringify(value); - } catch { - return fallback; - } -} diff --git a/src/features/chat/panel/view-model/thread-title.ts b/src/features/chat/panel/view-model/thread-title.ts index 2887b46f..cb818110 100644 --- a/src/features/chat/panel/view-model/thread-title.ts +++ b/src/features/chat/panel/view-model/thread-title.ts @@ -1,6 +1,11 @@ import { codexPanelDisplayTitle, explicitThreadName, getThreadTitle } from "../../../../domain/threads/model"; import type { ChatState } from "../../state/reducer"; -import type { RestoredThreadTitleSnapshot } from "./types"; + +export interface RestoredThreadTitleSnapshot { + threadId: string; + title: string | null; + explicitName: string | null; +} export function chatViewDisplayTitle(state: ChatState, restoredThreadTitle: string | null): string { return codexPanelDisplayTitle(state.activeThread.id, state.threadList.listedThreads, restoredThreadTitle); diff --git a/src/features/chat/panel/view-model/toolbar.ts b/src/features/chat/panel/view-model/toolbar.ts index 21206cb1..d7b0fd0d 100644 --- a/src/features/chat/panel/view-model/toolbar.ts +++ b/src/features/chat/panel/view-model/toolbar.ts @@ -1,12 +1,69 @@ import type { Thread } from "../../../../domain/threads/model"; import { getThreadTitle } from "../../../../domain/threads/model"; -import { runtimeConfigSections, rateLimitSummary } from "../../runtime/status-summary"; +import { runtimeConfigSections, rateLimitSummary } from "../../display/runtime-status"; +import type { RuntimeConfigSection, RateLimitSummary } from "../../display/runtime-status"; import { connectionDiagnosticSections } from "../../display/diagnostics"; -import type { ConnectionDiagnosticsModelInput, ToolbarThreadRow, ToolbarViewModel, ToolbarViewModelInput } from "./types"; +import type { RuntimeSnapshot } from "../../runtime/model"; +import type { ChatState } from "../../state/reducer"; + +export interface ToolbarThreadRow { + title: string; + threadId: string; + selected: boolean; + disabled: boolean; + canArchive: boolean; + archiveConfirm?: { active: boolean; defaultSaveMarkdown: boolean }; + rename: { + draft: string; + generating: boolean; + } | null; +} + +interface ToolbarDiagnosticRow { + label: string; + value: string; + level?: "normal" | "warning" | "error"; +} + +export interface ToolbarDiagnosticSection { + title: string; + rows: ToolbarDiagnosticRow[]; +} + +export interface ToolbarViewModel { + newChatDisabled: boolean; + chatActionsOpen: boolean; + historyOpen: boolean; + statusPanelOpen: boolean; + rateLimit: RateLimitSummary | null; + configSections: RuntimeConfigSection[]; + openPanel: "history" | "chat-actions" | "status" | null; + threads: ToolbarThreadRow[]; + connectLabel: string; + diagnostics: ToolbarDiagnosticSection[]; +} + +export interface ToolbarViewModelInput { + state: ChatState; + snapshot: RuntimeSnapshot; + connected: boolean; + turnBusy: boolean; + vaultPath: string; + configuredCommand: string; + archiveConfirmThreadId: string | null; + archiveExportEnabled: boolean; + renameState: (threadId: string) => ToolbarThreadRow["rename"]; +} + +export interface ConnectionDiagnosticsModelInput { + state: ChatState; + connected: boolean; + configuredCommand: string; +} export function toolbarViewModel(input: ToolbarViewModelInput): ToolbarViewModel { const { state, snapshot } = input; - const limit = rateLimitSummary(snapshot); + const limit = rateLimitSummary(snapshot, Date.now()); const historyOpen = state.ui.toolbarPanel === "history"; const chatActionsOpen = state.ui.toolbarPanel === "chat-actions"; const statusPanelOpen = state.ui.toolbarPanel === "status-panel"; diff --git a/src/features/chat/panel/view-model/types.ts b/src/features/chat/panel/view-model/types.ts deleted file mode 100644 index efd6fd0b..00000000 --- a/src/features/chat/panel/view-model/types.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { ReasoningEffort } from "../../../../domain/catalog/metadata"; -import type { RuntimeSnapshot } from "../../runtime/model"; -import type { RuntimeConfigSection, RateLimitSummary } from "../../runtime/status-summary"; -import type { ChatState } from "../../state/reducer"; - -export interface ComposerMetaViewModel { - fatal: string | null; - context: ComposerContextMeterViewModel; - statusSummary: string; - model: string; - effort: string | null; - planActive: boolean; - autoReviewActive: boolean; - fastActive: boolean; - modelChoices?: RuntimeChoice[]; - effortChoices?: RuntimeChoice[]; -} - -export interface RuntimeChoice { - label: string; - selected?: boolean; - disabled?: boolean; - meta?: string; - onClick: () => void; -} - -export interface ComposerContextMeterCellViewModel { - text: string; - placeholder: boolean; -} - -export interface ComposerContextMeterViewModel { - cells: ComposerContextMeterCellViewModel[]; - percent: string; -} - -export interface ToolbarThreadRow { - title: string; - threadId: string; - selected: boolean; - disabled: boolean; - canArchive: boolean; - archiveConfirm?: { active: boolean; defaultSaveMarkdown: boolean }; - rename: { - draft: string; - generating: boolean; - } | null; -} - -interface ToolbarDiagnosticRow { - label: string; - value: string; - level?: "normal" | "warning" | "error"; -} - -export interface ToolbarDiagnosticSection { - title: string; - rows: ToolbarDiagnosticRow[]; -} - -export interface ToolbarViewModel { - newChatDisabled: boolean; - chatActionsOpen: boolean; - historyOpen: boolean; - statusPanelOpen: boolean; - rateLimit: RateLimitSummary | null; - configSections: RuntimeConfigSection[]; - openPanel: "history" | "chat-actions" | "status" | null; - threads: ToolbarThreadRow[]; - connectLabel: string; - diagnostics: ToolbarDiagnosticSection[]; -} - -export interface ToolbarViewModelInput { - state: ChatState; - snapshot: RuntimeSnapshot; - connected: boolean; - turnBusy: boolean; - vaultPath: string; - configuredCommand: string; - archiveConfirmThreadId: string | null; - archiveExportEnabled: boolean; - renameState: (threadId: string) => ToolbarThreadRow["rename"]; -} - -export interface ConnectionDiagnosticsModelInput { - state: ChatState; - connected: boolean; - configuredCommand: string; -} - -export interface RuntimeComposerChoicesInput { - state: ChatState; - snapshot: RuntimeSnapshot; - requestModel: (model: string) => void; - requestReasoningEffort: (effort: ReasoningEffort) => void; - resetReasoningEffortToConfig: () => void; -} - -export interface StatusSummaryLinesInput { - activeThreadId: ChatState["activeThread"]["id"]; - snapshot: RuntimeSnapshot; -} - -export interface ModelStatusLinesInput { - runtimeConfig: ChatState["connection"]["runtimeConfig"]; - requestedModel: ChatState["runtime"]["requestedModel"]; - snapshot: RuntimeSnapshot; - collaborationModeLabel: string; -} - -export interface EffortStatusLinesInput { - runtimeConfig: ChatState["connection"]["runtimeConfig"]; - requestedReasoningEffort: ChatState["runtime"]["requestedReasoningEffort"]; - snapshot: RuntimeSnapshot; -} - -export interface RestoredThreadTitleSnapshot { - threadId: string; - title: string | null; - explicitName: string | null; -} diff --git a/src/features/chat/protocol/display-items.ts b/src/features/chat/protocol/display-items.ts index 527b4861..26f2ce4d 100644 --- a/src/features/chat/protocol/display-items.ts +++ b/src/features/chat/protocol/display-items.ts @@ -1,4 +1,4 @@ -import type { DisplayDetailSection, DisplayFileChange, DisplayFileMention, DisplayItem } from "../display/types"; +import type { DisplayDetailSection, DisplayFileChange, DisplayFileMention, DisplayItem, ExecutionState } from "../display/types"; import type { CodexInput, CodexInputItem } from "../../../app-server/request-input"; import type { AppServerFileUpdateChange, AppServerThreadItem, AppServerTurn } from "../../../app-server/turn-model"; import { definedProp, truncate } from "../../../utils"; @@ -7,13 +7,6 @@ import { appServerUserItemText } from "../../../app-server/turn-model"; import { agentDisplayItem } from "../display/agent"; import { pathRelativeToRoot } from "../display/paths"; import { normalizeProposedPlanMarkdown } from "../display/plan"; -import { - commandExecutionState, - dynamicToolCallExecutionState, - imageGenerationExecutionState, - mcpToolCallExecutionState, - patchApplyExecutionState, -} from "../display/state"; import { bodyDetail, compactToolSummary, @@ -42,6 +35,28 @@ type ReviewModeItem = | Extract; type ContextCompactionItem = Extract; type TextRange = [number, number]; +type DisplayExecutionState = Exclude; +type ExecutionStateByStatus = Readonly>; + +const COMMAND_STATES = { + inProgress: "running", + completed: "completed", + failed: "failed", + declined: "failed", +} as const satisfies ExecutionStateByStatus; + +const PATCH_STATES = { + inProgress: "running", + completed: "completed", + failed: "failed", + declined: "failed", +} as const satisfies ExecutionStateByStatus; + +const STANDARD_TOOL_STATES = { + inProgress: "running", + completed: "completed", + failed: "failed", +} as const satisfies ExecutionStateByStatus; export function displayItemsFromTurns(turns: readonly AppServerTurn[]): DisplayItem[] { const sortedTurns = [...turns].sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0)); @@ -602,3 +617,38 @@ function pathRelativeToWorkspace(path: string, workspaceRoot?: string | null): s function assertNever(_item: never): null { return null; } + +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); +} + +function standardToolCallExecutionState(status: string): ExecutionState { + return executionStateFromStatus(status, STANDARD_TOOL_STATES); +} + +function executionStateFromStatus(status: string, states: ExecutionStateByStatus): ExecutionState { + return states[status] ?? null; +} diff --git a/src/features/chat/protocol/requests/approval.ts b/src/features/chat/protocol/requests/approval.ts index f3cdc5e6..be7466cf 100644 --- a/src/features/chat/protocol/requests/approval.ts +++ b/src/features/chat/protocol/requests/approval.ts @@ -1,4 +1,5 @@ -import { addOptional, nonEmptyString, permissionRows } from "../../display/permission-details"; +import { jsonPreview } from "../../../../utils"; +import { permissionRows } from "../../display/permission-rows"; import type { RequestId } from "./model"; type SimpleApprovalDecision = "accept" | "acceptForSession" | "decline" | "cancel"; @@ -315,3 +316,23 @@ function grantedPermissions(requested: PermissionsApprovalParams["permissions"]) if (requested.fileSystem) granted.fileSystem = requested.fileSystem; return granted; } + +function addOptional(rows: { key: string; value: string }[], key: string, value: unknown): void { + if (value === null || value === undefined) return; + if (Array.isArray(value) && value.length === 0) return; + rows.push({ key, value: stringValue(value) }); +} + +function stringValue(value: unknown, fallback = ""): string { + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value); + if (Array.isArray(value) && value.every((item) => typeof item === "string" || typeof item === "number" || typeof item === "boolean")) { + return value.join("\n"); + } + if (value === null || value === undefined) return fallback; + return jsonPreview(value); +} + +function nonEmptyString(value: unknown): string | null { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; +} diff --git a/src/features/chat/turn-diff/model.ts b/src/features/chat/turn-diff/model.ts new file mode 100644 index 00000000..c4cbe651 --- /dev/null +++ b/src/features/chat/turn-diff/model.ts @@ -0,0 +1,30 @@ +export interface ChatTurnDiffViewState { + threadId: string; + turnId: string; + cwd: string | null; + files: string[]; + diff: string; +} + +export type PersistedChatTurnDiffViewState = Omit; + +export function persistedChatTurnDiffViewState(state: ChatTurnDiffViewState): PersistedChatTurnDiffViewState { + return { + threadId: state.threadId, + turnId: state.turnId, + cwd: state.cwd, + files: [...state.files], + }; +} + +export function isPersistedChatTurnDiffViewState(value: unknown): value is PersistedChatTurnDiffViewState { + if (!value || typeof value !== "object") return false; + const record = value as Partial; + return ( + typeof record.threadId === "string" && + typeof record.turnId === "string" && + (typeof record.cwd === "string" || record.cwd === null) && + Array.isArray(record.files) && + record.files.every((file) => typeof file === "string") + ); +} diff --git a/src/features/chat/ui/turn-diff.tsx b/src/features/chat/turn-diff/render.tsx similarity index 77% rename from src/features/chat/ui/turn-diff.tsx rename to src/features/chat/turn-diff/render.tsx index 59312037..d7ff9c2a 100644 --- a/src/features/chat/ui/turn-diff.tsx +++ b/src/features/chat/turn-diff/render.tsx @@ -6,42 +6,12 @@ import { displayDiffLines } from "../../../shared/diff/unified"; import { IconButton } from "../../../shared/ui/components"; import { renderUiRoot } from "../../../shared/ui/ui-root"; import { shortThreadId } from "../../../utils"; - -export interface ChatTurnDiffViewState { - threadId: string; - turnId: string; - cwd: string | null; - files: string[]; - diff: string; -} - -export type PersistedChatTurnDiffViewState = Omit; +import type { ChatTurnDiffViewState, PersistedChatTurnDiffViewState } from "./model"; export interface ChatTurnDiffViewActions { copyDiff?: () => void; } -export function persistedChatTurnDiffViewState(state: ChatTurnDiffViewState): PersistedChatTurnDiffViewState { - return { - threadId: state.threadId, - turnId: state.turnId, - cwd: state.cwd, - files: [...state.files], - }; -} - -export function isPersistedChatTurnDiffViewState(value: unknown): value is PersistedChatTurnDiffViewState { - if (!value || typeof value !== "object") return false; - const record = value as Partial; - return ( - typeof record.threadId === "string" && - typeof record.turnId === "string" && - (typeof record.cwd === "string" || record.cwd === null) && - Array.isArray(record.files) && - record.files.every((file) => typeof file === "string") - ); -} - export function renderChatTurnDiffView( parent: HTMLElement, state: ChatTurnDiffViewState | null, diff --git a/src/features/chat/chat-turn-diff-view.ts b/src/features/chat/turn-diff/view.ts similarity index 87% rename from src/features/chat/chat-turn-diff-view.ts rename to src/features/chat/turn-diff/view.ts index 687ee477..4b760e6b 100644 --- a/src/features/chat/chat-turn-diff-view.ts +++ b/src/features/chat/turn-diff/view.ts @@ -1,15 +1,15 @@ import { ItemView, type ViewStateResult } from "obsidian"; -import { VIEW_TYPE_CODEX_TURN_DIFF } from "../../constants"; -import { copyTextWithNotice } from "../../shared/ui/clipboard"; -import { unmountUiRoot } from "../../shared/ui/ui-root"; +import { VIEW_TYPE_CODEX_TURN_DIFF } from "../../../constants"; +import { copyTextWithNotice } from "../../../shared/ui/clipboard"; +import { unmountUiRoot } from "../../../shared/ui/ui-root"; import { isPersistedChatTurnDiffViewState, persistedChatTurnDiffViewState, - renderChatTurnDiffView, type PersistedChatTurnDiffViewState, type ChatTurnDiffViewState, -} from "./ui/turn-diff"; +} from "./model"; +import { renderChatTurnDiffView } from "./render"; export class CodexChatTurnDiffView extends ItemView { private metadata: PersistedChatTurnDiffViewState | null = null; diff --git a/src/features/chat/ui/composer.tsx b/src/features/chat/ui/composer.tsx index 8e32dabf..b385e91b 100644 --- a/src/features/chat/ui/composer.tsx +++ b/src/features/chat/ui/composer.tsx @@ -3,7 +3,7 @@ import type { ButtonHTMLAttributes, ComponentChild as UiNode, Ref } from "preact import { useLayoutEffect, useRef, useState } from "preact/hooks"; import type { ComposerSuggestion } from "../conversation/composer/suggestions"; -import type { ComposerMetaViewModel, RuntimeChoice } from "../panel/view-model/types"; +import type { ComposerMetaViewModel, RuntimeChoice } from "../panel/view-model/composer"; import { IconButton } from "../../../shared/ui/components"; import { syncTextareaHeight } from "../../../shared/ui/textarea-autogrow"; diff --git a/src/features/chat/ui/message-stream/context.ts b/src/features/chat/ui/message-stream/context.ts index 9fb17023..7a84fc4b 100644 --- a/src/features/chat/ui/message-stream/context.ts +++ b/src/features/chat/ui/message-stream/context.ts @@ -4,7 +4,7 @@ import type { ChatTurnLifecycleState } from "../../state/reducer"; import type { PendingRequestSnapshot } from "../../state/selectors"; import type { DisplayItem } from "../../display/types"; import type { PendingRequestMessageActions } from "../pending-request-message"; -import type { ChatTurnDiffViewState } from "../turn-diff"; +import type { ChatTurnDiffViewState } from "../../turn-diff/model"; export interface MessageStreamBlock { key: string; diff --git a/src/features/chat/ui/message-stream/message-item.tsx b/src/features/chat/ui/message-stream/message-item.tsx index e75d9ed7..11125090 100644 --- a/src/features/chat/ui/message-stream/message-item.tsx +++ b/src/features/chat/ui/message-stream/message-item.tsx @@ -1,8 +1,7 @@ import { type ComponentChild as UiNode, type Ref } from "preact"; import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks"; -import { executionState } from "../../display/state"; -import type { DisplayItem } from "../../display/types"; +import type { DisplayItem, ExecutionState } from "../../display/types"; import { MESSAGE_CONTENT_RENDERED_EVENT } from "../message-content-events"; import type { MessageContentContext, MessageItemContext, RenderableTextItem } from "./context"; import { MessageRole } from "./message-actions"; @@ -18,7 +17,7 @@ function MessageItem({ item, context }: { item: RenderableTextItem; context: Mes const collapsible = isCollapsibleUserMessage(item); const details = "details" in item ? item.details : undefined; return ( -
+
{collapsible ? ( @@ -168,7 +167,7 @@ function contentRenderMode(item: RenderableTextItem): "markdown" | "text" { return item.messageKind === "proposedPlan" && item.messageState === "streaming" ? "text" : "markdown"; } -function executionClassName(state: ReturnType): string { +function executionClassName(state: ExecutionState): string { return state ? ` codex-panel__execution codex-panel__execution--${state}` : ""; } diff --git a/src/features/chat/ui/message-stream/context-port.ts b/src/features/chat/ui/message-stream/ports.ts similarity index 95% rename from src/features/chat/ui/message-stream/context-port.ts rename to src/features/chat/ui/message-stream/ports.ts index 6925c016..910dec73 100644 --- a/src/features/chat/ui/message-stream/context-port.ts +++ b/src/features/chat/ui/message-stream/ports.ts @@ -1,5 +1,5 @@ import type { ChatAction, ChatState } from "../../state/reducer"; -import type { ChatMessageStreamActionPort, ChatMessageStreamContextPort, ChatMessageStreamRequestPort } from "./context-builder"; +import type { ChatMessageStreamActionPort, ChatMessageStreamContextPort, ChatMessageStreamRequestPort } from "./view-model"; export interface MessageStreamContextPortOptions { vaultPath: string; diff --git a/src/features/chat/ui/message-stream/render-state.ts b/src/features/chat/ui/message-stream/render-model.ts similarity index 91% rename from src/features/chat/ui/message-stream/render-state.ts rename to src/features/chat/ui/message-stream/render-model.ts index eaa9ba16..a4ab747e 100644 --- a/src/features/chat/ui/message-stream/render-state.ts +++ b/src/features/chat/ui/message-stream/render-model.ts @@ -1,7 +1,7 @@ import type { ChatState } from "../../state/reducer"; import type { MessageStreamScrollIntent, MessageStreamVirtualizerHandle } from "../message-virtualizer"; -import { messageStreamBlocks } from "./blocks"; -import { createMessageStreamContext, type ChatMessageStreamContextPort } from "./context-builder"; +import { messageStreamBlocks } from "./stream-blocks"; +import { createMessageStreamContext, type ChatMessageStreamContextPort } from "./view-model"; import type { MessageStreamRenderState } from "./render"; export interface MessageStreamRenderStateOptions { diff --git a/src/features/chat/ui/message-stream/renderer.ts b/src/features/chat/ui/message-stream/renderer.ts index 92c3c53a..7af19837 100644 --- a/src/features/chat/ui/message-stream/renderer.ts +++ b/src/features/chat/ui/message-stream/renderer.ts @@ -5,11 +5,11 @@ import { copyTextWithNotice } from "../../../../shared/ui/clipboard"; import type { ChatAction, ChatState, ChatStateStore } from "../../state/reducer"; import type { ComposerBoundaryScrollAction } from "../../conversation/composer/boundary-scroll"; import type { MessageStreamScrollIntent, MessageStreamVirtualizerHandle } from "../message-virtualizer"; -import type { ChatMessageStreamActionPort, ChatMessageStreamContextPort, ChatMessageStreamRequestPort } from "./context-builder"; -import { createMessageStreamContextPort } from "./context-port"; +import type { ChatMessageStreamActionPort, ChatMessageStreamContextPort, ChatMessageStreamRequestPort } from "./view-model"; +import { createMessageStreamContextPort } from "./ports"; import { MarkdownMessageRenderer } from "./markdown-renderer"; import { messageStreamBlocksNode, type MessageStreamRenderState } from "./render"; -import { createMessageStreamRenderState } from "./render-state"; +import { createMessageStreamRenderState } from "./render-model"; interface ChatMessageRendererObsidianPort { app: App; diff --git a/src/features/chat/ui/message-stream/blocks.tsx b/src/features/chat/ui/message-stream/stream-blocks.tsx similarity index 100% rename from src/features/chat/ui/message-stream/blocks.tsx rename to src/features/chat/ui/message-stream/stream-blocks.tsx diff --git a/src/features/chat/ui/message-stream/context-builder.ts b/src/features/chat/ui/message-stream/view-model.ts similarity index 98% rename from src/features/chat/ui/message-stream/context-builder.ts rename to src/features/chat/ui/message-stream/view-model.ts index 81e94519..54cc8f07 100644 --- a/src/features/chat/ui/message-stream/context-builder.ts +++ b/src/features/chat/ui/message-stream/view-model.ts @@ -9,7 +9,7 @@ import { isRollbackCandidateItem, rollbackCandidateFromItems, } from "../../display/action-candidates"; -import type { ChatTurnDiffViewState } from "../turn-diff"; +import type { ChatTurnDiffViewState } from "../../turn-diff/model"; import type { PendingRequestMessageActions } from "../pending-request-message"; import type { MessageStreamContext } from "./context"; diff --git a/src/features/chat/ui/regions.tsx b/src/features/chat/ui/regions.tsx index 721b2099..dfa5f102 100644 --- a/src/features/chat/ui/regions.tsx +++ b/src/features/chat/ui/regions.tsx @@ -2,7 +2,7 @@ import type { ComponentChild as UiNode } from "preact"; import { useComputed } from "@preact/signals"; import { useEffect, useState } from "preact/hooks"; -import { chatPanelGoalProps, chatPanelToolbarViewModel } from "./region-view-models"; +import { chatPanelGoalProps, chatPanelToolbarViewModel } from "../panel/region-view-models"; import { goalBannerNode } from "./goal-banner"; import { toolbarNode } from "./toolbar"; import type { ChatPanelGoalPorts, ChatPanelToolbarPorts } from "../panel/ui-ports"; diff --git a/src/features/chat/ui/toolbar.tsx b/src/features/chat/ui/toolbar.tsx index b3deb710..5fdf558f 100644 --- a/src/features/chat/ui/toolbar.tsx +++ b/src/features/chat/ui/toolbar.tsx @@ -1,9 +1,9 @@ import type { ButtonHTMLAttributes, ComponentChild as UiNode, TargetedKeyboardEvent } from "preact"; import { useLayoutEffect, useRef } from "preact/hooks"; -import type { RuntimeConfigSection, RateLimitSummary } from "../runtime/status-summary"; +import type { RuntimeConfigSection, RateLimitSummary } from "../display/runtime-status"; import { IconButton } from "../../../shared/ui/components"; -import type { ToolbarDiagnosticSection, ToolbarThreadRow, ToolbarViewModel } from "../panel/view-model/types"; +import type { ToolbarDiagnosticSection, ToolbarThreadRow, ToolbarViewModel } from "../panel/view-model/toolbar"; type ButtonProps = ButtonHTMLAttributes & { disabled?: boolean | undefined; diff --git a/src/features/chat/ui/work-items.tsx b/src/features/chat/ui/work-items.tsx index ab02bfcd..ad16290a 100644 --- a/src/features/chat/ui/work-items.tsx +++ b/src/features/chat/ui/work-items.tsx @@ -1,18 +1,18 @@ import type { ComponentChild as UiNode } from "preact"; import { useLayoutEffect, useState } from "preact/hooks"; -import { activeAgentRunSummary } from "../display/agent"; -import { executionState } from "../display/state"; +import { activeAgentRunSummary, agentActivityMetaLabel, agentMessagePreview, agentRunSummaryLabel } from "../display/agent"; +import { taskStatusMarker } from "../display/plan"; import type { AgentDisplayItem, AgentRunSummary, AgentRunSummaryAgent, ContextCompactionDisplayItem, DisplayItem, + ExecutionState, ReasoningDisplayItem, TaskProgressDisplayItem, } from "../display/types"; -import { agentActivityMetaLabel, agentMessagePreview, agentRunSummaryLabel, taskStatusMarker } from "../display/labels"; import { activeTurnId, type ChatTurnLifecycleState } from "../state/reducer"; import { createWorkMessageClassName } from "./work-message"; import { shortThreadId, truncate } from "../../../utils"; @@ -59,7 +59,7 @@ function AgentRunSummaryItem({ summary }: { summary: AgentRunSummary }): UiNode function TaskProgressItem({ item }: { item: TaskProgressDisplayItem }): UiNode { return ( - + {item.explanation ?
{item.explanation}
: null} {item.steps.length === 0 ? (
Plan updated
@@ -93,7 +93,11 @@ function AgentItem({ item, context }: { item: AgentDisplayItem; context: WorkIte setDetailsOpen(context.openDetails.has(detailsKey)); }, [context.openDetails, detailsKey]); return ( - +
{agentSummaryText(item)}
; + state: ExecutionState; children: UiNode; }): UiNode { const classes = [createWorkMessageClassName(className), state ? `codex-panel__execution codex-panel__execution--${state}` : ""] @@ -240,7 +244,7 @@ function isLongAgentMessage(message: string): boolean { function isReasoningActive(item: ReasoningDisplayItem, context: WorkItemContext): boolean { const activeTurn = workItemsActiveTurnId(context); if (!activeTurn || item.turnId !== activeTurn) return false; - if (executionState(item) === "completed") return false; + if (item.executionState === "completed") return false; const latestActiveTurnItem = [...context.displayItems].reverse().find((candidate) => candidate.turnId === activeTurn); return latestActiveTurnItem?.id === item.id; } diff --git a/src/features/chat/view.ts b/src/features/chat/view.ts index e23e670b..e1930652 100644 --- a/src/features/chat/view.ts +++ b/src/features/chat/view.ts @@ -16,7 +16,7 @@ import { effortStatusLines as buildEffortStatusLines, modelStatusLines as buildModelStatusLines, statusSummaryLines as buildStatusSummaryLines, -} from "./panel/view-model/runtime"; +} from "./display/runtime-status"; import { runtimeSnapshotForChatState } from "./runtime/snapshot"; import { activeThreadTitle as buildActiveThreadTitle, chatViewDisplayTitle } from "./panel/view-model/thread-title"; import { connectionDiagnosticsModel } from "./panel/view-model/toolbar"; @@ -26,7 +26,11 @@ import { ChatMessageScrollIntentController } from "./panel/message-scroll-intent import type { ChatControllerCompositionPorts } from "./panel/controller-ports"; import { createChatViewControllers, type ChatViewControllers } from "./panel/composition"; import type { ChatPanelComposerPorts, ChatPanelGoalPorts, ChatPanelStatePort, ChatPanelToolbarPorts } from "./panel/ui-ports"; -import { chatPanelComposerMetaViewModel, chatPanelComposerPlaceholder, chatPanelPendingRequestsSignature } from "./ui/region-view-models"; +import { + chatPanelComposerMetaViewModel, + chatPanelComposerPlaceholder, + chatPanelPendingRequestsSignature, +} from "./panel/region-view-models"; import { chatPanelComposerRegionNode, chatPanelGoalRegionNode, @@ -639,6 +643,7 @@ export class CodexChatView extends ItemView { return buildStatusSummaryLines({ activeThreadId: this.state.activeThread.id, snapshot: this.runtimeSnapshot(), + nowMs: Date.now(), }); } diff --git a/src/main.ts b/src/main.ts index 58bcacd7..d849a325 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,14 +4,14 @@ import { VIEW_TYPE_CODEX_PANEL, VIEW_TYPE_CODEX_THREADS, VIEW_TYPE_CODEX_TURN_DI import { registerSelectionRewriteCommand } from "./features/selection-rewrite/command"; import { CodexChatView } from "./features/chat/view"; import type { CodexChatHost } from "./features/chat/chat-host"; -import { CodexChatTurnDiffView } from "./features/chat/chat-turn-diff-view"; +import { CodexChatTurnDiffView } from "./features/chat/turn-diff/view"; import { openThreadPicker, type ThreadPickerHost } from "./features/thread-picker/modal"; import { CodexThreadsView, type CodexThreadsHost } from "./features/threads-view/view"; import { SharedAppServerCache } from "./app-server/shared-cache"; import type { SharedAppServerCacheContext } from "./app-server/shared-cache-state"; import { DEFAULT_SETTINGS, getVaultPath, normalizeSettings, settingsMatchNormalizedData, type CodexPanelSettings } from "./settings/model"; import { CodexPanelSettingTab, type CodexPanelSettingTabHost } from "./settings/tab"; -import { persistedChatTurnDiffViewState, type ChatTurnDiffViewState } from "./features/chat/ui/turn-diff"; +import { persistedChatTurnDiffViewState, type ChatTurnDiffViewState } from "./features/chat/turn-diff/model"; import { WorkspacePanelCoordinator } from "./workspace/panel-coordinator"; import { ThreadSurfaceCoordinator } from "./workspace/thread-surface-coordinator"; diff --git a/tests/features/chat/display/display-model.test.ts b/tests/features/chat/display/display-model.test.ts index 24dd3b22..cc1bb3e3 100644 --- a/tests/features/chat/display/display-model.test.ts +++ b/tests/features/chat/display/display-model.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { activeAgentRunSummary } from "../../../../src/features/chat/display/agent"; +import { activeAgentRunSummary, collabAgentStateExecutionState } from "../../../../src/features/chat/display/agent"; import { displayBlocksForItems } from "../../../../src/features/chat/display/blocks"; import { appendAssistantDelta, @@ -10,20 +10,20 @@ import { appendToolOutput, upsertDisplayItem, } from "../../../../src/features/chat/display/stream-updates"; -import { normalizeProposedPlanMarkdown, planProgressDisplayItem } from "../../../../src/features/chat/display/plan"; -import { pathRelativeToRoot } from "../../../../src/features/chat/display/paths"; -import { permissionRows } from "../../../../src/features/chat/display/permission-details"; -import { createAutoReviewResultItem, createReviewResultItem } from "../../../../src/features/chat/display/review"; import { - autoReviewExecutionState, - collabAgentStateExecutionState, + normalizeProposedPlanMarkdown, + planProgressDisplayItem, + taskProgressExecutionState, +} from "../../../../src/features/chat/display/plan"; +import { pathRelativeToRoot } from "../../../../src/features/chat/display/paths"; +import { permissionRows } from "../../../../src/features/chat/display/permission-rows"; +import { autoReviewExecutionState, createAutoReviewResultItem, createReviewResultItem } from "../../../../src/features/chat/display/review"; +import { commandExecutionState, dynamicToolCallExecutionState, - executionState, mcpToolCallExecutionState, patchApplyExecutionState, - taskProgressExecutionState, -} from "../../../../src/features/chat/display/state"; +} from "../../../../src/features/chat/protocol/display-items"; import { displayItemFromThreadItem, displayItemsFromTurns } from "../../../../src/features/chat/protocol/display-items"; import { referencedThreadPrompt } from "../../../../src/domain/threads/reference"; import type { DisplayItem } from "../../../../src/features/chat/display/types"; @@ -1555,17 +1555,16 @@ describe("execution state uses typed status adapters before rendered text", () = it("does not infer unknown status strings with broad matching", () => { expect(patchApplyExecutionState("done_with_errors")).toBeNull(); - expect( - executionState({ - id: "c1", - kind: "command", - role: "tool", - text: "Command", - command: "npm test", - cwd: "/vault", - status: "done_with_errors", - }), - ).toBeNull(); + const item: DisplayItem = { + id: "c1", + kind: "command", + role: "tool", + text: "Command", + command: "npm test", + cwd: "/vault", + status: "done_with_errors", + }; + expect(item.executionState ?? null).toBeNull(); }); it("does not overwrite streamed output with an empty completed item", () => { diff --git a/tests/features/chat/ui/message-stream/context-port.test.ts b/tests/features/chat/ui/message-stream/ports.test.ts similarity index 98% rename from tests/features/chat/ui/message-stream/context-port.test.ts rename to tests/features/chat/ui/message-stream/ports.test.ts index 8e01236b..58696232 100644 --- a/tests/features/chat/ui/message-stream/context-port.test.ts +++ b/tests/features/chat/ui/message-stream/ports.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { createChatState, type ChatAction } from "../../../../../src/features/chat/state/reducer"; -import { createMessageStreamContextPort } from "../../../../../src/features/chat/ui/message-stream/context-port"; +import { createMessageStreamContextPort } from "../../../../../src/features/chat/ui/message-stream/ports"; describe("message stream context port", () => { it("closes other fork action details before opening a fork action detail", () => { diff --git a/tests/features/chat/ui/message-stream/test-helpers.tsx b/tests/features/chat/ui/message-stream/test-helpers.tsx index a0491a8c..a86736f9 100644 --- a/tests/features/chat/ui/message-stream/test-helpers.tsx +++ b/tests/features/chat/ui/message-stream/test-helpers.tsx @@ -6,7 +6,7 @@ import type { PendingApproval } from "../../../../../src/features/chat/protocol/ import type { PendingUserInput } from "../../../../../src/features/chat/protocol/requests/user-input"; import { pendingRequestMessageNode, type PendingRequestMessageActions } from "../../../../../src/features/chat/ui/pending-request-message"; import type { ChatTurnLifecycleState } from "../../../../../src/features/chat/state/reducer"; -import { messageStreamBlocks as rawMessageStreamBlocks } from "../../../../../src/features/chat/ui/message-stream/blocks"; +import { messageStreamBlocks as rawMessageStreamBlocks } from "../../../../../src/features/chat/ui/message-stream/stream-blocks"; import type { MessageStreamBlock } from "../../../../../src/features/chat/ui/message-stream/context"; import { messageStreamBlocksNode } from "../../../../../src/features/chat/ui/message-stream/render"; import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/ui/ui-root"; diff --git a/tests/features/chat/ui/renderers/composer.test.ts b/tests/features/chat/ui/renderers/composer.test.ts index e28ba730..5de3de32 100644 --- a/tests/features/chat/ui/renderers/composer.test.ts +++ b/tests/features/chat/ui/renderers/composer.test.ts @@ -9,7 +9,7 @@ import { type ComposerCallbacks, } from "../../../../../src/features/chat/ui/composer"; import type { ComposerSuggestion } from "../../../../../src/features/chat/conversation/composer/suggestions"; -import type { ComposerMetaViewModel } from "../../../../../src/features/chat/panel/view-model/types"; +import type { ComposerMetaViewModel } from "../../../../../src/features/chat/panel/view-model/composer"; import { renderUiRoot } from "../../../../../src/shared/ui/ui-root"; import { waitForAsyncWork } from "../../../../support/async"; import { changeInputValue, composerSuggestionScrollFixture, installObsidianDomShims } from "../../../../support/dom"; diff --git a/tests/features/chat/ui/renderers/toolbar.test.ts b/tests/features/chat/ui/renderers/toolbar.test.ts index 4811cc6e..9b3a34a4 100644 --- a/tests/features/chat/ui/renderers/toolbar.test.ts +++ b/tests/features/chat/ui/renderers/toolbar.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest"; -import type { ToolbarViewModel } from "../../../../../src/features/chat/panel/view-model/types"; +import type { ToolbarViewModel } from "../../../../../src/features/chat/panel/view-model/toolbar"; import { toolbarNode } from "../../../../../src/features/chat/ui/toolbar"; import { renderUiRoot } from "../../../../../src/shared/ui/ui-root"; import { changeInputValue, installObsidianDomShims } from "../../../../support/dom"; diff --git a/tests/features/chat/ui/renderers/turn-diff.test.ts b/tests/features/chat/ui/renderers/turn-diff.test.ts index 25ca6c6e..962dd025 100644 --- a/tests/features/chat/ui/renderers/turn-diff.test.ts +++ b/tests/features/chat/ui/renderers/turn-diff.test.ts @@ -3,8 +3,9 @@ import { describe, expect, it, vi } from "vitest"; import type { WorkspaceLeaf } from "obsidian"; -import { CodexChatTurnDiffView } from "../../../../../src/features/chat/chat-turn-diff-view"; -import { persistedChatTurnDiffViewState, renderChatTurnDiffView } from "../../../../../src/features/chat/ui/turn-diff"; +import { persistedChatTurnDiffViewState } from "../../../../../src/features/chat/turn-diff/model"; +import { renderChatTurnDiffView } from "../../../../../src/features/chat/turn-diff/render"; +import { CodexChatTurnDiffView } from "../../../../../src/features/chat/turn-diff/view"; import { installObsidianDomShims } from "../../../../support/dom"; installObsidianDomShims(); diff --git a/tests/features/chat/view-model.test.ts b/tests/features/chat/view-model.test.ts index b2df01d6..63b41b0d 100644 --- a/tests/features/chat/view-model.test.ts +++ b/tests/features/chat/view-model.test.ts @@ -8,12 +8,8 @@ import { } from "../../../src/app-server/runtime-config"; import { createChatState } from "../../../src/features/chat/state/reducer"; import { composerMetaViewModel, composerPlaceholder } from "../../../src/features/chat/panel/view-model/composer"; -import { - effortStatusLines, - runtimeComposerChoices, - modelStatusLines, - statusSummaryLines, -} from "../../../src/features/chat/panel/view-model/runtime"; +import { effortStatusLines, modelStatusLines, statusSummaryLines } from "../../../src/features/chat/display/runtime-status"; +import { runtimeComposerChoices } from "../../../src/features/chat/panel/view-model/composer"; import { runtimeSnapshotForChatState } from "../../../src/features/chat/runtime/snapshot"; import { activeComposerThreadName, @@ -24,7 +20,7 @@ import { toolbarViewModel } from "../../../src/features/chat/panel/view-model/to import type { ChatState } from "../../../src/features/chat/state/reducer"; import type { ModelMetadata } from "../../../src/domain/catalog/metadata"; import type { Thread } from "../../../src/domain/threads/model"; -import { chatPanelComposerMetaViewModel, chatPanelGoalProps } from "../../../src/features/chat/ui/region-view-models"; +import { chatPanelComposerMetaViewModel, chatPanelGoalProps } from "../../../src/features/chat/panel/region-view-models"; import type { ChatPanelComposerPorts, ChatPanelGoalPorts } from "../../../src/features/chat/panel/ui-ports"; import type { ThreadGoal } from "../../../src/app-server/thread-goal"; @@ -186,7 +182,7 @@ describe("chat view model", () => { state.connection.availableModels = [modelFixture("gpt-5.5")]; const snapshot = runtimeSnapshotFixture(state); - expect(statusSummaryLines({ activeThreadId: state.activeThread.id, snapshot })[1]).toBe("Thread: thread-1"); + expect(statusSummaryLines({ activeThreadId: state.activeThread.id, snapshot, nowMs: 0 })[1]).toBe("Thread: thread-1"); expect( modelStatusLines({ runtimeConfig: state.connection.runtimeConfig, diff --git a/tests/runtime/runtime-settings.test.ts b/tests/runtime/runtime-settings.test.ts index 5a99af2f..a3e1f641 100644 --- a/tests/runtime/runtime-settings.test.ts +++ b/tests/runtime/runtime-settings.test.ts @@ -33,7 +33,7 @@ import { requestedTurnCollaborationModeSettings, serviceTierRequestForThreadStart, } from "../../src/features/chat/runtime/turn-settings"; -import { contextSummary, runtimeConfigSections, rateLimitSummary } from "../../src/features/chat/runtime/status-summary"; +import { contextSummary, runtimeConfigSections, rateLimitSummary } from "../../src/features/chat/display/runtime-status"; describe("runtime settings", () => { it("parses model overrides", () => { @@ -660,6 +660,7 @@ describe("runtime settings", () => { rateLimitReachedType: "rate_limit_reached", }, }), + 0, ), ).toMatchObject({ rows: [{ percent: 95, resetLabel: null }], @@ -678,6 +679,7 @@ describe("runtime settings", () => { rateLimitReachedType: null, }, }), + 0, ), ).toMatchObject({ rows: [