Organize chat display and panel view-model modules

This commit is contained in:
murashit 2026-06-12 10:28:07 +09:00
parent b98f8e6c1f
commit c7ecbf65bc
47 changed files with 534 additions and 537 deletions

View file

@ -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 = [

View file

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

View file

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

View file

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

View file

@ -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<ExecutionState, null>;
type ExecutionStateByStatus = Readonly<Record<string, DisplayExecutionState>>;
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;
}

View file

@ -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<string, number>): boolean {

View file

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

View file

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

View file

@ -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<ExecutionState, null>;
type ExecutionStateByStatus = Readonly<Record<string, DisplayExecutionState>>;
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*<proposed_plan>\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;
}

View file

@ -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<ExecutionState, null>;
type ExecutionStateByStatus = Readonly<Record<string, DisplayExecutionState>>;
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;
}

View file

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

View file

@ -1,105 +0,0 @@
import type { DisplayItem, ExecutionState } from "./types";
type DisplayExecutionState = Exclude<ExecutionState, null>;
type ExecutionStateByStatus = Readonly<Record<string, DisplayExecutionState>>;
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;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<AppServerThreadItem, { type: "exitedReviewMode" }>;
type ContextCompactionItem = Extract<AppServerThreadItem, { type: "contextCompaction" }>;
type TextRange = [number, number];
type DisplayExecutionState = Exclude<ExecutionState, null>;
type ExecutionStateByStatus = Readonly<Record<string, DisplayExecutionState>>;
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;
}

View file

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

View file

@ -0,0 +1,30 @@
export interface ChatTurnDiffViewState {
threadId: string;
turnId: string;
cwd: string | null;
files: string[];
diff: string;
}
export type PersistedChatTurnDiffViewState = Omit<ChatTurnDiffViewState, "diff">;
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<PersistedChatTurnDiffViewState>;
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")
);
}

View file

@ -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<ChatTurnDiffViewState, "diff">;
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<PersistedChatTurnDiffViewState>;
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,

View file

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

View file

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

View file

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

View file

@ -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 (
<div className={`${messageClass(item)}${executionClassName(executionState(item))}`}>
<div className={`${messageClass(item)}${executionClassName(item.executionState ?? null)}`}>
<MessageRole item={item} context={context} />
{collapsible ? (
<CollapsibleMessageContent item={item} context={context} />
@ -168,7 +167,7 @@ function contentRenderMode(item: RenderableTextItem): "markdown" | "text" {
return item.messageKind === "proposedPlan" && item.messageState === "streaming" ? "text" : "markdown";
}
function executionClassName(state: ReturnType<typeof executionState>): string {
function executionClassName(state: ExecutionState): string {
return state ? ` codex-panel__execution codex-panel__execution--${state}` : "";
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 (
<WorkMessage label="tasks" className="codex-panel__task-progress" state={executionState(item)}>
<WorkMessage label="tasks" className="codex-panel__task-progress" state={item.executionState ?? null}>
{item.explanation ? <div className="codex-panel__tool-summary">{item.explanation}</div> : null}
{item.steps.length === 0 ? (
<div className="codex-panel__tool-summary">Plan updated</div>
@ -93,7 +93,11 @@ function AgentItem({ item, context }: { item: AgentDisplayItem; context: WorkIte
setDetailsOpen(context.openDetails.has(detailsKey));
}, [context.openDetails, detailsKey]);
return (
<WorkMessage label="agent" className={`codex-panel__agent-activity${detailsOpen ? " is-open" : ""}`} state={executionState(item)}>
<WorkMessage
label="agent"
className={`codex-panel__agent-activity${detailsOpen ? " is-open" : ""}`}
state={item.executionState ?? null}
>
<div className="codex-panel__tool-summary codex-panel__agent-activity-summary">{agentSummaryText(item)}</div>
<details
className="codex-panel__output codex-panel__agent-details"
@ -169,7 +173,7 @@ function WorkMessage({
}: {
label: string;
className: string;
state: ReturnType<typeof executionState>;
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;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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: [