Reorganize chat display and thread modules

This commit is contained in:
murashit 2026-06-12 13:20:19 +09:00
parent 230f35627f
commit c516ccee6b
95 changed files with 718 additions and 714 deletions

View file

@ -103,8 +103,8 @@ const chatExternalDomBridgeFiles = [
"src/features/chat/ui/message-virtualizer.ts",
];
const chatPreactDomBridgeFiles = [
"src/features/chat/ui/message-stream/message-actions.tsx",
"src/features/chat/ui/message-stream/message-item.tsx",
"src/features/chat/ui/message-stream/text-item-actions.tsx",
"src/features/chat/ui/message-stream/text-item.tsx",
"src/features/chat/ui/message-stream/render.tsx",
"src/features/chat/ui/composer.tsx",
"src/features/chat/ui/goal-banner.tsx",

View file

@ -81,12 +81,12 @@ export function createChatControllerCompositionActions(
const status = {
set: ports.status.set,
addSystemMessage: (text: string) => {
ports.state.stateStore.dispatch({ type: "transcript/system-message-added", item: ports.state.systemItem(text) });
ports.state.stateStore.dispatch({ type: "transcript/system-item-added", item: ports.state.systemItem(text) });
render.now();
},
addStructuredSystemMessage: (text: string, details: DisplayDetailSection[]) => {
ports.state.stateStore.dispatch({
type: "transcript/system-message-added",
type: "transcript/system-item-added",
item: ports.state.structuredSystemItem(text, details),
});
render.now();

View file

@ -4,21 +4,21 @@ import type { ChatServerMetadataActions } from "./connection/server-actions/meta
import type { ChatServerThreadActions } from "./connection/server-actions/threads";
import type { ChatComposerController } from "./conversation/composer/controller";
import type { ChatInboundController } from "./protocol/inbound/controller";
import type { ChatThreadGoalActions } from "./threads/thread-goal-actions";
import type { GoalActions } from "./threads/goal-actions";
import { createChatRuntimeSettingsActions, type ChatRuntimeSettingsActions } from "./runtime/settings-actions";
import type { ChatThreadActions } from "./threads/thread-actions";
import type { ThreadHistoryController } from "./threads/thread-history-controller";
import type { ThreadRenameController } from "./threads/thread-rename-controller";
import type { ChatThreadActions } from "./threads/actions";
import type { HistoryController } from "./threads/history-controller";
import type { RenameController } from "./threads/rename-controller";
import type { ToolbarPanelController } from "./panel/regions/toolbar";
import type { ChatConnectionController } from "./connection/connection-controller";
import type { ChatReconnectActions } from "./connection/reconnect-actions";
import type { PendingRequestController } from "./conversation/pending-requests/controller";
import { rejectServerRequest, respondToServerRequest } from "./protocol/requests/server-request-responder";
import type { ComposerSubmissionActions } from "./conversation/turns/composer-submission-actions";
import type { RestoredThreadController } from "./threads/restored-thread-controller";
import type { ThreadIdentitySync } from "./threads/thread-identity-sync";
import type { ThreadResumeController } from "./threads/thread-resume-controller";
import type { ThreadSelectionActions } from "./threads/thread-selection-actions";
import type { RestorationController } from "./threads/restoration-controller";
import type { IdentitySync } from "./threads/identity-sync";
import type { ResumeController } from "./threads/resume-controller";
import type { SelectionActions } from "./threads/selection-actions";
import type { ChatViewRenderController } from "./panel/view-render-controller";
import type { ChatMessageRenderer } from "./ui/message-stream/renderer";
import type { ChatControllerCompositionPorts } from "./composition-ports";
@ -53,17 +53,17 @@ export interface ChatViewControllers {
diagnostics: ChatServerDiagnosticsActions;
};
thread: {
history: ThreadHistoryController;
resume: ThreadResumeController;
history: HistoryController;
resume: ResumeController;
actions: ChatThreadActions;
restored: RestoredThreadController;
identity: ThreadIdentitySync;
rename: ThreadRenameController;
selection: ThreadSelectionActions;
restoration: RestorationController;
identity: IdentitySync;
rename: RenameController;
selection: SelectionActions;
};
runtime: {
settings: ChatRuntimeSettingsActions;
goals: ChatThreadGoalActions;
goals: GoalActions;
};
requests: {
pending: PendingRequestController;
@ -105,7 +105,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
},
});
let connectionController: ChatConnectionController | null = null;
let threadSelection: ThreadSelectionActions | null = null;
let selection: SelectionActions | null = null;
let composerController: ChatComposerController | null = null;
const actions = createChatControllerCompositionActions(
{
@ -121,7 +121,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
ensureConnected: () => requireComposedController(connectionController, "connection controller").ensureConnected(),
refreshThreads: () => requireComposedController(connectionController, "connection controller").refreshThreads(),
refreshSkills: (forceReload) => requireComposedController(connectionController, "connection controller").refreshSkills(forceReload),
selectThread: (threadId) => requireComposedController(threadSelection, "thread selection actions").selectThread(threadId),
selectThread: (threadId) => requireComposedController(selection, "selection actions").selectThread(threadId),
setComposerText: (text) => {
requireComposedController(composerController, "composer controller").setDraft(text, { focus: true });
},
@ -182,8 +182,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
connection,
},
);
const { history, threadActions, goals, threadIdentity } = threadControllers;
const { restoredThread, threadResume, threadRename } = threadControllers;
const { history, actions: threadActions, goals, identity, restoration, resume, rename } = threadControllers;
const lifecycleActions = {
deferredTasks: ports.lifecycle.deferredTasks,
resumeWork: ports.lifecycle.resumeWork,
@ -210,10 +209,10 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
render: actions.render,
thread: {
restorePlaceholder: (restoredThreadState) => {
restoredThread.restore(restoredThreadState);
restoration.restore(restoredThreadState);
},
clearRestoredLifecycle: () => {
restoredThread.clear();
restoration.clear();
},
},
},
@ -221,7 +220,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
threadActions,
},
);
threadSelection = createThreadSelectionActionGroup(
selection = createThreadSelectionActionGroup(
{
plugin: {
focusThreadInOpenView: (threadId) => ports.plugin.focusThreadInOpenView(threadId),
@ -230,7 +229,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
stateStore: ports.state.stateStore,
},
thread: {
resumeThread: (threadId) => threadResume.resumeThread(threadId),
resumeThread: (threadId) => resume.resumeThread(threadId),
},
status: actions.status,
},
@ -239,7 +238,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
toolbarPanels.closeForThreadSelection();
},
},
).threadSelection;
).selection;
const { reconnectActions } = createChatReconnectControllerGroup(
{
state: {
@ -250,7 +249,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
render: actions.render,
status: actions.status,
thread: {
resumeThread: (threadId) => threadResume.resumeThread(threadId),
resumeThread: (threadId) => resume.resumeThread(threadId),
},
},
{
@ -309,7 +308,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
{
serverMetadata,
serverDiagnostics,
threadRename,
rename,
respondToServerRequest: (requestId, result) => respondToServerRequest(serverRequestHost, requestId, result),
rejectServerRequest: (requestId, code, message) => rejectServerRequest(serverRequestHost, requestId, code, message),
},
@ -331,7 +330,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
loadSharedThreadList: ports.thread.loadSharedThreadList,
refreshTabHeader: ports.thread.refreshTabHeader,
resetTurnPresence: (hadTurns) => {
threadRename.resetThreadTurnPresence(hadTurns);
rename.resetThreadTurnPresence(hadTurns);
},
},
status: actions.status,
@ -399,7 +398,7 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
selectThread: actions.thread.selectThread,
notifyIdentityChanged: ports.thread.notifyIdentityChanged,
resetTurnPresence: (hadTurns) => {
threadRename.resetThreadTurnPresence(hadTurns);
rename.resetThreadTurnPresence(hadTurns);
},
},
status: actions.status,
@ -423,7 +422,6 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
serverThreads,
runtimeSettings,
threadActions,
threadRename,
reconnectActions,
goals,
history,
@ -493,12 +491,12 @@ export function createChatViewControllers(ports: ChatControllerCompositionPorts)
},
thread: {
history,
resume: threadResume,
resume,
actions: threadActions,
restored: restoredThread,
identity: threadIdentity,
rename: threadRename,
selection: threadSelection,
restoration,
identity,
rename,
selection,
},
runtime: {
settings: runtimeSettings,

View file

@ -12,8 +12,8 @@ import { createChatServerThreadActions } from "./server-actions/threads";
import { ChatConnectionController } from "./connection-controller";
import { createChatReconnectActions } from "./reconnect-actions";
import type { rejectServerRequest, respondToServerRequest } from "../protocol/requests/server-request-responder";
import type { ChatThreadGoalActions } from "../threads/thread-goal-actions";
import type { ThreadRenameController } from "../threads/thread-rename-controller";
import type { GoalActions } from "../threads/goal-actions";
import type { RenameController } from "../threads/rename-controller";
import { ChatInboundController } from "../protocol/inbound/controller";
import type { ChatConnectionWorkTracker } from "../lifecycle";
@ -35,7 +35,7 @@ export function createChatServerActionControllers(
context: ChatServerActionControllerPorts,
refs: {
connection: ConnectionManager;
goals: ChatThreadGoalActions;
goals: GoalActions;
},
) {
const { plugin, runtime } = context;
@ -97,7 +97,7 @@ export function createChatInboundController(
refs: {
serverMetadata: ChatServerMetadataActions;
serverDiagnostics: ChatServerDiagnosticsActions;
threadRename: ThreadRenameController;
rename: RenameController;
respondToServerRequest: (requestId: Parameters<typeof respondToServerRequest>[1], result: unknown) => boolean;
rejectServerRequest: (requestId: Parameters<typeof rejectServerRequest>[1], code: number, message: string) => boolean;
},
@ -114,7 +114,7 @@ export function createChatInboundController(
refreshSkills: (forceReload) => void thread.refreshSkills(forceReload),
publishAppServerMetadata: thread.publishAppServerMetadataSnapshot,
maybeNameThread: (threadId, turnId, completedSummary) => {
refs.threadRename.maybeAutoNameThread(threadId, turnId, completedSummary);
refs.rename.maybeAutoNameThread(threadId, turnId, completedSummary);
},
notifyThreadArchived: plugin.notifyThreadArchived.bind(plugin),
notifyThreadRenamed: plugin.notifyThreadRenamed.bind(plugin),

View file

@ -11,7 +11,7 @@ import {
type McpServerStatusSummary,
} from "../../../../app-server/diagnostics";
import type { SharedAppServerMetadata } from "../../../../app-server/shared-cache-state";
import { mcpStatusLines as buildMcpStatusLines } from "../../display/diagnostics";
import { mcpStatusLines as buildMcpStatusLines } from "../../display/status/diagnostics";
import { cloneAppServerDiagnostics, type ChatServerActionHost } from "./host";
interface RefreshDiagnosticProbesOptions {

View file

@ -3,7 +3,7 @@ import type { Thread } from "../../../../domain/threads/model";
import type { RuntimeSnapshot } from "../../runtime/snapshot";
import { runtimeConfigOrDefault } from "../../runtime/effective";
import { serviceTierRequestForThreadStart } from "../../runtime/thread-settings";
import { resumedThreadActionFromAppServerResponse } from "../../threads/thread-resume";
import { resumedThreadActionFromAppServerResponse } from "../../threads/resume";
import type { ChatServerActionHost } from "./host";
import type { ChatState } from "../../state/reducer";

View file

@ -11,10 +11,9 @@ import { createComposerSubmissionActions } from "./turns/composer-submission-act
import { createPlanImplementationActions } from "./turns/plan-implementation-actions";
import { createSlashCommandActions } from "./turns/slash-command-actions";
import { TurnSubmissionController } from "./turns/turn-submission-controller";
import type { ChatThreadActions } from "../threads/thread-actions";
import type { ChatThreadGoalActions } from "../threads/thread-goal-actions";
import type { ThreadHistoryController } from "../threads/thread-history-controller";
import type { ThreadRenameController } from "../threads/thread-rename-controller";
import type { ChatThreadActions } from "../threads/actions";
import type { GoalActions } from "../threads/goal-actions";
import type { HistoryController } from "../threads/history-controller";
import type { ChatInboundController } from "../protocol/inbound/controller";
import { currentModel, runtimeConfigOrDefault } from "../runtime/effective";
import type { RuntimeSnapshot } from "../runtime/snapshot";
@ -94,10 +93,9 @@ export function createConversationSurfaceControllerGroup(
serverThreads: ChatServerThreadActions;
runtimeSettings: ChatRuntimeSettingsActions;
threadActions: ChatThreadActions;
threadRename: ThreadRenameController;
reconnectActions: ChatReconnectActions;
goals: ChatThreadGoalActions;
history: ThreadHistoryController;
goals: GoalActions;
history: HistoryController;
},
) {
const { plugin, state, render, messages, composerView, runtime, thread, liveState, status, lifecycle, client, scroll } = context;

View file

@ -3,7 +3,7 @@ import type { ApprovalAction, PendingApproval } from "../../protocol/requests/ap
import type { ChatInboundController } from "../../protocol/inbound/controller";
import { pendingRequestFocusSignature } from "./signatures";
import { pendingRequestBlockSnapshot, type PendingRequestBlockSnapshot } from "./snapshot";
import type { PendingRequestMessageActions } from "../../ui/pending-request-message";
import type { PendingRequestBlockActions } from "../../ui/pending-request-block";
import { answersForPendingUserInput, type PendingUserInput } from "../../protocol/requests/user-input";
export interface PendingRequestControllerHost {
@ -16,7 +16,7 @@ export interface PendingRequestControllerHost {
export class PendingRequestController {
private lastFocusSignature = "";
private readonly messageActions: PendingRequestMessageActions = {
private readonly blockActions: PendingRequestBlockActions = {
resolveApproval: (approval, action) => {
this.resolveApproval(approval, action);
},
@ -40,8 +40,8 @@ export class PendingRequestController {
return pendingRequestBlockSnapshot(this.host.stateStore.getState());
}
actions(): PendingRequestMessageActions {
return this.messageActions;
actions(): PendingRequestBlockActions {
return this.blockActions;
}
resolveApproval(approval: PendingApproval, action: ApprovalAction): void {

View file

@ -1,7 +1,7 @@
import type { PendingTurnStart } from "../../state/reducer";
import type { DisplayFileMention, DisplayItem, MessageDisplayItem } from "../../display/types";
import { fileMentionsFromInput, userMessageDisplayText } from "../../protocol/display-items";
import { attachHookRunsToTurn } from "../../display/hooks";
import { fileMentionsFromInput, userMessageDisplayText } from "../../display/items/user-message";
import { attachHookRunsToTurn } from "../../state/transcript-updates";
import type { CodexInput } from "../../../../app-server/request-input";
export interface LocalUserMessageParams {

View file

@ -1,4 +1,4 @@
import { jsonPreview } from "../../../utils";
import { jsonPreview } from "../../../../utils";
interface DetailRow {
key: string;

View file

@ -1,5 +1,5 @@
import type { DisplayDetailMetaRow, DisplayDetailSection } from "./types";
import { jsonPreview, truncate } from "../../../utils";
import type { DisplayDetailMetaRow, DisplayDetailSection } from "../types";
import { jsonPreview, truncate } from "../../../../utils";
const TOOL_SUMMARY_LIMIT = 140;

View file

@ -1,21 +1,14 @@
import {
chatTurnBusy,
type ChatActiveThreadState,
type ChatRuntimeState,
type ChatTranscriptState,
type ChatTurnState,
} from "../state/reducer";
import { isCompletedTurnOutcomeMessage } from "./predicates";
import type { DisplayItem, MessageDisplayItem } from "./types";
import { isCompletedTurnOutcomeMessage } from "./turn-outcome-message";
export interface ForkCandidate {
itemId: string;
displayItemId: string;
turnId: string;
}
export interface RollbackCandidate {
turnId: string;
itemId: string;
displayItemId: string;
text: string;
}
@ -23,13 +16,13 @@ export function forkCandidatesFromItems(items: readonly DisplayItem[]): readonly
const turnOutcomeItemsByTurn = new Map<string, ForkCandidate>();
for (const item of items) {
if (!item.turnId || !isCompletedTurnOutcomeMessage(item)) continue;
turnOutcomeItemsByTurn.set(item.turnId, { itemId: item.id, turnId: item.turnId });
turnOutcomeItemsByTurn.set(item.turnId, { displayItemId: item.id, turnId: item.turnId });
}
return [...turnOutcomeItemsByTurn.values()];
}
export function isForkCandidateItem(item: DisplayItem, candidates: readonly ForkCandidate[]): boolean {
return candidates.some((candidate) => item.id === candidate.itemId && item.turnId === candidate.turnId);
return candidates.some((candidate) => item.id === candidate.displayItemId && item.turnId === candidate.turnId);
}
export function turnsAfterTurnId(items: readonly DisplayItem[], turnId: string): number | null {
@ -47,28 +40,18 @@ export function rollbackCandidateFromItems(items: readonly DisplayItem[]): Rollb
return {
turnId: lastTurnId,
itemId: userMessage.id,
displayItemId: userMessage.id,
text: userMessage.text,
};
}
export function isRollbackCandidateItem(item: DisplayItem, candidate: RollbackCandidate | null): boolean {
return Boolean(
candidate && item.kind === "message" && item.role === "user" && item.id === candidate.itemId && item.turnId === candidate.turnId,
);
}
export function implementPlanCandidateFromState(state: {
activeThread: Pick<ChatActiveThreadState, "id">;
turn: ChatTurnState;
runtime: Pick<ChatRuntimeState, "selectedCollaborationMode">;
transcript: Pick<ChatTranscriptState, "displayItems">;
}): DisplayItem | null {
if (!state.activeThread.id || chatTurnBusy(state) || state.runtime.selectedCollaborationMode !== "plan") {
return null;
}
return (
[...state.transcript.displayItems].reverse().find((item) => item.kind === "message" && item.messageKind === "proposedPlan") ?? null
candidate &&
item.kind === "message" &&
item.role === "user" &&
item.id === candidate.displayItemId &&
item.turnId === candidate.turnId,
);
}

View file

@ -1,8 +1,6 @@
import type { AgentDisplayItem, AgentRunSummary, AgentRunSummaryAgent, AgentStateDisplay, DisplayItem, ExecutionState } from "./types";
import { definedProp, truncate } from "../../../utils";
import type { AgentDisplayItem, AgentStateDisplay, ExecutionState } from "../types";
import { definedProp } from "../../../../utils";
const ACTIVE_AGENT_PREVIEW_LIMIT = 96;
type AgentRunState = "running" | "completed" | "failed";
type DisplayExecutionState = Exclude<ExecutionState, null>;
type ExecutionStateByStatus = Readonly<Record<string, DisplayExecutionState>>;
@ -51,7 +49,7 @@ export function agentDisplayItem(item: DisplayCollabAgentToolCall, turnId?: stri
role: "tool",
text: `${agentActivitySummaryLabel(item.tool)}\nstatus: ${item.status}${receiverText}${promptText}`,
...definedProp("turnId", turnId),
itemId: item.id,
sourceItemId: item.id,
tool: item.tool,
status: item.status,
senderThreadId: item.senderThreadId,
@ -73,73 +71,6 @@ export function agentActivitySummaryLabel(tool: string): string {
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;
const agentStatuses = new Map<string, AgentStateDisplay>();
for (const item of items) {
if (item.kind !== "agent" || item.turnId !== activeTurnId) continue;
if (item.agents.length > 0) {
for (const agent of item.agents) {
agentStatuses.set(agent.threadId, agent);
}
} else {
for (const threadId of item.receiverThreadIds) {
agentStatuses.set(threadId, { threadId, status: item.status, message: null });
}
}
}
if (agentStatuses.size === 0) return null;
const summary = { running: 0, completed: 0, failed: 0, agents: [] as AgentRunSummaryAgent[], additionalAgents: 0 };
const agents = [...agentStatuses.values()];
for (const agent of agents) {
const state = agentRunState(agent.status);
summary[state] += 1;
}
if (summary.running === 0 && summary.failed === 0) return null;
summary.agents = agents
.filter((agent) => agentRunState(agent.status) === "running")
.sort((a, b) => a.threadId.localeCompare(b.threadId))
.map((agent) => ({
threadId: agent.threadId,
status: agent.status,
messagePreview: agentMessagePreview(agent.message, ACTIVE_AGENT_PREVIEW_LIMIT),
}));
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]) => ({
@ -163,10 +94,6 @@ function collabAgentExecutionState(tool: string, status: string, receiverThreadI
return null;
}
function agentRunState(status: string): AgentRunState {
return collabAgentStateExecutionState(status) ?? "running";
}
export function collabAgentStateExecutionState(status: string): ExecutionState {
return executionStateFromStatus(status, AGENT_STATES);
}

View file

@ -1,6 +1,6 @@
import type { ThreadGoal, ThreadGoalStatus } from "../../../app-server/thread-goal";
import { truncate } from "../../../utils";
import type { GoalDisplayItem } from "./types";
import type { ThreadGoal, ThreadGoalStatus } from "../../../../app-server/thread-goal";
import { truncate } from "../../../../utils";
import type { GoalDisplayItem } from "../types";
const GOAL_SUMMARY_LIMIT = 140;

View file

@ -1,5 +1,5 @@
import { definedProp } from "../../../utils";
import type { DisplayDetailMetaRow, DisplayDetailSection, DisplayItem, HookDisplayItem } from "./types";
import { definedProp } from "../../../../utils";
import type { DisplayDetailMetaRow, DisplayDetailSection, HookDisplayItem } from "../types";
interface DisplayHookRun {
id: string;
@ -28,7 +28,7 @@ export function hookRunDisplayItem(run: DisplayHookRun, turnId: string | null, s
text: hookSummary(run.eventName, run.statusMessage),
toolLabel: "hook",
...definedProp("turnId", turnId),
itemId: displayId,
sourceItemId: displayId,
status,
details,
output: "",
@ -44,31 +44,6 @@ function hookEventName(eventName: string | null | undefined): string {
return trimmed && trimmed.length > 0 ? trimmed : "Hook";
}
export function attachHookRunsToTurn(
items: readonly DisplayItem[],
turnId: string,
hookItemIds: readonly string[],
afterItemId?: string | null,
): DisplayItem[] {
const hookIdSet = new Set(hookItemIds);
const attachedHooks = items.filter((item) => hookIdSet.has(item.id)).map((item) => ({ ...item, turnId }));
if (attachedHooks.length === 0) return [...items];
const withoutAttachedHooks = items.filter((item) => !hookIdSet.has(item.id));
const anchorItemId = afterItemId ?? lastUserMessageAnchorId(withoutAttachedHooks, turnId);
if (!anchorItemId) return [...withoutAttachedHooks, ...attachedHooks];
const insertAfterIndex = withoutAttachedHooks.findIndex((item) => item.id === anchorItemId);
if (insertAfterIndex === -1) return [...withoutAttachedHooks, ...attachedHooks];
return [...withoutAttachedHooks.slice(0, insertAfterIndex + 1), ...attachedHooks, ...withoutAttachedHooks.slice(insertAfterIndex + 1)];
}
function lastUserMessageAnchorId(items: readonly DisplayItem[], turnId: string): string | null {
const anchor = [...items]
.reverse()
.find((item) => item.kind === "message" && item.role === "user" && (!item.turnId || item.turnId === turnId));
return anchor?.id ?? null;
}
function hookSummary(eventName: string | null | undefined, statusMessage: string | null | undefined): string {
const message = statusMessage?.trim();
const event = hookEventName(eventName);

View file

@ -0,0 +1,6 @@
export function normalizeProposedPlanMarkdown(text: string): string {
return text
.replace(/^\s*<proposed_plan>\s*\n?/i, "")
.replace(/\n?\s*<\/proposed_plan>\s*$/i, "")
.trim();
}

View file

@ -1,6 +1,6 @@
import { permissionRows } from "./permission-rows";
import type { DisplayItem, ExecutionState } from "./types";
import { pathsRelativeToRoot } from "./paths";
import { permissionRows } from "../details/permission-rows";
import type { DisplayItem, ExecutionState } from "../types";
import { pathsRelativeToRoot } from "../details/path-labels";
type DisplayExecutionState = Exclude<ExecutionState, null>;
type ExecutionStateByStatus = Readonly<Record<string, DisplayExecutionState>>;

View file

@ -1,5 +1,5 @@
import type { DisplayItem } from "./types";
import type { DisplayDetailSection } from "./types";
import type { DisplayItem } from "../types";
import type { DisplayDetailSection } from "../types";
export function createSystemItem(id: string, text: string): DisplayItem {
return {

View file

@ -1,4 +1,4 @@
import type { DisplayItem, ExecutionState } from "./types";
import type { DisplayItem, ExecutionState } from "../types";
type DisplayExecutionState = Exclude<ExecutionState, null>;
type ExecutionStateByStatus = Readonly<Record<string, DisplayExecutionState>>;
@ -16,26 +16,13 @@ export interface TaskPlanStep {
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, "")
.replace(/\n?\s*<\/proposed_plan>\s*$/i, "")
.trim();
}
export function planProgressDisplayItem(turnId: string, explanation: string | null, plan: readonly TaskPlanStep[]): DisplayItem {
export function taskProgressDisplayItem(turnId: string, explanation: string | null, plan: readonly TaskPlanStep[]): DisplayItem {
const trimmedExplanation = explanation?.trim();
const lines = plan.map((step) => `${taskStatusMarker(step.status)} ${step.step}`);
const lines = plan.map((step) => `${taskProgressTextMarker(step.status)} ${step.step}`);
const body = [trimmedExplanation, ...lines].filter((line): line is string => Boolean(line && line.length > 0)).join("\n");
const status = plan.some((step) => step.status === "inProgress" || step.status === "pending") ? "inProgress" : "completed";
return {
@ -44,7 +31,7 @@ export function planProgressDisplayItem(turnId: string, explanation: string | nu
role: "tool",
text: body.length > 0 ? body : "Plan updated",
turnId,
itemId: `plan-progress-${turnId}`,
sourceItemId: `plan-progress-${turnId}`,
explanation: trimmedExplanation !== undefined && trimmedExplanation.length > 0 ? trimmedExplanation : null,
steps: plan.map((step) => ({ step: step.step, status: step.status })),
status,
@ -52,6 +39,12 @@ export function planProgressDisplayItem(turnId: string, explanation: string | nu
};
}
function taskProgressTextMarker(status: TaskStepStatus): string {
if (status === "completed") return "[x]";
if (status === "inProgress") return "[>]";
return "[ ]";
}
function executionStateFromStatus(status: string, states: ExecutionStateByStatus): ExecutionState {
return states[status] ?? null;
}

View file

@ -0,0 +1,109 @@
import type { CodexInput, CodexInputItem } from "../../../../app-server/request-input";
import type { DisplayFileMention } from "../types";
type TextRange = [number, number];
export function fileMentionsFromInput(input: readonly CodexInputItem[]): DisplayFileMention[] {
const seen = new Set<string>();
const mentions: DisplayFileMention[] = [];
for (const item of input) {
if (item.type !== "mention" || seen.has(item.path)) continue;
seen.add(item.path);
mentions.push({ name: item.name, path: item.path });
}
return mentions;
}
export function userMessageDisplayText(text: string, input: CodexInput): string {
const names = resolvedSkillNames(input);
if (names.length === 0) return text;
const pattern = new RegExp(`(^|[\\s([{])\\$(${names.map(escapeRegExp).join("|")})(?=$|[\\s\\])}.,;!?])`, "gi");
const codeRanges = markdownCodeRanges(text);
return text.replace(pattern, (match: string, prefix: string, name: string, offset: number) => {
const dollarIndex = offset + prefix.length;
return isIndexInRanges(dollarIndex, codeRanges) ? match : `${prefix}${markdownCodeSpan(`$${name}`)}`;
});
}
function resolvedSkillNames(input: readonly CodexInputItem[]): string[] {
const seen = new Set<string>();
const names: string[] = [];
for (const item of input) {
if (item.type !== "skill") continue;
const key = item.name.toLowerCase();
if (seen.has(key)) continue;
seen.add(key);
names.push(item.name);
}
return names.sort((a, b) => b.length - a.length);
}
function markdownCodeSpan(text: string): string {
if (!text.includes("`")) return `\`${text}\``;
const longestRun = Math.max(...Array.from(text.matchAll(/`+/g), (match) => match[0].length));
const delimiter = "`".repeat(longestRun + 1);
return `${delimiter} ${text} ${delimiter}`;
}
function markdownCodeRanges(text: string): TextRange[] {
return [...markdownFenceRanges(text), ...markdownInlineCodeRanges(text)].sort((a, b) => a[0] - b[0]);
}
function markdownFenceRanges(text: string): TextRange[] {
const ranges: TextRange[] = [];
let active: { marker: string; start: number } | null = null;
let offset = 0;
for (const line of text.matchAll(/[^\n]*(?:\n|$)/g)) {
const value = line[0];
if (value.length === 0) break;
const fence = /^(?: {0,3})(`{3,}|~{3,})/.exec(value);
if (fence) {
const marker = fence[1];
if (!marker) continue;
if (!active) {
active = { marker, start: offset };
} else if (marker.startsWith(active.marker.charAt(0)) && marker.length >= active.marker.length) {
ranges.push([active.start, offset + value.length]);
active = null;
}
}
offset += value.length;
}
if (active) ranges.push([active.start, text.length]);
return ranges;
}
function markdownInlineCodeRanges(text: string): TextRange[] {
const ranges: TextRange[] = [];
const fenceRanges = markdownFenceRanges(text);
let index = 0;
while (index < text.length) {
if (isIndexInRanges(index, fenceRanges) || text[index] !== "`") {
index += 1;
continue;
}
const match = /`+/.exec(text.slice(index));
if (!match) {
index += 1;
continue;
}
const delimiter = match[0];
const end = text.indexOf(delimiter, index + delimiter.length);
if (end < 0) {
index += delimiter.length;
continue;
}
ranges.push([index, end + delimiter.length]);
index = end + delimiter.length;
}
return ranges;
}
function isIndexInRanges(index: number, ranges: readonly TextRange[]): boolean {
return ranges.some(([start, end]) => index >= start && index < end);
}
function escapeRegExp(value: string): string {
return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
}

View file

@ -1,12 +1,12 @@
import { DIAGNOSTIC_PROBE_METHODS, appServerIdentity, appServerPlatform } from "../../../app-server/diagnostics";
import { CLIENT_VERSION } from "../../../constants";
import { DIAGNOSTIC_PROBE_METHODS, appServerIdentity, appServerPlatform } from "../../../../app-server/diagnostics";
import { CLIENT_VERSION } from "../../../../constants";
import type {
Diagnostics,
InitializeDiagnostics,
DiagnosticProbeResult,
McpServerDiagnostic,
McpServerStatusSummary,
} from "../../../app-server/diagnostics";
} from "../../../../app-server/diagnostics";
interface DiagnosticRow {
label: string;

View file

@ -1,9 +1,8 @@
import type { RateLimitWindow, SpendControlLimitSnapshot, ThreadTokenUsage } from "../../../app-server/runtime-metrics";
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 type { RateLimitWindow, SpendControlLimitSnapshot, ThreadTokenUsage } from "../../../../app-server/runtime-metrics";
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 {
currentApprovalsReviewer,
currentApprovalPolicy,
@ -14,9 +13,9 @@ import {
runtimeConfigOrDefault,
serviceTierLabel,
supportedReasoningEfforts,
} from "../runtime/effective";
import type { RuntimeSnapshot } from "../runtime/snapshot";
import { collaborationModeLabel, pendingRuntimeSettingLabel } from "../runtime/settings";
} from "../../runtime/effective";
import type { RuntimeSnapshot } from "../../runtime/snapshot";
import { collaborationModeLabel, pendingRuntimeSettingLabel } from "../../runtime/settings";
export interface ContextSummary {
label: string;
@ -47,21 +46,21 @@ export interface RuntimeConfigSection {
}
export interface StatusSummaryLinesInput {
activeThreadId: ChatState["activeThread"]["id"];
activeThreadId: RuntimeSnapshot["activeThreadId"];
snapshot: RuntimeSnapshot;
nowMs: number;
}
export interface ModelStatusLinesInput {
runtimeConfig: ChatState["connection"]["runtimeConfig"];
requestedModel: ChatState["runtime"]["requestedModel"];
runtimeConfig: RuntimeSnapshot["runtimeConfig"];
requestedModel: RuntimeSnapshot["requestedModel"];
snapshot: RuntimeSnapshot;
collaborationModeLabel: string;
}
export interface EffortStatusLinesInput {
runtimeConfig: ChatState["connection"]["runtimeConfig"];
requestedReasoningEffort: ChatState["runtime"]["requestedReasoningEffort"];
runtimeConfig: RuntimeSnapshot["runtimeConfig"];
requestedReasoningEffort: RuntimeSnapshot["requestedReasoningEffort"];
snapshot: RuntimeSnapshot;
}

View file

@ -0,0 +1,77 @@
import { truncate } from "../../../../utils";
import { collabAgentStateExecutionState } from "../items/agent";
import type { AgentRunSummary, AgentRunSummaryAgent, AgentStateDisplay, DisplayItem } from "../types";
const ACTIVE_AGENT_PREVIEW_LIMIT = 96;
type AgentRunState = "running" | "completed" | "failed";
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;
const agentStatuses = new Map<string, AgentStateDisplay>();
for (const item of items) {
if (item.kind !== "agent" || item.turnId !== activeTurnId) continue;
if (item.agents.length > 0) {
for (const agent of item.agents) {
agentStatuses.set(agent.threadId, agent);
}
} else {
for (const threadId of item.receiverThreadIds) {
agentStatuses.set(threadId, { threadId, status: item.status, message: null });
}
}
}
if (agentStatuses.size === 0) return null;
const summary = { running: 0, completed: 0, failed: 0, agents: [] as AgentRunSummaryAgent[], additionalAgents: 0 };
const agents = [...agentStatuses.values()];
for (const agent of agents) {
const state = agentRunState(agent.status);
summary[state] += 1;
}
if (summary.running === 0 && summary.failed === 0) return null;
summary.agents = agents
.filter((agent) => agentRunState(agent.status) === "running")
.sort((a, b) => a.threadId.localeCompare(b.threadId))
.map((agent) => ({
threadId: agent.threadId,
status: agent.status,
messagePreview: agentMessagePreview(agent.message, ACTIVE_AGENT_PREVIEW_LIMIT),
}));
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 agentRunState(status: string): AgentRunState {
return collabAgentStateExecutionState(status) ?? "running";
}

View file

@ -1,6 +1,6 @@
import type { DisplayBlock, DisplayItem, DisplayKind } from "./types";
import { isCompletedTurnOutcomeMessage } from "./turn-outcome-message";
import { pathRelativeToRoot } from "./paths";
import type { DisplayBlock, DisplayItem, DisplayKind } from "../types";
import { isCompletedTurnOutcomeMessage } from "../predicates";
import { pathRelativeToRoot } from "../details/path-labels";
const STEERING_ACTIVITY_LABEL = "steer";
const STEERING_ACTIVITY_KIND = "userSteered";
@ -79,7 +79,7 @@ function steeringActivityItem(item: DisplayItem, turnId: string): DisplayItem {
role: "tool",
text: item.text,
turnId,
...(item.itemId ? { itemId: item.itemId } : {}),
...(item.sourceItemId ? { sourceItemId: item.sourceItemId } : {}),
activityKind: STEERING_ACTIVITY_KIND,
toolLabel: STEERING_ACTIVITY_LABEL,
};

View file

@ -1,12 +1,12 @@
import type { DisplayDetailSection, DisplayFileChange, DisplayFileMention, DisplayItem, ExecutionState } from "../display/types";
import type { CodexInput, CodexInputItem } from "../../../app-server/request-input";
import type { DisplayDetailSection, DisplayFileChange, DisplayItem, ExecutionState } from "./types";
import type { AppServerFileUpdateChange, AppServerThreadItem, AppServerTurn } from "../../../app-server/turn-model";
import { definedProp, truncate } from "../../../utils";
import { referencedThreadDisplayFromPrompt } from "../../../domain/threads/reference";
import { appServerUserItemText } from "../../../app-server/turn-model";
import { agentDisplayItem } from "../display/agent";
import { pathRelativeToRoot } from "../display/paths";
import { normalizeProposedPlanMarkdown } from "../display/plan";
import { agentDisplayItem } from "./items/agent";
import { pathRelativeToRoot } from "./details/path-labels";
import { normalizeProposedPlanMarkdown } from "./items/proposed-plan";
import { fileMentionsFromInput, userMessageDisplayText } from "./items/user-message";
import {
bodyDetail,
compactToolSummary,
@ -15,7 +15,7 @@ import {
jsonTargetLabel,
metaDetail,
statusQualifier,
} from "../display/tool-format";
} from "./details/tool-details";
type UserMessageItem = Extract<AppServerThreadItem, { type: "userMessage" }>;
type AgentMessageItem = Extract<AppServerThreadItem, { type: "agentMessage" }>;
@ -34,7 +34,6 @@ type ReviewModeItem =
| Extract<AppServerThreadItem, { type: "enteredReviewMode" }>
| 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>>;
@ -124,7 +123,7 @@ function userMessageDisplayItem(item: UserMessageItem, turnId?: string): Display
referencedThread: referencedThread.reference,
...definedProp("turnId", turnId),
...definedProp("clientId", item.clientId),
itemId: item.id,
sourceItemId: item.id,
...(mentionedFiles.length > 0 ? { mentionedFiles } : {}),
};
}
@ -137,116 +136,11 @@ function userMessageDisplayItem(item: UserMessageItem, turnId?: string): Display
copyText: text,
...definedProp("turnId", turnId),
...definedProp("clientId", item.clientId),
itemId: item.id,
sourceItemId: item.id,
...(mentionedFiles.length > 0 ? { mentionedFiles } : {}),
};
}
export function fileMentionsFromInput(input: readonly CodexInputItem[]): DisplayFileMention[] {
const seen = new Set<string>();
const mentions: DisplayFileMention[] = [];
for (const item of input) {
if (item.type !== "mention" || seen.has(item.path)) continue;
seen.add(item.path);
mentions.push({ name: item.name, path: item.path });
}
return mentions;
}
export function userMessageDisplayText(text: string, input: CodexInput): string {
const names = resolvedSkillNames(input);
if (names.length === 0) return text;
const pattern = new RegExp(`(^|[\\s([{])\\$(${names.map(escapeRegExp).join("|")})(?=$|[\\s\\])}.,;!?])`, "gi");
const codeRanges = markdownCodeRanges(text);
return text.replace(pattern, (match: string, prefix: string, name: string, offset: number) => {
const dollarIndex = offset + prefix.length;
return isIndexInRanges(dollarIndex, codeRanges) ? match : `${prefix}${markdownCodeSpan(`$${name}`)}`;
});
}
function resolvedSkillNames(input: readonly CodexInputItem[]): string[] {
const seen = new Set<string>();
const names: string[] = [];
for (const item of input) {
if (item.type !== "skill") continue;
const key = item.name.toLowerCase();
if (seen.has(key)) continue;
seen.add(key);
names.push(item.name);
}
return names.sort((a, b) => b.length - a.length);
}
function markdownCodeSpan(text: string): string {
if (!text.includes("`")) return `\`${text}\``;
const longestRun = Math.max(...Array.from(text.matchAll(/`+/g), (match) => match[0].length));
const delimiter = "`".repeat(longestRun + 1);
return `${delimiter} ${text} ${delimiter}`;
}
function markdownCodeRanges(text: string): TextRange[] {
return [...markdownFenceRanges(text), ...markdownInlineCodeRanges(text)].sort((a, b) => a[0] - b[0]);
}
function markdownFenceRanges(text: string): TextRange[] {
const ranges: TextRange[] = [];
let active: { marker: string; start: number } | null = null;
let offset = 0;
for (const line of text.matchAll(/[^\n]*(?:\n|$)/g)) {
const value = line[0];
if (value.length === 0) break;
const fence = /^(?: {0,3})(`{3,}|~{3,})/.exec(value);
if (fence) {
const marker = fence[1];
if (!marker) continue;
if (!active) {
active = { marker, start: offset };
} else if (marker.startsWith(active.marker.charAt(0)) && marker.length >= active.marker.length) {
ranges.push([active.start, offset + value.length]);
active = null;
}
}
offset += value.length;
}
if (active) ranges.push([active.start, text.length]);
return ranges;
}
function markdownInlineCodeRanges(text: string): TextRange[] {
const ranges: TextRange[] = [];
const fenceRanges = markdownFenceRanges(text);
let index = 0;
while (index < text.length) {
if (isIndexInRanges(index, fenceRanges) || text[index] !== "`") {
index += 1;
continue;
}
const match = /`+/.exec(text.slice(index));
if (!match) {
index += 1;
continue;
}
const delimiter = match[0];
const end = text.indexOf(delimiter, index + delimiter.length);
if (end < 0) {
index += delimiter.length;
continue;
}
ranges.push([index, end + delimiter.length]);
index = end + delimiter.length;
}
return ranges;
}
function isIndexInRanges(index: number, ranges: readonly TextRange[]): boolean {
return ranges.some(([start, end]) => index >= start && index < end);
}
function escapeRegExp(value: string): string {
return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
}
function agentMessageDisplayItem(item: AgentMessageItem, turnId?: string): DisplayItem {
return {
id: item.id,
@ -256,7 +150,7 @@ function agentMessageDisplayItem(item: AgentMessageItem, turnId?: string): Displ
text: item.text,
copyText: item.text,
...definedProp("turnId", turnId),
itemId: item.id,
sourceItemId: item.id,
messageState: "completed",
};
}
@ -271,7 +165,7 @@ function planDisplayItem(item: PlanItem, turnId?: string): DisplayItem {
text,
copyText: text,
...definedProp("turnId", turnId),
itemId: item.id,
sourceItemId: item.id,
messageState: "completed",
};
}
@ -283,7 +177,7 @@ function hookPromptDisplayItem(item: HookPromptItem, turnId?: string): DisplayIt
role: "tool",
text: item.fragments.map((fragment) => fragment.text).join("\n\n") || "Hook prompt",
...definedProp("turnId", turnId),
itemId: item.id,
sourceItemId: item.id,
};
}
@ -294,7 +188,7 @@ function reasoningDisplayItem(item: ReasoningItem, turnId?: string): DisplayItem
role: "tool",
text: reasoningText(item),
...definedProp("turnId", turnId),
itemId: item.id,
sourceItemId: item.id,
};
}
@ -309,7 +203,7 @@ function mcpToolCallDisplayItem(item: McpToolCallItem, turnId?: string): Display
text: compactToolSummary(null, target, statusQualifier(item.status, failure)),
toolLabel: name,
...definedProp("turnId", turnId),
itemId: item.id,
sourceItemId: item.id,
status: item.status,
details: jsonDetails([
["Arguments JSON", item.arguments],
@ -332,7 +226,7 @@ function dynamicToolCallDisplayItem(item: DynamicToolCallItem, turnId?: string):
text: compactToolSummary(null, target, statusQualifier(item.status, failure)),
toolLabel: name,
...definedProp("turnId", turnId),
itemId: item.id,
sourceItemId: item.id,
status: item.status,
details: jsonDetails([
["Arguments JSON", item.arguments],
@ -351,7 +245,7 @@ function webSearchDisplayItem(item: WebSearchItem, turnId?: string): DisplayItem
text: webSearchSummary(item),
toolLabel: "web search",
...definedProp("turnId", turnId),
itemId: item.id,
sourceItemId: item.id,
details: webSearchDetails(item),
output: "",
};
@ -366,7 +260,7 @@ function imageViewDisplayItem(item: ImageViewItem, turnId?: string): DisplayItem
toolLabel: "imageView",
summaryPath: true,
...definedProp("turnId", turnId),
itemId: item.id,
sourceItemId: item.id,
};
}
@ -380,7 +274,7 @@ function imageGenerationDisplayItem(item: ImageGenerationItem, turnId?: string):
toolLabel: "imageGeneration",
summaryPath: Boolean(item.savedPath),
...definedProp("turnId", turnId),
itemId: item.id,
sourceItemId: item.id,
status: item.status,
details: [
...bodyDetail("Saved path", item.savedPath),
@ -400,7 +294,7 @@ function reviewModeDisplayItem(item: ReviewModeItem, turnId?: string): DisplayIt
text: item.type === "enteredReviewMode" ? "Entered review mode" : "Exited review mode",
toolLabel: item.type,
...definedProp("turnId", turnId),
itemId: item.id,
sourceItemId: item.id,
output: item.review,
};
}
@ -412,7 +306,7 @@ function contextCompactionDisplayItem(item: ContextCompactionItem, turnId?: stri
role: "tool",
text: "Context compaction",
...definedProp("turnId", turnId),
itemId: item.id,
sourceItemId: item.id,
};
}
@ -571,7 +465,7 @@ function commandDisplayItem(item: CommandExecutionItem, turnId?: string): Displa
actionLabel: commandActionLabel(item),
text: compactToolSummary(null, target, qualifier),
...definedProp("turnId", turnId),
itemId: item.id,
sourceItemId: item.id,
command: item.command,
cwd: item.cwd,
status: item.status,
@ -591,7 +485,7 @@ function fileChangeDisplayItem(item: FileChangeItem, turnId?: string): DisplayIt
role: "tool",
text: compactToolSummary(null, fileChangeTargetLabel(changes), qualifier),
...definedProp("turnId", turnId),
itemId: item.id,
sourceItemId: item.id,
status: item.status,
changes,
executionState: patchApplyExecutionState(item.status),

View file

@ -25,7 +25,7 @@ interface DisplayBase {
role: DisplayRole;
text: string;
turnId?: string;
itemId?: string;
sourceItemId?: string;
executionState?: ExecutionState;
}

View file

@ -5,7 +5,7 @@ import type { CodexPanelSettings } from "../../../settings/model";
import type { ChatServerMetadataActions } from "../connection/server-actions/metadata";
import type { ChatServerThreadActions } from "../connection/server-actions/threads";
import type { ChatComposerController } from "../conversation/composer/controller";
import type { ChatThreadActions } from "../threads/thread-actions";
import type { ChatThreadActions } from "../threads/actions";
import { scheduleAppServerWarmup } from "../connection/app-server-warmup";
import { closeChatView, openChatView, type ChatViewLifecycleHost } from "./view-lifecycle";
import { createToolbarArchiveConfirmState, ToolbarPanelController } from "./regions/toolbar";

View file

@ -7,7 +7,7 @@ import {
supportedReasoningEfforts,
} from "../../runtime/effective";
import { compactReasoningEffortLabel } from "../../conversation/turns/runtime-overrides";
import { contextSummary } from "../../display/runtime-status";
import { contextSummary } from "../../display/status/runtime";
import { sortedModelMetadata } from "../../../../domain/catalog/metadata";
import type { ReasoningEffort } from "../../../../domain/catalog/metadata";
import type { RuntimeSnapshot } from "../../runtime/snapshot";

View file

@ -1,8 +1,8 @@
import type { Thread } from "../../../../domain/threads/model";
import { getThreadTitle } from "../../../../domain/threads/model";
import type { ChatThreadActions } from "../../threads/thread-actions";
import { runtimeConfigSections, rateLimitSummary } from "../../display/runtime-status";
import { connectionDiagnosticSections } from "../../display/diagnostics";
import type { ChatThreadActions } from "../../threads/actions";
import { runtimeConfigSections, rateLimitSummary } from "../../display/status/runtime";
import { connectionDiagnosticSections } from "../../display/status/diagnostics";
import type { RuntimeSnapshot } from "../../runtime/snapshot";
import type { ChatAction, ChatState, ChatStateStore } from "../../state/reducer";
import type { ChatPanelShellState } from "../../ui/shell";

View file

@ -3,7 +3,7 @@ import type { McpServerStartupStatus } from "../../../../app-server/diagnostics"
import type { ThreadConversationSummary } from "../../../../domain/threads/transcript";
import { classifyAppServerLog } from "./app-server-logs";
import { activeTurnId, type ChatAction, type ChatState, type ChatStateStore } from "../../state/reducer";
import { createStructuredSystemItem, createSystemItem } from "../../display/system";
import { createStructuredSystemItem, createSystemItem } from "../../display/items/system";
import type { DisplayDetailSection } from "../../display/types";
import { approvalResponse, type ApprovalAction, type PendingApproval } from "../requests/approval";
import { userInputResponse, type PendingUserInput } from "../requests/user-input";
@ -107,11 +107,11 @@ export class ChatInboundController {
}
addSystemMessage(text: string): void {
this.dispatch({ type: "transcript/system-message-added", item: createSystemItem(this.localItemId("system"), text) });
this.dispatch({ type: "transcript/system-item-added", item: createSystemItem(this.localItemId("system"), text) });
}
addStructuredSystemMessage(text: string, details: DisplayDetailSection[]): void {
this.dispatch({ type: "transcript/system-message-added", item: createStructuredSystemItem(this.localItemId("system"), text, details) });
this.dispatch({ type: "transcript/system-item-added", item: createStructuredSystemItem(this.localItemId("system"), text, details) });
}
addDedupedSystemMessage(text: string): void {

View file

@ -11,7 +11,7 @@ import type { ServerNotification } from "../../../../app-server/types";
import type { ThreadConversationSummary } from "../../../../domain/threads/transcript";
import { jsonPreview } from "../../../../utils";
import { activeTurnId, pendingTurnStart as pendingTurnStartForState, type ChatAction, type ChatState } from "../../state/reducer";
import { createAutoReviewResultItem, createReviewResultItem } from "../../display/review";
import { createAutoReviewResultItem, createReviewResultItem } from "../../display/items/review-result";
import {
appendAssistantDelta,
appendItemOutput,
@ -20,13 +20,14 @@ import {
appendToolOutput,
completeReasoningItems,
upsertDisplayItem,
} from "../../display/stream-updates";
import { displayItemFromThreadItem, displayItemsFromTurns, normalizeFileChanges, shouldSuppressLifecycleItem } from "../display-items";
import { planProgressDisplayItem } from "../../display/plan";
import { createSystemItem } from "../../display/system";
} from "../../state/transcript-updates";
import { displayItemFromThreadItem, displayItemsFromTurns, normalizeFileChanges, shouldSuppressLifecycleItem } from "../../display/turn-items";
import { taskProgressDisplayItem } from "../../display/items/task-progress";
import { createSystemItem } from "../../display/items/system";
import type { DisplayItem, DisplayKind, MessageDisplayItem } from "../../display/types";
import { goalChangeItem } from "../../display/goal-messages";
import { attachHookRunsToTurn, hookRunDisplayItem } from "../../display/hooks";
import { goalChangeItem } from "../../display/items/goal";
import { hookRunDisplayItem } from "../../display/items/hook-run";
import { attachHookRunsToTurn } from "../../state/transcript-updates";
import {
routeServerNotification,
type DiagnosticStatusNotificationMethod,
@ -135,7 +136,7 @@ const STREAM_UPDATE_PLANNERS = {
"turn/plan/updated": (_state, notification) =>
actionPlan({
type: "transcript/item-upserted",
item: planProgressDisplayItem(notification.params.turnId, notification.params.explanation, notification.params.plan),
item: taskProgressDisplayItem(notification.params.turnId, notification.params.explanation, notification.params.plan),
}),
"item/reasoning/summaryTextDelta": (state, notification) =>
appendToolTextPlan(state, notification.params.itemId, notification.params.turnId, "reasoning", notification.params.delta, "reasoning"),
@ -420,7 +421,7 @@ function fileChangePlan(itemId: string, turnId: string, changes: AppServerFileUp
role: "tool",
text: `File change ${status}`,
turnId,
itemId,
sourceItemId: itemId,
status,
changes: normalizeFileChanges(changes),
},
@ -553,7 +554,7 @@ function isString(value: string | null | undefined): value is string {
}
function systemMessagePlan(message: { id: string; text: string }): ChatNotificationPlan {
return actionPlan({ type: "transcript/system-message-added", item: createSystemItem(message.id, message.text) });
return actionPlan({ type: "transcript/system-item-added", item: createSystemItem(message.id, message.text) });
}
function actionPlan(action: ChatAction): ChatNotificationPlan {

View file

@ -1,5 +1,5 @@
import { jsonPreview } from "../../../../utils";
import { permissionRows } from "../../display/permission-rows";
import { permissionRows } from "../../display/details/permission-rows";
import type { RequestId } from "../../../../app-server/types";
type SimpleApprovalDecision = "accept" | "acceptForSession" | "decline" | "cancel";

View file

@ -28,7 +28,7 @@ import {
import type { RequestedServiceTier } from "../runtime/settings";
import type { RequestId } from "../../../app-server/types";
import type { ComposerSuggestion } from "../conversation/composer/suggestions";
import { upsertDisplayItem } from "../display/stream-updates";
import { upsertDisplayItem } from "./transcript-updates";
import type { DisplayItem } from "../display/types";
import type {
ActiveThreadResumedAction,

View file

@ -1,8 +1,7 @@
import { activeTurnId as selectActiveTurnId, chatTurnBusy, pendingTurnStart } from "./reducer";
import type { ChatState, PendingTurnStart } from "./reducer";
import type { ChatActiveThreadState, ChatRuntimeState, ChatState, ChatTranscriptState, ChatTurnState, PendingTurnStart } from "./reducer";
import type { Thread } from "../../../domain/threads/model";
import type { DisplayItem } from "../display/types";
import { implementPlanCandidateFromState } from "../display/action-candidates";
export interface SubmissionStateSnapshot {
activeThreadId: string | null;
@ -43,3 +42,17 @@ export function submissionStateSnapshot(state: ChatState): SubmissionStateSnapsh
export function canImplementPlan(state: ChatState, item: DisplayItem): boolean {
return item.id === implementPlanCandidateFromState(state)?.id;
}
export function implementPlanCandidateFromState(state: {
activeThread: Pick<ChatActiveThreadState, "id">;
turn: ChatTurnState;
runtime: Pick<ChatRuntimeState, "selectedCollaborationMode">;
transcript: Pick<ChatTranscriptState, "displayItems">;
}): DisplayItem | null {
if (!state.activeThread.id || chatTurnBusy(state) || state.runtime.selectedCollaborationMode !== "plan") {
return null;
}
return (
[...state.transcript.displayItems].reverse().find((item) => item.kind === "message" && item.messageKind === "proposedPlan") ?? null
);
}

View file

@ -1,6 +1,6 @@
import type { AssistantAuthoredMessageDisplayItem, DisplayFileChange, DisplayItem, DisplayKind } from "./types";
import { isAssistantAuthoredMessage } from "./turn-outcome-message";
import { normalizeProposedPlanMarkdown } from "./plan";
import { normalizeProposedPlanMarkdown } from "../display/items/proposed-plan";
import { isAssistantAuthoredMessage } from "../display/predicates";
import type { AssistantAuthoredMessageDisplayItem, DisplayFileChange, DisplayItem, DisplayKind } from "../display/types";
export function upsertDisplayItem(items: readonly DisplayItem[], next: DisplayItem): DisplayItem[] {
const index = items.findIndex((item) => item.id === next.id);
@ -29,8 +29,8 @@ function mergeChanges(previous: DisplayItem, next: DisplayItem): DisplayFileChan
return nextChanges && nextChanges.length > 0 ? nextChanges : previousChanges;
}
export function appendAssistantDelta(items: readonly DisplayItem[], itemId: string, turnId: string, delta: string): DisplayItem[] {
const index = items.findIndex((item) => item.itemId === itemId && item.kind === "message" && item.messageKind === "assistantResponse");
export function appendAssistantDelta(items: readonly DisplayItem[], sourceItemId: string, turnId: string, delta: string): DisplayItem[] {
const index = items.findIndex((item) => item.sourceItemId === sourceItemId && item.kind === "message" && item.messageKind === "assistantResponse");
if (index !== -1) {
return items.map((item, itemIndex) =>
itemIndex === index && item.kind === "message" && item.messageKind === "assistantResponse"
@ -47,14 +47,14 @@ export function appendAssistantDelta(items: readonly DisplayItem[], itemId: stri
return [
...items,
{
id: itemId,
id: sourceItemId,
kind: "message",
messageKind: "assistantResponse",
role: "assistant",
text: delta,
copyText: delta,
turnId,
itemId,
sourceItemId,
messageState: "streaming",
},
];
@ -72,8 +72,8 @@ export function completeReasoningItems(items: readonly DisplayItem[], turnId: st
);
}
export function appendPlanDelta(items: readonly DisplayItem[], itemId: string, turnId: string, delta: string): DisplayItem[] {
const index = items.findIndex((item) => item.itemId === itemId && isAssistantAuthoredMessage(item));
export function appendPlanDelta(items: readonly DisplayItem[], sourceItemId: string, turnId: string, delta: string): DisplayItem[] {
const index = items.findIndex((item) => item.sourceItemId === sourceItemId && isAssistantAuthoredMessage(item));
if (index !== -1) {
return items.map((item, itemIndex) =>
itemIndex === index && isAssistantAuthoredMessage(item) ? appendPlanDeltaToMessage(item, turnId, delta) : item,
@ -83,14 +83,14 @@ export function appendPlanDelta(items: readonly DisplayItem[], itemId: string, t
return [
...items,
{
id: itemId,
id: sourceItemId,
kind: "message",
messageKind: "proposedPlan",
role: "assistant",
text,
copyText: text,
turnId,
itemId,
sourceItemId,
messageState: "streaming",
},
];
@ -110,37 +110,37 @@ function appendPlanDeltaToMessage(item: AssistantAuthoredMessageDisplayItem, tur
export function appendItemText(
items: readonly DisplayItem[],
itemId: string,
sourceItemId: string,
turnId: string,
label: string,
delta: string,
kind: Extract<DisplayKind, "tool" | "hook" | "reasoning"> = "tool",
): DisplayItem[] {
const index = items.findIndex((item) => item.itemId === itemId);
const index = items.findIndex((item) => item.sourceItemId === sourceItemId);
if (index !== -1) {
return items.map((item, itemIndex) => (itemIndex === index ? { ...item, text: `${item.text}${delta}` } : item));
}
return [
...items,
{
id: itemId,
id: sourceItemId,
kind,
role: "tool",
text: `${label}: ${delta}`,
turnId,
itemId,
sourceItemId,
},
];
}
export function appendToolOutput(
items: readonly DisplayItem[],
itemId: string,
sourceItemId: string,
turnId: string,
delta: string,
fallbackLabel: string,
): DisplayItem[] {
const index = items.findIndex((item) => item.itemId === itemId);
const index = items.findIndex((item) => item.sourceItemId === sourceItemId);
if (index !== -1) {
return items.map((item, itemIndex) =>
itemIndex === index && (item.kind === "tool" || item.kind === "hook" || item.kind === "reasoning")
@ -151,13 +151,13 @@ export function appendToolOutput(
return [
...items,
{
id: itemId,
id: sourceItemId,
kind: "tool",
role: "tool",
text: "details",
toolLabel: fallbackLabel,
turnId,
itemId,
sourceItemId,
output: delta,
},
];
@ -165,13 +165,13 @@ export function appendToolOutput(
export function appendItemOutput(
items: readonly DisplayItem[],
itemId: string,
sourceItemId: string,
turnId: string,
delta: string,
kind: "command" | "fileChange",
fallbackText: string,
): DisplayItem[] {
const index = items.findIndex((item) => item.itemId === itemId);
const index = items.findIndex((item) => item.sourceItemId === sourceItemId);
if (index !== -1) {
return items.map((item, itemIndex) =>
itemIndex === index && (item.kind === "command" || item.kind === "fileChange")
@ -182,12 +182,12 @@ export function appendItemOutput(
return [
...items,
{
id: itemId,
id: sourceItemId,
kind,
role: "tool",
text: fallbackText,
turnId,
itemId,
sourceItemId,
output: delta,
...(kind === "fileChange"
? {
@ -204,3 +204,28 @@ export function appendItemOutput(
},
] as DisplayItem[];
}
export function attachHookRunsToTurn(
items: readonly DisplayItem[],
turnId: string,
hookItemIds: readonly string[],
afterItemId?: string | null,
): DisplayItem[] {
const hookIdSet = new Set(hookItemIds);
const attachedHooks = items.filter((item) => hookIdSet.has(item.id)).map((item) => ({ ...item, turnId }));
if (attachedHooks.length === 0) return [...items];
const withoutAttachedHooks = items.filter((item) => !hookIdSet.has(item.id));
const anchorItemId = afterItemId ?? lastUserMessageAnchorId(withoutAttachedHooks, turnId);
if (!anchorItemId) return [...withoutAttachedHooks, ...attachedHooks];
const insertAfterIndex = withoutAttachedHooks.findIndex((item) => item.id === anchorItemId);
if (insertAfterIndex === -1) return [...withoutAttachedHooks, ...attachedHooks];
return [...withoutAttachedHooks.slice(0, insertAfterIndex + 1), ...attachedHooks, ...withoutAttachedHooks.slice(insertAfterIndex + 1)];
}
function lastUserMessageAnchorId(items: readonly DisplayItem[], turnId: string): string | null {
const anchor = [...items]
.reverse()
.find((item) => item.kind === "message" && item.role === "user" && (!item.turnId || item.turnId === turnId));
return anchor?.id ?? null;
}

View file

@ -1,4 +1,4 @@
import { upsertDisplayItem } from "../display/stream-updates";
import { upsertDisplayItem } from "./transcript-updates";
import type { DisplayItem } from "../display/types";
export interface ChatTranscriptState {
@ -11,7 +11,7 @@ export interface ChatTranscriptState {
export type TranscriptAction =
| { type: "transcript/item-added"; item: DisplayItem }
| { type: "transcript/system-message-added"; item: DisplayItem }
| { type: "transcript/system-item-added"; item: DisplayItem }
| { type: "transcript/deduped-log-added"; text: string; item: DisplayItem }
| { type: "transcript/history-loading-set"; loading: boolean }
| {
@ -36,7 +36,7 @@ export function initialChatTranscriptState(): ChatTranscriptState {
export function reduceTranscriptSlice(state: ChatTranscriptState, action: TranscriptAction): ChatTranscriptState {
switch (action.type) {
case "transcript/item-added":
case "transcript/system-message-added":
case "transcript/system-item-added":
return patchObject(state, { displayItems: [...state.displayItems, action.item] });
case "transcript/deduped-log-added":
if (state.reportedLogs.has(action.text)) return state;

View file

@ -1,11 +1,11 @@
import { archiveThread } from "./thread-archive-actions";
import { compactThread } from "./thread-compact-actions";
import type { ChatThreadActionsHost } from "./thread-action-context";
import { forkThread, forkThreadFromTurn } from "./thread-fork-actions";
import { renameThread } from "./thread-rename-actions";
import { rollbackThread } from "./thread-rollback-actions";
import { archiveThread } from "./archive-actions";
import { compactThread } from "./compact-actions";
import type { ChatThreadActionsHost } from "./action-context";
import { forkThread, forkThreadFromTurn } from "./fork-actions";
import { renameThread } from "./rename-actions";
import { rollbackThread } from "./rollback-actions";
export type { ChatThreadActionsHost } from "./thread-action-context";
export type { ChatThreadActionsHost } from "./action-context";
export interface ChatThreadActions {
compactThread: (threadId: string) => Promise<void>;

View file

@ -4,8 +4,8 @@ import { threadFromAppServerThread } from "../../../app-server/thread-model";
import { transcriptEntriesFromAppServerTurn } from "../../../app-server/turn-model";
import { exportArchivedThreadMarkdown } from "../../../domain/threads/export";
import { chatTurnBusy } from "../state/reducer";
import type { ChatThreadActionsHost } from "./thread-action-context";
import { threadActionState } from "./thread-action-context";
import type { ChatThreadActionsHost } from "./action-context";
import { threadActionState } from "./action-context";
export async function archiveThread(
host: ChatThreadActionsHost,

View file

@ -1,5 +1,5 @@
import type { ChatThreadActionsHost } from "./thread-action-context";
import { threadActionState, threadActionStillTargetsOriginalPanel } from "./thread-action-context";
import type { ChatThreadActionsHost } from "./action-context";
import { threadActionState, threadActionStillTargetsOriginalPanel } from "./action-context";
export async function compactThread(host: ChatThreadActionsHost, threadId: string): Promise<void> {
await host.ensureConnected();

View file

@ -2,14 +2,14 @@ import type { ConnectionManager } from "../../../app-server/connection-manager";
import type { AppServerClient } from "../../../app-server/client";
import { recoverRolloutTokenUsage } from "../../../app-server/rollout-token-usage";
import type { ArchiveExportAdapter } from "../../../domain/threads/export";
import { createChatThreadActions } from "./thread-actions";
import { createChatThreadGoalActions } from "./thread-goal-actions";
import { ThreadHistoryController } from "./thread-history-controller";
import { createThreadIdentitySync } from "./thread-identity-sync";
import { ThreadRenameController } from "./thread-rename-controller";
import { ThreadResumeController } from "./thread-resume-controller";
import { createThreadSelectionActions } from "./thread-selection-actions";
import { RestoredThreadController } from "./restored-thread-controller";
import { createChatThreadActions } from "./actions";
import { createGoalActions } from "./goal-actions";
import { HistoryController } from "./history-controller";
import { createIdentitySync } from "./identity-sync";
import { RenameController } from "./rename-controller";
import { ResumeController } from "./resume-controller";
import { createSelectionActions } from "./selection-actions";
import { RestorationController } from "./restoration-controller";
import type { ChatStateStore } from "../state/reducer";
import type { ChatResumeWorkTracker, ChatViewDeferredTasks } from "../lifecycle";
import type { CodexPanelSettings } from "../../../settings/model";
@ -76,7 +76,7 @@ export function createThreadControllerGroup(
const currentClient = client.getClient;
const { deferredTasks, resumeWork } = lifecycle;
const threadRename = new ThreadRenameController({
const rename = new RenameController({
stateStore,
vaultPath: plugin.vaultPath,
settings: () => plugin.settings,
@ -87,9 +87,9 @@ export function createThreadControllerGroup(
notifyThreadRenamed: plugin.notifyThreadRenamed.bind(plugin),
});
const resetThreadTurnPresence = (hadTurns: boolean) => {
threadRename.resetThreadTurnPresence(hadTurns);
rename.resetThreadTurnPresence(hadTurns);
};
const history = new ThreadHistoryController({
const history = new HistoryController({
stateStore,
currentClient,
render: render.now,
@ -102,7 +102,7 @@ export function createThreadControllerGroup(
resumeWork.invalidate();
history.invalidate();
};
const threadActions = createChatThreadActions({
const actions = createChatThreadActions({
stateStore,
vaultPath: plugin.vaultPath,
settings: () => plugin.settings,
@ -125,7 +125,7 @@ export function createThreadControllerGroup(
plugin.refreshSharedThreadListFromOpenSurface();
},
});
const goals = createChatThreadGoalActions({
const goals = createGoalActions({
stateStore,
currentClient,
ensureConnected: client.ensureConnected,
@ -136,22 +136,22 @@ export function createThreadControllerGroup(
render: render.now,
refreshLiveState: liveState.refresh,
});
let threadResume: ThreadResumeController | null = null;
const restoredThread = new RestoredThreadController({
let resume: ResumeController | null = null;
const restoration = new RestorationController({
deferredTasks,
opened: lifecycle.getOpened,
resumeThread: (threadId) => requireThreadController(threadResume, "thread resume controller").resumeThread(threadId),
resumeThread: (threadId) => requireThreadController(resume, "resume controller").resumeThread(threadId),
invalidateResumeWork,
stateStore,
setStatus: status.set,
refreshTabHeader: thread.refreshTabHeader,
});
threadResume = new ThreadResumeController({
resume = new ResumeController({
stateStore,
vaultPath: plugin.vaultPath,
resumeWork,
history,
restoredThread,
restoration,
currentClient,
ensureConnected: client.ensureConnected,
closing: lifecycle.getClosing,
@ -168,9 +168,9 @@ export function createThreadControllerGroup(
return response?.dataBase64 ?? "";
}),
});
const threadIdentity = createThreadIdentitySync({
const identity = createIdentitySync({
stateStore,
restoredThread,
restoration,
invalidateResumeWork,
clearDeferredRestoredThreadHydration: lifecycle.clearDeferredRestoredThreadHydration,
resetThreadTurnPresence,
@ -182,12 +182,12 @@ export function createThreadControllerGroup(
return {
history,
threadActions,
actions,
goals,
restoredThread,
threadResume: requireThreadController(threadResume, "thread resume controller"),
threadIdentity,
threadRename,
restoration,
resume: requireThreadController(resume, "resume controller"),
identity,
rename,
invalidateResumeWork,
};
}
@ -221,7 +221,7 @@ export function createThreadSelectionActionGroup(
const { plugin, thread, status } = context;
const stateStore = context.state.stateStore;
const threadSelection = createThreadSelectionActions({
const selection = createSelectionActions({
stateStore,
closeForThreadSelection: () => {
refs.closeForThreadSelection();
@ -231,5 +231,5 @@ export function createThreadSelectionActionGroup(
addSystemMessage: status.addSystemMessage,
});
return { threadSelection };
return { selection };
}

View file

@ -1,9 +1,9 @@
import { inheritedForkThreadName } from "../../../domain/threads/model";
import { chatTurnBusy } from "../state/reducer";
import { turnsAfterTurnId } from "../display/action-candidates";
import { archiveThreadOnServer } from "./thread-archive-actions";
import type { ChatThreadActionsHost } from "./thread-action-context";
import { threadActionState, threadActionStillTargetsOriginalPanel } from "./thread-action-context";
import { turnsAfterTurnId } from "../display/item-actions";
import { archiveThreadOnServer } from "./archive-actions";
import type { ChatThreadActionsHost } from "./action-context";
import { threadActionState, threadActionStillTargetsOriginalPanel } from "./action-context";
export function forkThread(host: ChatThreadActionsHost, threadId: string): Promise<void> {
return forkThreadFromTurn(host, threadId, null, false);

View file

@ -8,9 +8,9 @@ import {
} from "../../../app-server/thread-goal";
import type { ChatStateStore } from "../state/reducer";
import type { GoalDisplayItem } from "../display/types";
import { goalChangeItem } from "../display/goal-messages";
import { goalChangeItem } from "../display/items/goal";
export interface ChatThreadGoalActionsHost {
export interface GoalActionsHost {
stateStore: ChatStateStore;
currentClient: () => AppServerClient | null;
ensureConnected: () => Promise<void>;
@ -20,7 +20,7 @@ export interface ChatThreadGoalActionsHost {
refreshLiveState: () => void;
}
export interface ChatThreadGoalActions {
export interface GoalActions {
activeGoal: () => ThreadGoal | null;
syncThreadGoal: (threadId: string) => Promise<void>;
setObjective: (threadId: string, objective: string, tokenBudget: number | null) => Promise<boolean>;
@ -28,7 +28,7 @@ export interface ChatThreadGoalActions {
clear: (threadId: string) => Promise<boolean>;
}
export function createChatThreadGoalActions(host: ChatThreadGoalActionsHost): ChatThreadGoalActions {
export function createGoalActions(host: GoalActionsHost): GoalActions {
return {
activeGoal: () => host.stateStore.getState().activeThread.goal,
syncThreadGoal: (threadId) => syncThreadGoal(host, threadId),
@ -38,7 +38,7 @@ export function createChatThreadGoalActions(host: ChatThreadGoalActionsHost): Ch
};
}
async function syncThreadGoal(host: ChatThreadGoalActionsHost, threadId: string): Promise<void> {
async function syncThreadGoal(host: GoalActionsHost, threadId: string): Promise<void> {
const client = host.currentClient();
if (!client) return;
try {
@ -50,7 +50,7 @@ async function syncThreadGoal(host: ChatThreadGoalActionsHost, threadId: string)
}
async function setObjective(
host: ChatThreadGoalActionsHost,
host: GoalActionsHost,
threadId: string,
objective: string,
tokenBudget: number | null,
@ -73,11 +73,11 @@ async function setObjective(
return applied;
}
function setGoalStatus(host: ChatThreadGoalActionsHost, threadId: string, status: ThreadGoalStatus): Promise<boolean> {
function setGoalStatus(host: GoalActionsHost, threadId: string, status: ThreadGoalStatus): Promise<boolean> {
return setGoal(host, threadId, { status });
}
async function clearGoal(host: ChatThreadGoalActionsHost, threadId: string): Promise<boolean> {
async function clearGoal(host: GoalActionsHost, threadId: string): Promise<boolean> {
await host.ensureConnected();
const client = host.currentClient();
if (!client) return false;
@ -91,7 +91,7 @@ async function clearGoal(host: ChatThreadGoalActionsHost, threadId: string): Pro
}
}
async function setGoal(host: ChatThreadGoalActionsHost, threadId: string, params: ThreadGoalUpdate): Promise<boolean> {
async function setGoal(host: GoalActionsHost, threadId: string, params: ThreadGoalUpdate): Promise<boolean> {
await host.ensureConnected();
const client = host.currentClient();
if (!client) return false;
@ -105,7 +105,7 @@ async function setGoal(host: ChatThreadGoalActionsHost, threadId: string, params
}
function applyGoalIfActive(
host: ChatThreadGoalActionsHost,
host: GoalActionsHost,
threadId: string,
goal: ThreadGoal | null,
options: { reportChange: boolean },
@ -120,7 +120,7 @@ function applyGoalIfActive(
return true;
}
async function recordGoalUserMessage(host: ChatThreadGoalActionsHost, threadId: string, objective: string): Promise<void> {
async function recordGoalUserMessage(host: GoalActionsHost, threadId: string, objective: string): Promise<void> {
const client = host.currentClient();
if (!client) return;
try {
@ -130,7 +130,7 @@ async function recordGoalUserMessage(host: ChatThreadGoalActionsHost, threadId:
}
}
function addThreadScopedSystemMessage(host: ChatThreadGoalActionsHost, threadId: string, text: string): void {
function addThreadScopedSystemMessage(host: GoalActionsHost, threadId: string, text: string): void {
if (host.stateStore.getState().activeThread.id !== threadId) return;
host.addSystemMessage(text);
}

View file

@ -1,9 +1,9 @@
import type { AppServerClient } from "../../../app-server/client";
import type { ThreadTurnsPage } from "../../../app-server/turn-history";
import type { ChatAction, ChatState, ChatStateStore } from "../state/reducer";
import { displayItemsFromTurns } from "../protocol/display-items";
import { displayItemsFromTurns } from "../display/turn-items";
export interface ThreadHistoryControllerHost {
export interface HistoryControllerHost {
stateStore: ChatStateStore;
currentClient: () => AppServerClient | null;
render: () => void;
@ -20,10 +20,10 @@ type ThreadHistoryLoadLifecycleEvent =
| { type: "finished"; load: ActiveThreadHistoryLoad }
| { type: "invalidated" };
export class ThreadHistoryController {
export class HistoryController {
private lifecycle: ThreadHistoryLoadLifecycleState = { kind: "idle" };
constructor(private readonly host: ThreadHistoryControllerHost) {}
constructor(private readonly host: HistoryControllerHost) {}
private get state(): ChatState {
return this.host.stateStore.getState();

View file

@ -1,10 +1,10 @@
import type { RestoredThreadController } from "./restored-thread-controller";
import type { RestorationController } from "./restoration-controller";
import { activeThreadId, listedThreads } from "../state/selectors";
import type { ChatStateStore } from "../state/reducer";
export interface ThreadIdentitySyncHost {
export interface IdentitySyncHost {
stateStore: ChatStateStore;
restoredThread: RestoredThreadController;
restoration: RestorationController;
invalidateResumeWork: () => void;
clearDeferredRestoredThreadHydration: () => void;
resetThreadTurnPresence: (hadTurns: boolean) => void;
@ -14,13 +14,13 @@ export interface ThreadIdentitySyncHost {
render: () => void;
}
export interface ThreadIdentitySync {
export interface IdentitySync {
clearActiveThreadContext: () => void;
notifyThreadArchived: (threadId: string) => void;
notifyThreadRenamed: (threadId: string, name: string | null) => void;
}
export function createThreadIdentitySync(host: ThreadIdentitySyncHost): ThreadIdentitySync {
export function createIdentitySync(host: IdentitySyncHost): IdentitySync {
return {
clearActiveThreadContext: () => {
clearActiveThreadContext(host);
@ -34,9 +34,9 @@ export function createThreadIdentitySync(host: ThreadIdentitySyncHost): ThreadId
};
}
function clearActiveThreadContext(host: ThreadIdentitySyncHost): void {
function clearActiveThreadContext(host: IdentitySyncHost): void {
host.invalidateResumeWork();
host.restoredThread.clear();
host.restoration.clear();
host.clearDeferredRestoredThreadHydration();
host.stateStore.dispatch({ type: "active-thread/cleared" });
host.resetThreadTurnPresence(false);
@ -44,13 +44,13 @@ function clearActiveThreadContext(host: ThreadIdentitySyncHost): void {
host.refreshLiveState();
}
function notifyThreadArchived(host: ThreadIdentitySyncHost, threadId: string): void {
function notifyThreadArchived(host: IdentitySyncHost, threadId: string): void {
if (activeThreadId(host.stateStore.getState()) !== threadId) return;
clearActiveThreadContext(host);
host.render();
}
function notifyThreadRenamed(host: ThreadIdentitySyncHost, threadId: string, name: string | null): void {
function notifyThreadRenamed(host: IdentitySyncHost, threadId: string, name: string | null): void {
let changed = false;
const renamedThreads = listedThreads(host.stateStore.getState()).map((thread) => {
if (thread.id !== threadId) return thread;
@ -58,12 +58,12 @@ function notifyThreadRenamed(host: ThreadIdentitySyncHost, threadId: string, nam
return { ...thread, name };
});
host.stateStore.dispatch({ type: "thread-list/applied", threads: renamedThreads });
const restoredThread = host.restoredThread.placeholder();
const restoredThread = host.restoration.placeholder();
if (restoredThread?.threadId === threadId && (restoredThread.title !== name || restoredThread.explicitName !== name)) {
host.restoredThread.rename(threadId, name);
host.restoration.rename(threadId, name);
changed = true;
}
const activeThreadChanged = activeThreadId(host.stateStore.getState()) === threadId || host.restoredThread.isPending(threadId);
const activeThreadChanged = activeThreadId(host.stateStore.getState()) === threadId || host.restoration.isPending(threadId);
if (!changed && !activeThreadChanged) return;
if (activeThreadChanged) {
host.notifyActiveThreadIdentityChanged();

View file

@ -1,6 +1,6 @@
import type { ThreadNamingContext } from "../../../domain/threads/naming";
import { truncate } from "../../../utils";
import { isCompletedTurnOutcomeMessage } from "../display/turn-outcome-message";
import { isCompletedTurnOutcomeMessage } from "../display/predicates";
import type { DisplayItem } from "../display/types";
const MAX_CONTEXT_CHARS = 4_000;

View file

@ -12,10 +12,10 @@ import type { CodexPanelSettings } from "../../../settings/model";
import type { ChatAction, ChatState, ChatStateStore } from "../state/reducer";
import { generateThreadTitleWithCodex } from "../../../app-server/thread-title-generation";
import { completedConversationSummaryFromAppServerTurn } from "../../../app-server/turn-model";
import { firstNamingContextFromDisplayItems, namingContextFromDisplayItems } from "./thread-naming";
import { renameConnectedThread } from "./thread-rename-actions";
import { firstNamingContextFromDisplayItems, namingContextFromDisplayItems } from "./naming";
import { renameConnectedThread } from "./rename-actions";
export interface ThreadRenameEditState {
export interface RenameEditState {
draft: string;
generating: boolean;
}
@ -34,7 +34,7 @@ type RenameLifecycleEvent =
| { type: "generation-finished"; threadId: string; generatingState: RenameGeneratingState }
| { type: "cleared" };
export interface ThreadRenameControllerHost {
export interface RenameControllerHost {
stateStore: ChatStateStore;
vaultPath: string;
settings: () => CodexPanelSettings;
@ -46,7 +46,7 @@ export interface ThreadRenameControllerHost {
generateThreadTitle?: (context: ThreadNamingContext) => Promise<string | null>;
}
export class ThreadRenameController {
export class RenameController {
private activeThreadHadTurns = false;
private readonly autoNameAttemptedThreadIds = new Set<string>();
private readonly autoNameInFlightThreadIds = new Set<string>();
@ -54,7 +54,7 @@ export class ThreadRenameController {
private nextRenameGenerationId = 1;
private renameState: RenameLifecycleState = { kind: "idle" };
constructor(private readonly host: ThreadRenameControllerHost) {}
constructor(private readonly host: RenameControllerHost) {}
private get state(): ChatState {
return this.host.stateStore.getState();
@ -68,7 +68,7 @@ export class ThreadRenameController {
this.activeThreadHadTurns = hadTurns;
}
editState(threadId: string): ThreadRenameEditState | null {
editState(threadId: string): RenameEditState | null {
if (this.renameState.kind === "idle" || this.renameState.threadId !== threadId) return null;
return {
draft: this.renameState.draft,

View file

@ -7,7 +7,7 @@ import {
type ChatViewDeferredTasks,
} from "../lifecycle";
export interface RestoredThreadControllerHost {
export interface RestorationControllerHost {
deferredTasks: ChatViewDeferredTasks;
opened: () => boolean;
resumeThread: (threadId: string) => Promise<void>;
@ -17,10 +17,10 @@ export interface RestoredThreadControllerHost {
refreshTabHeader: () => void;
}
export class RestoredThreadController {
export class RestorationController {
private lifecycle: RestoredThreadLifecycleState = { kind: "idle" };
constructor(private readonly host: RestoredThreadControllerHost) {}
constructor(private readonly host: RestorationControllerHost) {}
placeholder(): RestoredThreadPlaceholderState | null {
return this.lifecycle.kind === "placeholder" ? this.lifecycle : null;

View file

@ -2,17 +2,17 @@ import type { AppServerClient } from "../../../app-server/client";
import type { ThreadTokenUsage } from "../../../app-server/runtime-metrics";
import { activeThreadId, canSwitchToThread, displayItemsEmpty, listedThreads } from "../state/selectors";
import type { ChatStateStore } from "../state/reducer";
import type { RestoredThreadController } from "./restored-thread-controller";
import { resumedThreadActionFromAppServerResponse } from "./thread-resume";
import type { ThreadHistoryController } from "./thread-history-controller";
import type { RestorationController } from "./restoration-controller";
import { resumedThreadActionFromAppServerResponse } from "./resume";
import type { HistoryController } from "./history-controller";
import type { ChatResumeWorkTracker, ActiveChatResume } from "../lifecycle";
export interface ThreadResumeControllerHost {
export interface ResumeControllerHost {
stateStore: ChatStateStore;
vaultPath: string;
resumeWork: ChatResumeWorkTracker;
history: ThreadHistoryController;
restoredThread: RestoredThreadController;
history: HistoryController;
restoration: RestorationController;
currentClient: () => AppServerClient | null;
ensureConnected: () => Promise<void>;
closing: () => boolean;
@ -26,8 +26,8 @@ export interface ThreadResumeControllerHost {
recoverTokenUsageFromRollout?: (path: string) => Promise<ThreadTokenUsage | null>;
}
export class ThreadResumeController {
constructor(private readonly host: ThreadResumeControllerHost) {}
export class ResumeController {
constructor(private readonly host: ResumeControllerHost) {}
async resumeThread(threadId: string): Promise<void> {
if (!canSwitchToThread(this.host.stateStore.getState(), threadId)) {
@ -72,7 +72,7 @@ export class ThreadResumeController {
listedThreads: listedThreads(this.host.stateStore.getState()),
}),
);
this.host.restoredThread.clear();
this.host.restoration.clear();
this.host.clearDeferredRestoredThreadHydration();
this.host.resetThreadTurnPresence(false);
this.host.notifyActiveThreadIdentityChanged();

View file

@ -1,10 +1,10 @@
import { threadFromAppServerThread } from "../../../app-server/thread-model";
import { rollbackCandidateFromItems } from "../display/action-candidates";
import { displayItemsFromTurns } from "../protocol/display-items";
import { rollbackCandidateFromItems } from "../display/item-actions";
import { displayItemsFromTurns } from "../display/turn-items";
import { chatTurnBusy } from "../state/reducer";
import type { ChatThreadActionsHost } from "./thread-action-context";
import { threadActionDispatch, threadActionState, threadActionStillTargetsPanel } from "./thread-action-context";
import { resumedThreadActionFromActiveRuntime } from "./thread-resume";
import type { ChatThreadActionsHost } from "./action-context";
import { threadActionDispatch, threadActionState, threadActionStillTargetsPanel } from "./action-context";
import { resumedThreadActionFromActiveRuntime } from "./resume";
export async function rollbackThread(host: ChatThreadActionsHost, threadId: string): Promise<void> {
if (chatTurnBusy(threadActionState(host))) {

View file

@ -1,7 +1,7 @@
import { canSwitchToThread } from "../state/selectors";
import type { ChatStateStore } from "../state/reducer";
export interface ThreadSelectionActionsHost {
export interface SelectionActionsHost {
stateStore: ChatStateStore;
closeForThreadSelection: () => void;
focusThreadInOpenView: (threadId: string) => Promise<boolean>;
@ -9,12 +9,12 @@ export interface ThreadSelectionActionsHost {
addSystemMessage: (text: string) => void;
}
export interface ThreadSelectionActions {
export interface SelectionActions {
selectThread(threadId: string): Promise<void>;
selectThreadFromToolbar(threadId: string): Promise<void>;
}
export function createThreadSelectionActions(host: ThreadSelectionActionsHost): ThreadSelectionActions {
export function createSelectionActions(host: SelectionActionsHost): SelectionActions {
const selectThread = async (threadId: string): Promise<void> => {
if (!canSwitchToThread(host.stateStore.getState(), threadId)) {
host.addSystemMessage("Finish or interrupt the current turn before switching threads.");

View file

@ -1,13 +1,13 @@
import type { ChatState } from "../../state/reducer";
import { chatTurnBusy } from "../../state/reducer";
import type { DisplayItem } from "../../display/types";
import { implementPlanCandidateFromState } from "../../state/selectors";
import {
forkCandidatesFromItems,
implementPlanCandidateFromState,
isForkCandidateItem,
isRollbackCandidateItem,
rollbackCandidateFromItems,
} from "../../display/action-candidates";
} from "../../display/item-actions";
import type { MessageStreamContext } from "./context";
import type { ChatMessageStreamContextPort } from "./ports";

View file

@ -3,7 +3,7 @@ import type { ComponentChild as UiNode } from "preact";
import type { ChatTurnLifecycleState } from "../../state/reducer";
import type { PendingRequestBlockSnapshot } from "../../conversation/pending-requests/snapshot";
import type { DisplayItem } from "../../display/types";
import type { PendingRequestMessageActions } from "../pending-request-message";
import type { PendingRequestBlockActions } from "../pending-request-block";
import type { ChatTurnDiffViewState } from "../../turn-diff/model";
export interface MessageStreamBlock {
@ -11,18 +11,18 @@ export interface MessageStreamBlock {
node: UiNode;
}
export type RenderableTextItem = Extract<DisplayItem, { kind: "message" | "system" | "userInputResult" }>;
export type TextDisplayItem = Extract<DisplayItem, { kind: "message" | "system" | "userInputResult" }>;
export interface MessageDetailStateContext {
export interface TextItemDetailStateContext {
openDetails: ReadonlySet<string>;
onDetailsToggle?: (key: string, open: boolean) => void;
}
export interface MessageContentContext extends MessageDetailStateContext {
export interface TextItemContentContext extends TextItemDetailStateContext {
renderMarkdown: (parent: HTMLElement, text: string) => void;
}
export interface MessageActionContext extends MessageDetailStateContext {
export interface TextItemActionContext extends TextItemDetailStateContext {
turnLifecycle: ChatTurnLifecycleState;
copyText?: (text: string) => void;
canImplementPlanItem?: (item: DisplayItem) => boolean;
@ -33,7 +33,7 @@ export interface MessageActionContext extends MessageDetailStateContext {
onForkItem?: (item: DisplayItem, archiveSource: boolean) => void;
}
export interface MessageMetadataContext extends MessageDetailStateContext {
export interface TextItemMetadataContext extends TextItemDetailStateContext {
activeThreadId: string | null;
workspaceRoot?: string | null;
openTurnDiff?: (state: ChatTurnDiffViewState) => void;
@ -51,13 +51,13 @@ interface MessageStreamLayoutContext {
pendingRequests?: PendingRequestBlockContext;
}
export interface MessageItemContext extends MessageContentContext, MessageActionContext, MessageMetadataContext {}
export interface TextItemContext extends TextItemContentContext, TextItemActionContext, TextItemMetadataContext {}
export interface MessageStreamContext extends MessageStreamLayoutContext, MessageItemContext {}
export interface MessageStreamContext extends MessageStreamLayoutContext, TextItemContext {}
export interface PendingRequestBlockContext {
signature: string;
snapshot: () => PendingRequestBlockSnapshot;
actions: () => PendingRequestMessageActions;
actions: () => PendingRequestBlockActions;
consumeAutoFocus: () => boolean;
}

View file

@ -2,7 +2,7 @@ import type { ChatAction, ChatState } from "../../state/reducer";
import type { PendingRequestBlockSnapshot } from "../../conversation/pending-requests/snapshot";
import type { DisplayItem } from "../../display/types";
import type { ChatTurnDiffViewState } from "../../turn-diff/model";
import type { PendingRequestMessageActions } from "../pending-request-message";
import type { PendingRequestBlockActions } from "../pending-request-block";
export interface ChatMessageStreamActionPort {
rollbackThread: (threadId: string) => void;
@ -14,7 +14,7 @@ export interface ChatMessageStreamActionPort {
export interface ChatMessageStreamRequestPort {
pendingSignature: () => string;
pendingSnapshot: () => PendingRequestBlockSnapshot;
pendingActions: () => PendingRequestMessageActions;
pendingActions: () => PendingRequestBlockActions;
consumePendingAutoFocus: () => boolean;
}

View file

@ -2,21 +2,21 @@ import { Fragment, type ComponentChild as UiNode } from "preact";
import { useLayoutEffect, useState } from "preact/hooks";
import { activeTurnId } from "../../state/reducer";
import { displayBlocksForItems } from "../../display/blocks";
import type { ToolResultDisplayItem } from "../../display/tool-view";
import { displayBlocksForItems } from "../../display/stream/blocks";
import type { ToolResultDisplayItem } from "../tool-result-view";
import type { DisplayBlock, DisplayItem } from "../../display/types";
import { userInputDraftKey, userInputOtherDraftKey } from "../../protocol/requests/user-input";
import { pendingRequestMessageNode } from "../pending-request-message";
import { pendingRequestBlockNode } from "../pending-request-block";
import { toolResultNode } from "../tool-result";
import { activeAgentRunSummaryBlock, agentRunSummaryNode, workItemNode, type WorkItemDisplayItem } from "../work-items";
import type { MessageStreamBlock, MessageStreamContext, RenderableTextItem } from "./context";
import { messageItemNode } from "./message-item";
import type { MessageStreamBlock, MessageStreamContext, TextDisplayItem } from "./context";
import { textItemNode } from "./text-item";
function messageStreamActiveTurnId(context: Pick<MessageStreamContext, "turnLifecycle">): string | null {
return activeTurnId({ lifecycle: context.turnLifecycle });
}
function isRenderableTextItem(item: DisplayItem): item is RenderableTextItem {
function isTextDisplayItem(item: DisplayItem): item is TextDisplayItem {
return item.kind === "message" || item.kind === "system" || item.kind === "userInputResult";
}
@ -37,7 +37,7 @@ function isRenderableWorkItem(item: DisplayItem): item is WorkItemDisplayItem {
}
function displayItemNode(item: DisplayItem, context: MessageStreamContext): UiNode {
if (isRenderableTextItem(item)) return messageItemNode(item, context);
if (isTextDisplayItem(item)) return textItemNode(item, context);
if (isRenderableToolResultItem(item)) return toolResultNode(item, context);
if (isRenderableWorkItem(item)) return workItemNode(item, context);
}
@ -89,7 +89,7 @@ function bottomLiveBlocks(context: MessageStreamContext, activeTurn: string | nu
const snapshot = context.pendingRequests.snapshot();
blocks.push({
key: "pending-requests",
node: pendingRequestMessageNode(
node: pendingRequestBlockNode(
snapshot.approvals,
snapshot.pendingUserInputs,
{

View file

@ -4,9 +4,9 @@ import { useEffect, useRef } from "preact/hooks";
import { activeTurnId } from "../../state/reducer";
import type { DisplayItem } from "../../display/types";
import { IconButton } from "../../../../shared/ui/components";
import type { MessageActionContext, RenderableTextItem } from "./context";
import type { TextItemActionContext, TextDisplayItem } from "./context";
export function MessageRole({ item, context }: { item: RenderableTextItem; context: MessageActionContext }): UiNode {
export function TextItemHeader({ item, context }: { item: TextDisplayItem; context: TextItemActionContext }): UiNode {
const forkActionsKey = `message:fork-actions:${item.id}`;
const forkActionsOpen = context.openDetails.has(forkActionsKey);
const roleRef = useRef<HTMLDivElement | null>(null);
@ -27,7 +27,7 @@ export function MessageRole({ item, context }: { item: RenderableTextItem; conte
const copyAction =
item.kind === "message" && context.copyText && isMessageCopyActionVisible(item, context) && !forkActionsOpen ? (
<MessageAction
<TextItemAction
icon="copy"
label="Copy message"
className="codex-panel__copy-message"
@ -39,7 +39,7 @@ export function MessageRole({ item, context }: { item: RenderableTextItem; conte
<div ref={roleRef} className={`codex-panel__message-role${forkActionsOpen ? " codex-panel__message-role--fork-open" : ""}`}>
<span>{displayRoleLabel(item)}</span>
{forkActionsOpen && context.canForkItem?.(item) ? (
<MessageAction
<TextItemAction
icon="archive"
label="Fork and archive"
className="codex-panel__fork-and-archive-message"
@ -52,7 +52,7 @@ export function MessageRole({ item, context }: { item: RenderableTextItem; conte
copyAction
)}
{context.canForkItem?.(item) ? (
<MessageAction
<TextItemAction
icon="git-fork"
label={forkActionsOpen ? "Fork" : "Fork from here"}
className="codex-panel__fork-message"
@ -67,7 +67,7 @@ export function MessageRole({ item, context }: { item: RenderableTextItem; conte
/>
) : null}
{context.canImplementPlanItem?.(item) ? (
<MessageAction
<TextItemAction
icon="play"
label="Implement plan"
className="codex-panel__implement-plan"
@ -75,7 +75,7 @@ export function MessageRole({ item, context }: { item: RenderableTextItem; conte
/>
) : null}
{context.canRollbackItem?.(item) ? (
<MessageAction
<TextItemAction
icon="undo-2"
label="Rollback last turn"
className="codex-panel__rollback-turn"
@ -86,7 +86,7 @@ export function MessageRole({ item, context }: { item: RenderableTextItem; conte
);
}
function MessageAction({
function TextItemAction({
icon,
label,
className,
@ -120,7 +120,7 @@ function displayRoleLabel(item: DisplayItem): string {
return "System";
}
function isMessageCopyActionVisible(item: DisplayItem, context: Pick<MessageActionContext, "turnLifecycle">): boolean {
function isMessageCopyActionVisible(item: DisplayItem, context: Pick<TextItemActionContext, "turnLifecycle">): boolean {
if (item.kind !== "message" || item.copyText === undefined) return false;
const activeTurn = activeTurnId({ lifecycle: context.turnLifecycle });
return !(activeTurn && item.role === "assistant" && item.turnId === activeTurn);

View file

@ -2,7 +2,7 @@ import { Fragment, type ComponentChild as UiNode } from "preact";
import type { DisplayDetailSection, DisplayItem } from "../../display/types";
import { IconButton } from "../../../../shared/ui/components";
import type { MessageDetailStateContext, MessageMetadataContext } from "./context";
import type { TextItemDetailStateContext, TextItemMetadataContext } from "./context";
export function ReferencedThread({ item }: { item: Extract<DisplayItem, { kind: "message" }> }): UiNode {
const reference = item.referencedThread;
@ -26,7 +26,7 @@ export function EditedFiles({
context,
}: {
item: Extract<DisplayItem, { kind: "message" }>;
context: MessageMetadataContext;
context: TextItemMetadataContext;
}): UiNode {
const editedFiles = item.editedFiles ?? [];
const label = editedFiles.length === 1 ? "Edited 1 file" : `Edited ${String(editedFiles.length)} files`;
@ -76,7 +76,7 @@ export function MentionedFiles({
context,
}: {
item: Extract<DisplayItem, { kind: "message" }>;
context: MessageDetailStateContext;
context: TextItemDetailStateContext;
}): UiNode {
const mentionedFiles = item.mentionedFiles ?? [];
const label = mentionedFiles.length === 1 ? "Mentioned 1 file" : `Mentioned ${String(mentionedFiles.length)} files`;
@ -115,14 +115,14 @@ export function AutoReviewSummaries({ summaries }: { summaries: string[] }): UiN
);
}
export function MessageDetails({
itemId,
export function TextItemDetails({
displayItemId,
details,
context,
}: {
itemId: string;
displayItemId: string;
details: DisplayDetailSection[];
context: MessageDetailStateContext;
context: TextItemDetailStateContext;
}): UiNode {
return (
<>
@ -130,7 +130,7 @@ export function MessageDetails({
<RememberedDetails
key={`${section.title ?? "Details"}:${String(index)}`}
detailsClassName="codex-panel__output"
detailsKey={`${itemId}:message-detail:${String(index)}`}
detailsKey={`${displayItemId}:text-item-detail:${String(index)}`}
summary={section.title ?? "Details"}
context={context}
>
@ -184,7 +184,7 @@ function RememberedDetails({
detailsClassName: string;
detailsKey: string;
summary: string;
context: MessageDetailStateContext;
context: TextItemDetailStateContext;
children: UiNode;
}): UiNode {
const details = (

View file

@ -3,26 +3,26 @@ import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
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";
import { AutoReviewSummaries, EditedFiles, MentionedFiles, MessageDetails, ReferencedThread, SystemDetails } from "./message-metadata";
import type { TextItemContentContext, TextItemContext, TextDisplayItem } from "./context";
import { TextItemHeader } from "./text-item-actions";
import { AutoReviewSummaries, EditedFiles, MentionedFiles, TextItemDetails, ReferencedThread, SystemDetails } from "./text-item-metadata";
const USER_MESSAGE_COLLAPSE_HEIGHT_PX = 360;
export function messageItemNode(item: RenderableTextItem, context: MessageItemContext): UiNode {
return <MessageItem item={item} context={context} />;
export function textItemNode(item: TextDisplayItem, context: TextItemContext): UiNode {
return <TextItem item={item} context={context} />;
}
function MessageItem({ item, context }: { item: RenderableTextItem; context: MessageItemContext }): UiNode {
function TextItem({ item, context }: { item: TextDisplayItem; context: TextItemContext }): UiNode {
const collapsible = isCollapsibleUserMessage(item);
const details = "details" in item ? item.details : undefined;
return (
<div className={`${messageClass(item)}${executionClassName(item.executionState ?? null)}`}>
<MessageRole item={item} context={context} />
<div className={`${textItemClass(item)}${executionClassName(item.executionState ?? null)}`}>
<TextItemHeader item={item} context={context} />
{collapsible ? (
<CollapsibleMessageContent item={item} context={context} />
<CollapsibleTextItemContent item={item} context={context} />
) : (
<TextContent key={messageContentKey(item)} item={item} context={context} />
<TextContent key={textItemContentKey(item)} item={item} context={context} />
)}
{item.kind === "message" && item.editedFiles && item.editedFiles.length > 0 ? <EditedFiles item={item} context={context} /> : null}
{item.kind === "message" && item.referencedThread ? <ReferencedThread item={item} /> : null}
@ -35,14 +35,14 @@ function MessageItem({ item, context }: { item: RenderableTextItem; context: Mes
{item.kind === "system" && item.details && item.details.length > 0 ? (
<SystemDetails details={item.details} />
) : details && details.length > 0 ? (
<MessageDetails itemId={item.id} details={details} context={context} />
<TextItemDetails displayItemId={item.id} details={details} context={context} />
) : null}
</div>
);
}
function CollapsibleMessageContent({ item, context }: { item: RenderableTextItem; context: MessageContentContext }): UiNode {
const key = `message:${item.id}:expanded`;
function CollapsibleTextItemContent({ item, context }: { item: TextDisplayItem; context: TextItemContentContext }): UiNode {
const key = `text-item:${item.id}:expanded`;
const renderModeKey = contentRenderMode(item);
const collapseRef = useRef<HTMLDivElement | null>(null);
const contentRef = useRef<HTMLDivElement | null>(null);
@ -93,7 +93,7 @@ function CollapsibleMessageContent({ item, context }: { item: RenderableTextItem
.filter(Boolean)
.join(" ")}
>
<TextContent key={messageContentKey(item)} item={item} context={context} contentRef={contentRef} collapsed={overflows && !expanded} />
<TextContent key={textItemContentKey(item)} item={item} context={context} contentRef={contentRef} collapsed={overflows && !expanded} />
<details
className="codex-panel__message-collapse-details"
hidden={!overflows || expanded}
@ -111,8 +111,8 @@ function CollapsibleMessageContent({ item, context }: { item: RenderableTextItem
}
interface TextContentProps {
item: RenderableTextItem;
context: MessageContentContext;
item: TextDisplayItem;
context: TextItemContentContext;
contentRef?: Ref<HTMLDivElement>;
collapsed?: boolean;
}
@ -158,11 +158,11 @@ function TextContent({ item, context, contentRef, collapsed = false }: TextConte
);
}
function messageContentKey(item: RenderableTextItem): string {
function textItemContentKey(item: TextDisplayItem): string {
return `${item.id}\u001f${contentRenderMode(item)}\u001f${item.text}`;
}
function contentRenderMode(item: RenderableTextItem): "markdown" | "text" {
function contentRenderMode(item: TextDisplayItem): "markdown" | "text" {
if (item.kind !== "message") return "text";
return item.messageKind === "proposedPlan" && item.messageState === "streaming" ? "text" : "markdown";
}
@ -181,7 +181,7 @@ function userMessageCollapseHeight(element: HTMLElement): number {
return Math.min(USER_MESSAGE_COLLAPSE_HEIGHT_PX, viewportHeight * 0.45);
}
function messageClass(item: DisplayItem): string {
function textItemClass(item: DisplayItem): string {
const classes = ["codex-panel__message", `codex-panel__message--${item.role}`];
if (item.kind === "approvalResult") classes.push("codex-panel__message--approval-result");
if (item.kind === "userInputResult") classes.push("codex-panel__message--user-input-result");

View file

@ -15,7 +15,7 @@ import { createWorkMessageClassName } from "./work-message";
type PendingUserInputRequestId = PendingUserInput["requestId"];
export interface PendingRequestMessageActions {
export interface PendingRequestBlockActions {
resolveApproval: (approval: PendingApproval, action: ApprovalAction) => void;
resolveUserInput: (input: PendingUserInput) => void;
cancelUserInput: (input: PendingUserInput) => void;
@ -23,24 +23,24 @@ export interface PendingRequestMessageActions {
setUserInputDraft: (key: string, value: string) => void;
}
export interface PendingRequestMessageDrafts {
export interface PendingRequestBlockDrafts {
values: ReadonlyMap<string, string>;
draftKey: (requestId: PendingUserInputRequestId, questionId: string) => string;
otherDraftKey: (requestId: PendingUserInputRequestId, questionId: string) => string;
}
export function pendingRequestMessageNode(
export function pendingRequestBlockNode(
approvals: readonly PendingApproval[],
pendingUserInputs: readonly PendingUserInput[],
drafts: PendingRequestMessageDrafts,
drafts: PendingRequestBlockDrafts,
openDetails: ReadonlySet<string>,
actions: PendingRequestMessageActions,
actions: PendingRequestBlockActions,
autoFocusRequested = false,
consumeAutoFocus?: () => boolean,
autoFocusSignature = "",
): UiNode {
return (
<PendingRequestMessage
<PendingRequestBlock
approvals={approvals}
pendingUserInputs={pendingUserInputs}
drafts={drafts}
@ -53,7 +53,7 @@ export function pendingRequestMessageNode(
);
}
function PendingRequestMessage({
function PendingRequestBlock({
approvals,
pendingUserInputs,
drafts,
@ -65,9 +65,9 @@ function PendingRequestMessage({
}: {
approvals: readonly PendingApproval[];
pendingUserInputs: readonly PendingUserInput[];
drafts: PendingRequestMessageDrafts;
drafts: PendingRequestBlockDrafts;
openDetails: ReadonlySet<string>;
actions: PendingRequestMessageActions;
actions: PendingRequestBlockActions;
autoFocusRequested: boolean;
consumeAutoFocus: (() => boolean) | undefined;
autoFocusSignature: string;
@ -81,7 +81,7 @@ function PendingRequestMessage({
}, [autoFocusRequested, consumeAutoFocus, autoFocusSignature]);
if (approvals.length === 0 && pendingUserInputs.length === 0) return null;
return (
<div ref={requestRef} className={createWorkMessageClassName("codex-panel__pending-request-message", "warning")}>
<div ref={requestRef} className={createWorkMessageClassName("codex-panel__pending-request-block", "warning")}>
<div className="codex-panel__message-role">Request</div>
{approvals.map((approval) => (
<ApprovalCard key={String(approval.requestId)} approval={approval} openDetails={openDetails} actions={actions} />
@ -116,7 +116,7 @@ function ApprovalCard({
}: {
approval: PendingApproval;
openDetails: ReadonlySet<string>;
actions: PendingRequestMessageActions;
actions: PendingRequestBlockActions;
}): UiNode {
return (
<PendingRequestCard className="codex-panel__approval">
@ -148,7 +148,7 @@ function ApprovalDetails({
}: {
approval: PendingApproval;
openDetails: ReadonlySet<string>;
actions: PendingRequestMessageActions;
actions: PendingRequestBlockActions;
}): UiNode {
const key = `approval:${String(approval.requestId)}:details`;
return (
@ -175,8 +175,8 @@ function UserInputCard({
actions,
}: {
input: PendingUserInput;
drafts: PendingRequestMessageDrafts;
actions: PendingRequestMessageActions;
drafts: PendingRequestBlockDrafts;
actions: PendingRequestBlockActions;
}): UiNode {
return (
<PendingRequestCard className="codex-panel__user-input">
@ -217,8 +217,8 @@ function UserInputQuestions({
actions,
}: {
input: PendingUserInput;
drafts: PendingRequestMessageDrafts;
actions: PendingRequestMessageActions;
drafts: PendingRequestBlockDrafts;
actions: PendingRequestBlockActions;
}): UiNode {
return (
<>
@ -301,8 +301,8 @@ function OtherUserInputOption({
groupName: string;
current: string;
optionLabels: ReadonlySet<string>;
drafts: PendingRequestMessageDrafts;
actions: PendingRequestMessageActions;
drafts: PendingRequestBlockDrafts;
actions: PendingRequestBlockActions;
}): UiNode {
const draftKey = drafts.draftKey(input.requestId, questionId);
const otherKey = drafts.otherDraftKey(input.requestId, questionId);
@ -382,8 +382,8 @@ function FreeformUserInput({
questionText: string;
isSecret: boolean;
current: string;
drafts: PendingRequestMessageDrafts;
actions: PendingRequestMessageActions;
drafts: PendingRequestBlockDrafts;
actions: PendingRequestBlockActions;
}): UiNode {
const draftKey = drafts.draftKey(input.requestId, questionId);
return (

View file

@ -1,4 +1,4 @@
import { pathRelativeToRoot } from "./paths";
import { pathRelativeToRoot } from "../display/details/path-labels";
import { definedProp } from "../../../utils";
import type {
ApprovalResultDisplayItem,
@ -12,7 +12,7 @@ import type {
HookDisplayItem,
ReviewResultDisplayItem,
ToolCallDisplayItem,
} from "./types";
} from "../display/types";
export type ToolResultDisplayItem =
| CommandDisplayItem

View file

@ -1,7 +1,7 @@
import type { ComponentChild as UiNode } from "preact";
import { useLayoutEffect, useRef, useState } from "preact/hooks";
import { toolResultView, type ToolResultDetailSection, type ToolResultDisplayItem, type ToolResultView } from "../display/tool-view";
import { toolResultView, type ToolResultDetailSection, type ToolResultDisplayItem, type ToolResultView } from "./tool-result-view";
import { renderRawDiffLines } from "../../../shared/diff/render";
export interface ToolResultRenderContext {

View file

@ -1,7 +1,7 @@
import type { ButtonHTMLAttributes, ComponentChild as UiNode, TargetedKeyboardEvent } from "preact";
import { useLayoutEffect, useRef } from "preact/hooks";
import type { RuntimeConfigSection, RateLimitSummary } from "../display/runtime-status";
import type { RuntimeConfigSection, RateLimitSummary } from "../display/status/runtime";
import { IconButton } from "../../../shared/ui/components";
type ButtonProps = ButtonHTMLAttributes & {

View file

@ -1,8 +1,7 @@
import type { ComponentChild as UiNode } from "preact";
import { useLayoutEffect, useState } from "preact/hooks";
import { activeAgentRunSummary, agentActivityMetaLabel, agentMessagePreview, agentRunSummaryLabel } from "../display/agent";
import { taskStatusMarker } from "../display/plan";
import { activeAgentRunSummary, agentActivityMetaLabel, agentMessagePreview, agentRunSummaryLabel } from "../display/stream/agent-summary";
import type {
AgentDisplayItem,
AgentRunSummary,
@ -77,6 +76,12 @@ function TaskProgressItem({ item }: { item: TaskProgressDisplayItem }): UiNode {
);
}
function taskStatusMarker(status: TaskProgressDisplayItem["steps"][number]["status"]): string {
if (status === "completed") return "[x]";
if (status === "inProgress") return "[>]";
return "[ ]";
}
function ContextCompactionItem({ item, context }: { item: ContextCompactionDisplayItem; context: WorkItemContext }): UiNode {
const active = workItemsActiveTurnId(context) === item.turnId;
return (

View file

@ -12,12 +12,12 @@ import { chatTurnBusy, createChatStateStore, type ChatState, type ChatAction } f
import type { OpenCodexPanelSnapshot } from "../../workspace/open-panel-snapshot";
import type { SharedAppServerMetadata } from "../../app-server/shared-cache-state";
import type { CodexChatHost } from "./chat-host";
import { createStructuredSystemItem, createSystemItem } from "./display/system";
import { createStructuredSystemItem, createSystemItem } from "./display/items/system";
import {
effortStatusLines as buildEffortStatusLines,
modelStatusLines as buildModelStatusLines,
statusSummaryLines as buildStatusSummaryLines,
} from "./display/runtime-status";
} from "./display/status/runtime";
import { runtimeSnapshotForChatState } from "./runtime/snapshot";
import { codexPanelDisplayTitle, getThreadTitle } from "../../domain/threads/model";
import { connectionDiagnosticsModel } from "./panel/regions/toolbar";
@ -574,19 +574,19 @@ export class CodexChatView extends ItemView {
}
private async ensureRestoredThreadLoaded(): Promise<boolean> {
return this.controllers.thread.restored.ensureLoaded();
return this.controllers.thread.restoration.ensureLoaded();
}
private isRestoredThreadPending(threadId: string): boolean {
return this.controllers.thread.restored.isPending(threadId);
return this.controllers.thread.restoration.isPending(threadId);
}
private scheduleDeferredRestoredThreadHydration(): void {
this.controllers.thread.restored.scheduleHydration();
this.controllers.thread.restoration.scheduleHydration();
}
private clearDeferredRestoredThreadHydration(): void {
this.controllers.thread.restored.clearHydration();
this.controllers.thread.restoration.clearHydration();
}
private scheduleDeferredAppServerWarmup(): void {
@ -601,11 +601,11 @@ export class CodexChatView extends ItemView {
}
private restoredThreadPlaceholder() {
return this.controllers.thread.restored.placeholder();
return this.controllers.thread.restoration.placeholder();
}
private restoredThreadTitle(): string | null {
return this.controllers.thread.restored.title();
return this.controllers.thread.restoration.title();
}
private closeToolbarPanelOnOutsidePointer(event: PointerEvent): void {

View file

@ -1,4 +1,4 @@
.codex-panel__pending-request-message {
.codex-panel__pending-request-block {
display: flex;
flex-direction: column;
gap: var(--codex-panel-item-gap);

View file

@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest";
import type { AppServerClient } from "../../../../../src/app-server/client";
import { createChatState, createChatStateStore, type ChatStateStore } from "../../../../../src/features/chat/state/reducer";
import { implementPlanCandidateFromState } from "../../../../../src/features/chat/display/action-candidates";
import { implementPlanCandidateFromState } from "../../../../../src/features/chat/state/selectors";
import {
createPlanImplementationActions,
type PlanImplementationActionsHost,

View file

@ -7,7 +7,7 @@ import {
upsertMcpServerDiagnostic,
upsertMcpServerStatusDiagnostics,
} from "../../../src/app-server/diagnostics";
import { connectionDiagnosticSections, hasDiagnosticIssue } from "../../../src/features/chat/display/diagnostics";
import { connectionDiagnosticSections, hasDiagnosticIssue } from "../../../src/features/chat/display/status/diagnostics";
describe("connection diagnostics", () => {
it("formats base rows, capability probes, and MCP issues for /doctor", () => {

View file

@ -1,7 +1,8 @@
import { describe, expect, it } from "vitest";
import { activeAgentRunSummary, collabAgentStateExecutionState } from "../../../../src/features/chat/display/agent";
import { displayBlocksForItems } from "../../../../src/features/chat/display/blocks";
import { collabAgentStateExecutionState } from "../../../../src/features/chat/display/items/agent";
import { activeAgentRunSummary } from "../../../../src/features/chat/display/stream/agent-summary";
import { displayBlocksForItems } from "../../../../src/features/chat/display/stream/blocks";
import {
appendAssistantDelta,
appendItemOutput,
@ -9,22 +10,22 @@ import {
appendPlanDelta,
appendToolOutput,
upsertDisplayItem,
} from "../../../../src/features/chat/display/stream-updates";
} from "../../../../src/features/chat/state/transcript-updates";
import {
normalizeProposedPlanMarkdown,
planProgressDisplayItem,
taskProgressDisplayItem,
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";
} from "../../../../src/features/chat/display/items/task-progress";
import { normalizeProposedPlanMarkdown } from "../../../../src/features/chat/display/items/proposed-plan";
import { pathRelativeToRoot } from "../../../../src/features/chat/display/details/path-labels";
import { permissionRows } from "../../../../src/features/chat/display/details/permission-rows";
import { autoReviewExecutionState, createAutoReviewResultItem, createReviewResultItem } from "../../../../src/features/chat/display/items/review-result";
import {
commandExecutionState,
dynamicToolCallExecutionState,
mcpToolCallExecutionState,
patchApplyExecutionState,
} from "../../../../src/features/chat/protocol/display-items";
import { displayItemFromThreadItem, displayItemsFromTurns } from "../../../../src/features/chat/protocol/display-items";
} from "../../../../src/features/chat/display/turn-items";
import { displayItemFromThreadItem, displayItemsFromTurns } from "../../../../src/features/chat/display/turn-items";
import { referencedThreadPrompt } from "../../../../src/domain/threads/reference";
import type { DisplayItem } from "../../../../src/features/chat/display/types";
import type { Thread } from "../../../../src/domain/threads/model";
@ -244,7 +245,7 @@ describe("thread item conversion preserves app-server semantics", () => {
it("formats structured plan progress as task progress", () => {
expect(
planProgressDisplayItem("t1", "Working plan", [
taskProgressDisplayItem("t1", "Working plan", [
{ step: "Inspect code", status: "completed" },
{ step: "Patch UI", status: "inProgress" },
{ step: "Run tests", status: "pending" },
@ -829,7 +830,7 @@ describe("thread item conversion preserves app-server semantics", () => {
kind: "contextCompaction",
text: "Context compaction",
turnId: "t1",
itemId: "compact-1",
sourceItemId: "compact-1",
});
});
@ -920,14 +921,14 @@ describe("streaming updates target item identity without mutating history", () =
const items: DisplayItem[] = [
{
id: "a1",
itemId: "a1",
sourceItemId: "a1",
kind: "message",
role: "assistant",
text: "hello",
messageKind: "assistantResponse",
messageState: "completed",
},
{ id: "tool1", itemId: "tool1", kind: "tool", role: "tool", text: "tool" },
{ id: "tool1", sourceItemId: "tool1", kind: "tool", role: "tool", text: "tool" },
];
const updated = appendAssistantDelta(items, "a1", "t1", " world");
@ -939,10 +940,10 @@ describe("streaming updates target item identity without mutating history", () =
});
it("appends tool text and output without mutating existing display items", () => {
const tool: DisplayItem = { id: "tool1", itemId: "tool1", kind: "tool", role: "tool", text: "plan: " };
const tool: DisplayItem = { id: "tool1", sourceItemId: "tool1", kind: "tool", role: "tool", text: "plan: " };
const command: DisplayItem = {
id: "cmd1",
itemId: "cmd1",
sourceItemId: "cmd1",
kind: "command",
role: "tool",
text: "Command running",
@ -1570,7 +1571,7 @@ describe("execution state uses typed status adapters before rendered text", () =
it("does not overwrite streamed output with an empty completed item", () => {
const streamed: DisplayItem = {
id: "c1",
itemId: "c1",
sourceItemId: "c1",
kind: "command",
role: "tool",
text: "Command running",
@ -1581,7 +1582,7 @@ describe("execution state uses typed status adapters before rendered text", () =
};
const completed: DisplayItem = {
id: "c1",
itemId: "c1",
sourceItemId: "c1",
kind: "command",
role: "tool",
text: "npm test",

View file

@ -6,7 +6,7 @@ import {
isRollbackCandidateItem,
rollbackCandidateFromItems,
turnsAfterTurnId,
} from "../../../../src/features/chat/display/action-candidates";
} from "../../../../src/features/chat/display/item-actions";
import type { DisplayItem } from "../../../../src/features/chat/display/types";
describe("fork candidates", () => {
@ -56,9 +56,9 @@ describe("fork candidates", () => {
const candidates = forkCandidatesFromItems(items);
expect(candidates).toEqual([
{ itemId: "a1", turnId: "turn-1" },
{ itemId: "a2", turnId: "turn-2" },
{ itemId: "a3", turnId: "turn-3" },
{ displayItemId: "a1", turnId: "turn-1" },
{ displayItemId: "a2", turnId: "turn-2" },
{ displayItemId: "a3", turnId: "turn-3" },
]);
expect(isForkCandidateItem(expectPresent(items[4]), candidates)).toBe(true);
expect(isForkCandidateItem(expectPresent(items[3]), candidates)).toBe(false);
@ -96,7 +96,7 @@ describe("rollback candidate", () => {
const candidate = rollbackCandidateFromItems(items);
expect(candidate).toEqual({ turnId: "turn-2", itemId: "u2", text: "latest" });
expect(candidate).toEqual({ turnId: "turn-2", displayItemId: "u2", text: "latest" });
expect(isRollbackCandidateItem(expectPresent(items[2]), candidate)).toBe(true);
expect(isRollbackCandidateItem(expectPresent(items[0]), candidate)).toBe(false);
expect(isRollbackCandidateItem({ ...expectPresent(items[2]), turnId: "turn-other" }, candidate)).toBe(false);

View file

@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import type { ThreadGoal } from "../../../../src/app-server/thread-goal";
import { goalChangeItem } from "../../../../src/features/chat/display/goal-messages";
import type { ThreadGoal } from "../../../../../src/app-server/thread-goal";
import { goalChangeItem } from "../../../../../src/features/chat/display/items/goal";
describe("goal display items", () => {
it("keeps goal event summaries compact while retaining the full objective in details", () => {

View file

@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import type { McpServerStatusSummary } from "../../../src/app-server/diagnostics";
import { mcpStatusLines } from "../../../src/features/chat/display/diagnostics";
import { mcpStatusLines } from "../../../src/features/chat/display/status/diagnostics";
function mcpServer(overrides: Partial<McpServerStatusSummary> = {}): McpServerStatusSummary {
return {

View file

@ -4,7 +4,7 @@ import { describe, expect, it, vi } from "vitest";
import { createChatState, createChatStateStore } from "../../../../../src/features/chat/state/reducer";
import { createToolbarArchiveConfirmState, ToolbarPanelController } from "../../../../../src/features/chat/panel/regions/toolbar";
import type { ChatThreadActions } from "../../../../../src/features/chat/threads/thread-actions";
import type { ChatThreadActions } from "../../../../../src/features/chat/threads/actions";
describe("ToolbarPanelController", () => {
it("tracks archive confirmation and delegates archive actions", async () => {

View file

@ -1,7 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import { ChatInboundController } from "../../../../../src/features/chat/protocol/inbound/controller";
import { attachHookRunsToTurn } from "../../../../../src/features/chat/display/hooks";
import { attachHookRunsToTurn } from "../../../../../src/features/chat/state/transcript-updates";
import {
activeTurnId,
chatReducer,
@ -1105,7 +1105,7 @@ describe("ChatInboundController", () => {
{ id: "local-user-1", kind: "message", messageKind: "user", role: "user", text: "hello", turnId: "turn-active" },
{
id: "a1",
itemId: "a1",
sourceItemId: "a1",
kind: "message",
role: "assistant",
messageKind: "assistantResponse",
@ -1241,7 +1241,7 @@ describe("ChatInboundController", () => {
{ id: "local-user-1", kind: "message", messageKind: "user", role: "user", text: "start", turnId: "turn-active" },
{
id: "a1",
itemId: "a1",
sourceItemId: "a1",
kind: "message",
role: "assistant",
messageKind: "assistantResponse",

View file

@ -4,7 +4,7 @@ import type { AppServerClient } from "../../../../src/app-server/client";
import type { AppServerThread } from "../../../../src/app-server/thread-model";
import type { ArchiveExportAdapter } from "../../../../src/domain/threads/export";
import { createChatState, createChatStateStore } from "../../../../src/features/chat/state/reducer";
import { createChatThreadActions, type ChatThreadActionsHost } from "../../../../src/features/chat/threads/thread-actions";
import { createChatThreadActions, type ChatThreadActionsHost } from "../../../../src/features/chat/threads/actions";
import type { DisplayItem } from "../../../../src/features/chat/display/types";
import { DEFAULT_SETTINGS } from "../../../../src/settings/model";
import { notices } from "../../../mocks/obsidian";

View file

@ -2,11 +2,11 @@ import { describe, expect, it, vi } from "vitest";
import type { AppServerClient } from "../../../../src/app-server/client";
import type { ThreadGoal } from "../../../../src/app-server/thread-goal";
import { createChatThreadGoalActions } from "../../../../src/features/chat/threads/thread-goal-actions";
import { createGoalActions } from "../../../../src/features/chat/threads/goal-actions";
import { createChatState, createChatStateStore } from "../../../../src/features/chat/state/reducer";
import { deferred } from "../../../support/async";
describe("createChatThreadGoalActions", () => {
describe("createGoalActions", () => {
it("syncs the active thread goal into chat state", async () => {
const state = createChatState();
state.activeThread.id = "thread";
@ -15,7 +15,7 @@ describe("createChatThreadGoalActions", () => {
const client = { getThreadGoal: vi.fn().mockResolvedValue({ goal: currentGoal }) } as unknown as AppServerClient;
const render = vi.fn();
const refreshLiveState = vi.fn();
const controller = createChatThreadGoalActions({
const controller = createGoalActions({
stateStore,
currentClient: () => client,
ensureConnected: vi.fn().mockResolvedValue(undefined),
@ -38,7 +38,7 @@ describe("createChatThreadGoalActions", () => {
const stateStore = createChatStateStore(state);
const addSystemMessage = vi.fn();
const client = { getThreadGoal: vi.fn().mockRejectedValue(new Error("offline")) } as unknown as AppServerClient;
const controller = createChatThreadGoalActions({
const controller = createGoalActions({
stateStore,
currentClient: () => client,
ensureConnected: vi.fn().mockResolvedValue(undefined),
@ -70,7 +70,7 @@ describe("createChatThreadGoalActions", () => {
} as unknown as AppServerClient;
const addSystemMessage = vi.fn();
const addGoalEvent = vi.fn();
const controller = createChatThreadGoalActions({
const controller = createGoalActions({
stateStore,
currentClient: () => client,
ensureConnected: vi.fn().mockResolvedValue(undefined),
@ -102,7 +102,7 @@ describe("createChatThreadGoalActions", () => {
const update = deferred<never>();
const client = { setThreadGoal: vi.fn().mockReturnValue(update.promise) } as unknown as AppServerClient;
const addSystemMessage = vi.fn();
const controller = createChatThreadGoalActions({
const controller = createGoalActions({
stateStore,
currentClient: () => client,
ensureConnected: vi.fn().mockResolvedValue(undefined),
@ -129,7 +129,7 @@ describe("createChatThreadGoalActions", () => {
const clear = deferred<never>();
const client = { clearThreadGoal: vi.fn().mockReturnValue(clear.promise) } as unknown as AppServerClient;
const addSystemMessage = vi.fn();
const controller = createChatThreadGoalActions({
const controller = createGoalActions({
stateStore,
currentClient: () => client,
ensureConnected: vi.fn().mockResolvedValue(undefined),
@ -157,7 +157,7 @@ describe("createChatThreadGoalActions", () => {
const client = { setThreadGoal, injectThreadItems } as unknown as AppServerClient;
const addSystemMessage = vi.fn();
const addGoalEvent = vi.fn();
const controller = createChatThreadGoalActions({
const controller = createGoalActions({
stateStore,
currentClient: () => client,
ensureConnected: vi.fn().mockResolvedValue(undefined),
@ -196,7 +196,7 @@ describe("createChatThreadGoalActions", () => {
const injectThreadItems = vi.fn().mockRejectedValue(new Error("offline"));
const client = { setThreadGoal, injectThreadItems } as unknown as AppServerClient;
const addSystemMessage = vi.fn();
const controller = createChatThreadGoalActions({
const controller = createGoalActions({
stateStore,
currentClient: () => client,
ensureConnected: vi.fn().mockResolvedValue(undefined),
@ -222,7 +222,7 @@ describe("createChatThreadGoalActions", () => {
});
const client = { setThreadGoal, injectThreadItems } as unknown as AppServerClient;
const addSystemMessage = vi.fn();
const controller = createChatThreadGoalActions({
const controller = createGoalActions({
stateStore,
currentClient: () => client,
ensureConnected: vi.fn().mockResolvedValue(undefined),
@ -246,7 +246,7 @@ describe("createChatThreadGoalActions", () => {
const injectThreadItems = vi.fn().mockResolvedValue({});
const client = { setThreadGoal, injectThreadItems } as unknown as AppServerClient;
const addGoalEvent = vi.fn();
const controller = createChatThreadGoalActions({
const controller = createGoalActions({
stateStore,
currentClient: () => client,
ensureConnected: vi.fn().mockResolvedValue(undefined),
@ -271,7 +271,7 @@ describe("createChatThreadGoalActions", () => {
const client = { setThreadGoal } as unknown as AppServerClient;
const addSystemMessage = vi.fn();
const addGoalEvent = vi.fn();
const controller = createChatThreadGoalActions({
const controller = createGoalActions({
stateStore,
currentClient: () => client,
ensureConnected: vi.fn().mockResolvedValue(undefined),
@ -294,7 +294,7 @@ describe("createChatThreadGoalActions", () => {
const currentGoal = goal();
const client = { getThreadGoal: vi.fn().mockResolvedValue({ goal: currentGoal }) } as unknown as AppServerClient;
const addSystemMessage = vi.fn();
const controller = createChatThreadGoalActions({
const controller = createGoalActions({
stateStore,
currentClient: () => client,
ensureConnected: vi.fn().mockResolvedValue(undefined),

View file

@ -1,12 +1,12 @@
import { describe, expect, it, vi } from "vitest";
import { createChatState, createChatStateStore } from "../../../../src/features/chat/state/reducer";
import { ThreadHistoryController } from "../../../../src/features/chat/threads/thread-history-controller";
import { HistoryController } from "../../../../src/features/chat/threads/history-controller";
import type { AppServerClient } from "../../../../src/app-server/client";
import type { AppServerThreadItem, AppServerTurn } from "../../../../src/app-server/turn-model";
import { deferred } from "../../../support/async";
describe("ThreadHistoryController", () => {
describe("HistoryController", () => {
it("keeps the latest history load when an older request resolves later", async () => {
const first = deferred<ThreadTurnsListResponse>();
const second = deferred<ThreadTurnsListResponse>();
@ -96,7 +96,7 @@ function historyFixture(options: { threadTurnsList: ReturnType<typeof vi.fn> })
const addSystemMessage = vi.fn();
const keepCurrentScrollPosition = vi.fn();
const showLatestPageAtBottom = vi.fn();
const loader = new ThreadHistoryController({
const loader = new HistoryController({
stateStore,
currentClient: () =>
({

View file

@ -1,8 +1,8 @@
import { describe, expect, it, vi } from "vitest";
import { createChatState, createChatStateStore } from "../../../../src/features/chat/state/reducer";
import { createThreadIdentitySync } from "../../../../src/features/chat/threads/thread-identity-sync";
import type { RestoredThreadController } from "../../../../src/features/chat/threads/restored-thread-controller";
import { createIdentitySync } from "../../../../src/features/chat/threads/identity-sync";
import type { RestorationController } from "../../../../src/features/chat/threads/restoration-controller";
import type { RestoredThreadPlaceholderState } from "../../../../src/features/chat/lifecycle";
import type { Thread } from "../../../../src/domain/threads/model";
@ -21,15 +21,15 @@ function createController() {
const stateStore = createChatStateStore(createChatState());
const restoredPlaceholder = vi.fn<() => RestoredThreadPlaceholderState | null>(() => null);
const restoredRename = vi.fn();
const restoredThread = {
const restoration = {
clear: vi.fn(),
isPending: vi.fn(() => false),
placeholder: restoredPlaceholder,
rename: restoredRename,
} as unknown as RestoredThreadController;
} as unknown as RestorationController;
const host = {
stateStore,
restoredThread,
restoration,
invalidateResumeWork: vi.fn(),
clearDeferredRestoredThreadHydration: vi.fn(),
resetThreadTurnPresence: vi.fn(),
@ -38,10 +38,10 @@ function createController() {
refreshLiveState: vi.fn(),
render: vi.fn(),
};
return { controller: createThreadIdentitySync(host), host, restoredPlaceholder, restoredRename, stateStore };
return { controller: createIdentitySync(host), host, restoredPlaceholder, restoredRename, stateStore };
}
describe("createThreadIdentitySync", () => {
describe("createIdentitySync", () => {
it("clears the active thread when it is archived", () => {
const { controller, host, stateStore } = createController();
stateStore.dispatch({

View file

@ -17,7 +17,7 @@ import {
type ThreadNamingClient,
type ThreadNamingClientFactory,
} from "../../../../src/app-server/thread-title-generation";
import { firstNamingContextFromDisplayItems, namingContextFromDisplayItems } from "../../../../src/features/chat/threads/thread-naming";
import { firstNamingContextFromDisplayItems, namingContextFromDisplayItems } from "../../../../src/features/chat/threads/naming";
import type { AppServerClient, AppServerClientHandlers, AppServerStartStructuredTurnOptions } from "../../../../src/app-server/client";
import type { AppServerInitialization } from "../../../../src/app-server/initialization";
import type { RequestId, ServerNotification } from "../../../../src/app-server/types";

View file

@ -1,14 +1,14 @@
import { describe, expect, it, vi } from "vitest";
import { createChatStateStore } from "../../../../src/features/chat/state/reducer";
import { ThreadRenameController } from "../../../../src/features/chat/threads/thread-rename-controller";
import { RenameController } from "../../../../src/features/chat/threads/rename-controller";
import type { AppServerClient } from "../../../../src/app-server/client";
import type { Thread } from "../../../../src/domain/threads/model";
import type { AppServerThreadItem, AppServerTurn } from "../../../../src/app-server/turn-model";
import { DEFAULT_SETTINGS } from "../../../../src/settings/model";
import { deferred } from "../../../support/async";
describe("ThreadRenameController", () => {
describe("RenameController", () => {
it("notifies subscribers without rerendering after updating a controlled rename draft", () => {
const { controller, render } = controllerFixture();
const listener = vi.fn();
@ -190,8 +190,8 @@ describe("ThreadRenameController", () => {
});
function controllerFixture(
overrides: Partial<ConstructorParameters<typeof ThreadRenameController>[0]> = {},
): ConstructorParameters<typeof ThreadRenameController>[0] & { controller: ThreadRenameController; render: ReturnType<typeof vi.fn> } {
overrides: Partial<ConstructorParameters<typeof RenameController>[0]> = {},
): ConstructorParameters<typeof RenameController>[0] & { controller: RenameController; render: ReturnType<typeof vi.fn> } {
const stateStore = createChatStateStore();
stateStore.dispatch({ type: "thread-list/applied", threads: [threadFixture("thread")] });
const render = vi.fn();
@ -205,8 +205,8 @@ function controllerFixture(
addSystemMessage: vi.fn(),
notifyThreadRenamed: vi.fn(),
...overrides,
} satisfies ConstructorParameters<typeof ThreadRenameController>[0];
return { ...host, controller: new ThreadRenameController(host), render };
} satisfies ConstructorParameters<typeof RenameController>[0];
return { ...host, controller: new RenameController(host), render };
}
function fakeClient(options: { setThreadName?: ReturnType<typeof vi.fn> } = {}): AppServerClient {

View file

@ -3,16 +3,16 @@
import { describe, expect, it, vi } from "vitest";
import { createChatState, createChatStateStore } from "../../../../src/features/chat/state/reducer";
import { RestoredThreadController } from "../../../../src/features/chat/threads/restored-thread-controller";
import { RestorationController } from "../../../../src/features/chat/threads/restoration-controller";
import { ChatViewDeferredTasks } from "../../../../src/features/chat/lifecycle";
import { deferred } from "../../../support/async";
describe("RestoredThreadController", () => {
describe("RestorationController", () => {
it("restores a placeholder and schedules deferred hydration", () => {
vi.useFakeTimers();
const stateStore = createChatStateStore(createChatState());
const resumeThread = vi.fn().mockResolvedValue(undefined);
const controller = new RestoredThreadController({
const controller = new RestorationController({
deferredTasks: new ChatViewDeferredTasks(() => window),
opened: () => true,
resumeThread,
@ -57,8 +57,8 @@ describe("RestoredThreadController", () => {
});
});
function restoredThreadControllerFixture(overrides: Partial<ConstructorParameters<typeof RestoredThreadController>[0]> = {}) {
return new RestoredThreadController({
function restoredThreadControllerFixture(overrides: Partial<ConstructorParameters<typeof RestorationController>[0]> = {}) {
return new RestorationController({
deferredTasks: new ChatViewDeferredTasks(() => window),
opened: () => false,
resumeThread: vi.fn().mockResolvedValue(undefined),

View file

@ -2,9 +2,9 @@ import { describe, expect, it, vi } from "vitest";
import type { AppServerClient } from "../../../../src/app-server/client";
import { createChatState, createChatStateStore } from "../../../../src/features/chat/state/reducer";
import type { RestoredThreadController } from "../../../../src/features/chat/threads/restored-thread-controller";
import { ThreadResumeController } from "../../../../src/features/chat/threads/thread-resume-controller";
import type { ThreadHistoryController } from "../../../../src/features/chat/threads/thread-history-controller";
import type { RestorationController } from "../../../../src/features/chat/threads/restoration-controller";
import { ResumeController } from "../../../../src/features/chat/threads/resume-controller";
import type { HistoryController } from "../../../../src/features/chat/threads/history-controller";
import { ChatResumeWorkTracker } from "../../../../src/features/chat/lifecycle";
import type { AppServerThread } from "../../../../src/app-server/thread-model";
import type { Thread as PanelThread } from "../../../../src/domain/threads/model";
@ -58,7 +58,7 @@ function activation(threadId: string): ThreadResumeResponse {
function createController(
response: ThreadResumeResponse = activation("thread"),
overrides: Partial<ConstructorParameters<typeof ThreadResumeController>[0]> = {},
overrides: Partial<ConstructorParameters<typeof ResumeController>[0]> = {},
) {
const stateStore = createChatStateStore(createChatState());
const resumeThread = vi.fn().mockResolvedValue(response);
@ -71,8 +71,8 @@ function createController(
stateStore,
vaultPath: "/vault",
resumeWork: new ChatResumeWorkTracker(),
history: { loadLatest, applyLatestPage, invalidate: invalidateHistory } as unknown as ThreadHistoryController,
restoredThread: { clear: restoredClear } as unknown as RestoredThreadController,
history: { loadLatest, applyLatestPage, invalidate: invalidateHistory } as unknown as HistoryController,
restoration: { clear: restoredClear } as unknown as RestorationController,
currentClient: () => client,
ensureConnected: vi.fn().mockResolvedValue(undefined),
closing: () => false,
@ -87,7 +87,7 @@ function createController(
...overrides,
};
return {
controller: new ThreadResumeController(host),
controller: new ResumeController(host),
host,
applyLatestPage,
invalidateHistory,
@ -98,7 +98,7 @@ function createController(
};
}
describe("ThreadResumeController", () => {
describe("ResumeController", () => {
it("resumes the thread and loads its latest history", async () => {
const { controller, host, loadLatest, restoredClear, resumeThread, stateStore } = createController();

View file

@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import type { ThreadActivationSnapshot } from "../../../../src/app-server/thread-activation";
import { resumedThreadAction } from "../../../../src/features/chat/threads/thread-resume";
import { resumedThreadAction } from "../../../../src/features/chat/threads/resume";
import type { Thread } from "../../../../src/domain/threads/model";
describe("chat thread resume helpers", () => {

View file

@ -2,9 +2,9 @@ import { describe, expect, it, vi } from "vitest";
import { createChatState, createChatStateStore, type ChatStateStore } from "../../../../src/features/chat/state/reducer";
import {
createThreadSelectionActions,
type ThreadSelectionActionsHost,
} from "../../../../src/features/chat/threads/thread-selection-actions";
createSelectionActions,
type SelectionActionsHost,
} from "../../../../src/features/chat/threads/selection-actions";
function resumeThreadState(stateStore: ChatStateStore, threadId: string): void {
stateStore.dispatch({
@ -20,9 +20,9 @@ function resumeThreadState(stateStore: ChatStateStore, threadId: string): void {
});
}
function createController(overrides: Partial<ThreadSelectionActionsHost> = {}) {
function createController(overrides: Partial<SelectionActionsHost> = {}) {
const stateStore = createChatStateStore(createChatState());
const host: ThreadSelectionActionsHost = {
const host: SelectionActionsHost = {
stateStore,
closeForThreadSelection: vi.fn(),
focusThreadInOpenView: vi.fn().mockResolvedValue(false),
@ -30,10 +30,10 @@ function createController(overrides: Partial<ThreadSelectionActionsHost> = {}) {
addSystemMessage: vi.fn(),
...overrides,
};
return { controller: createThreadSelectionActions(host), host, stateStore };
return { controller: createSelectionActions(host), host, stateStore };
}
describe("ThreadSelectionActions", () => {
describe("SelectionActions", () => {
it("focuses an already open thread without resuming it", async () => {
const { controller, host } = createController({
focusThreadInOpenView: vi.fn().mockResolvedValue(true),

View file

@ -5,7 +5,7 @@ import { act } from "preact/test-utils";
import { MarkdownRenderer } from "obsidian";
import type { DisplayItem } from "../../../../../src/features/chat/display/types";
import { implementPlanCandidateFromState } from "../../../../../src/features/chat/display/action-candidates";
import { implementPlanCandidateFromState } from "../../../../../src/features/chat/state/selectors";
import { MarkdownMessageRenderer } from "../../../../../src/features/chat/ui/message-stream/markdown-renderer";
import { deferred } from "../../../../support/async";
import { topLevelDetailsSummaries } from "../../../../support/dom";
@ -133,7 +133,7 @@ describe("message stream rendering and message actions", () => {
rows: [
{ key: "status", value: "approved" },
{ key: "action", value: "apply patch" },
{ key: "files", value: "src/display/tool-view.ts\nsrc/ui/message-stream.ts" },
{ key: "files", value: "src/ui/tool-result-view.ts\nsrc/ui/message-stream.ts" },
],
},
],
@ -154,7 +154,7 @@ describe("message stream rendering and message actions", () => {
expect(element.querySelector(".codex-panel__meta-grid")?.textContent).toContain("statusapproved");
expect(element.querySelector(".codex-panel__meta-grid")?.textContent).toContain("actionapply patch");
expect(element.querySelector(".codex-panel__meta-grid")?.textContent).toContain(
"filessrc/display/tool-view.ts\nsrc/ui/message-stream.ts",
"filessrc/ui/tool-result-view.ts\nsrc/ui/message-stream.ts",
);
expect([...element.querySelectorAll(".codex-panel__output-title")].map((title) => title.textContent)).toEqual([]);
});
@ -633,7 +633,7 @@ describe("message stream rendering and message actions", () => {
it("hides copy action for the active assistant message while a turn is running", () => {
const item = {
id: "a-running",
itemId: "a-running",
sourceItemId: "a-running",
kind: "message",
role: "assistant",
messageKind: "assistantResponse",
@ -841,7 +841,7 @@ describe("message stream rendering and message actions", () => {
details.dispatchEvent(new Event("toggle"));
});
}
expect(openDetails.has("message:u1:expanded")).toBe(true);
expect(openDetails.has("text-item:u1:expanded")).toBe(true);
expect(content?.classList.contains("codex-panel__message-content--collapsed")).toBe(false);
expect(details?.hidden).toBe(true);
expect(onDetailsToggle).toHaveBeenCalled();
@ -849,10 +849,10 @@ describe("message stream rendering and message actions", () => {
void act(() => {
document.body.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true }));
});
expect(openDetails.has("message:u1:expanded")).toBe(false);
expect(openDetails.has("text-item:u1:expanded")).toBe(false);
expect(content?.classList.contains("codex-panel__message-content--collapsed")).toBe(true);
expect(details?.hidden).toBe(false);
expect(onDetailsToggle).toHaveBeenCalledWith("message:u1:expanded", false);
expect(onDetailsToggle).toHaveBeenCalledWith("text-item:u1:expanded", false);
element.querySelector<HTMLButtonElement>(".codex-panel__copy-message")?.click();
expect(copyText).toHaveBeenCalledWith("full copied text");

View file

@ -47,9 +47,9 @@ describe("pending request renderer decisions", () => {
pendingRequestActions({ resolveUserInput }),
);
expect(parent.querySelectorAll(".codex-panel__pending-request-message")).toHaveLength(1);
expect(parent.querySelector(".codex-panel__pending-request-message")?.classList.contains("codex-panel__work-message")).toBe(true);
expect(parent.querySelector(".codex-panel__pending-request-message")?.classList.contains("codex-panel__work-message--warning")).toBe(
expect(parent.querySelectorAll(".codex-panel__pending-request-block")).toHaveLength(1);
expect(parent.querySelector(".codex-panel__pending-request-block")?.classList.contains("codex-panel__work-message")).toBe(true);
expect(parent.querySelector(".codex-panel__pending-request-block")?.classList.contains("codex-panel__work-message--warning")).toBe(
true,
);
expect(parent.querySelector<HTMLButtonElement>(".codex-panel__pending-request-button.mod-cta")?.textContent).toBe("Submit");
@ -202,7 +202,7 @@ describe("pending request renderer decisions", () => {
pendingRequestActions(),
);
expect(parent.querySelectorAll(".codex-panel__pending-request-message")).toHaveLength(1);
expect(parent.querySelectorAll(".codex-panel__pending-request-block")).toHaveLength(1);
expect(parent.querySelectorAll(".codex-panel__pending-request-card")).toHaveLength(2);
expect(parent.querySelectorAll(".codex-panel__pending-request-body")).toHaveLength(2);
expect(parent.querySelector(".codex-panel__approval-body")).toBeNull();

View file

@ -6,7 +6,7 @@ import { createMessageStreamContextPort } from "../../../../../src/features/chat
describe("message stream context port", () => {
it("closes other fork action details before opening a fork action detail", () => {
const state = createChatState();
state.ui.openDetails = new Set(["message:fork-actions:old", "message:u1:expanded"]);
state.ui.openDetails = new Set(["message:fork-actions:old", "text-item:u1:expanded"]);
const dispatched: ChatAction[] = [];
const port = createMessageStreamContextPort({
@ -78,8 +78,8 @@ describe("message stream context port", () => {
},
});
port.setOpenDetail("message:u1:expanded", true);
port.setOpenDetail("text-item:u1:expanded", true);
expect(dispatched).toEqual([{ type: "ui/detail-open-set", key: "message:u1:expanded", open: true }]);
expect(dispatched).toEqual([{ type: "ui/detail-open-set", key: "text-item:u1:expanded", open: true }]);
});
});

View file

@ -4,7 +4,7 @@ import { act } from "preact/test-utils";
import type { PendingApproval } from "../../../../../src/features/chat/protocol/requests/approval";
import type { PendingUserInput } from "../../../../../src/features/chat/protocol/requests/user-input";
import { pendingRequestMessageNode, type PendingRequestMessageActions } from "../../../../../src/features/chat/ui/pending-request-message";
import { pendingRequestBlockNode, type PendingRequestBlockActions } from "../../../../../src/features/chat/ui/pending-request-block";
import type { ChatTurnLifecycleState } from "../../../../../src/features/chat/state/reducer";
import { messageStreamBlocks as rawMessageStreamBlocks } from "../../../../../src/features/chat/ui/message-stream/stream-blocks";
import type { MessageStreamBlock } from "../../../../../src/features/chat/ui/message-stream/context";
@ -93,11 +93,11 @@ export function unmountUiRootInAct(parent: HTMLElement): void {
});
}
export function renderPendingRequestNode(parent: HTMLElement, ...args: Parameters<typeof pendingRequestMessageNode>): void {
renderUiRootInAct(parent, pendingRequestMessageNode(...args));
export function renderPendingRequestNode(parent: HTMLElement, ...args: Parameters<typeof pendingRequestBlockNode>): void {
renderUiRootInAct(parent, pendingRequestBlockNode(...args));
}
export function pendingRequestActions(overrides: Partial<PendingRequestMessageActions> = {}): PendingRequestMessageActions {
export function pendingRequestActions(overrides: Partial<PendingRequestBlockActions> = {}): PendingRequestBlockActions {
return {
resolveApproval: vi.fn(),
resolveUserInput: vi.fn(),

View file

@ -75,7 +75,7 @@ describe("ChatPanelShell", () => {
store.dispatch({ type: "connection/status-set", status: "Working" });
store.dispatch({ type: "ui/panel-set", panel: "status-panel" });
store.dispatch({
type: "transcript/system-message-added",
type: "transcript/system-item-added",
item: { id: "system-1", kind: "system", role: "system", text: "Model set." },
});
await settleShellEffects();
@ -292,7 +292,7 @@ describe("ChatPanelShell", () => {
await act(async () => {
store.dispatch({
type: "transcript/system-message-added",
type: "transcript/system-item-added",
item: { id: "system-1", kind: "system", role: "system", text: "Restored." },
});
await settleShellEffects();

View file

@ -8,7 +8,7 @@ import { createChatStateStore } from "../../../../src/features/chat/state/reduce
import { runtimeSnapshotForChatState } from "../../../../src/features/chat/runtime/snapshot";
import { createToolbarArchiveConfirmState, ToolbarPanelController } from "../../../../src/features/chat/panel/regions/toolbar";
import type { ChatPanelToolbarPorts } from "../../../../src/features/chat/panel/regions/ports";
import type { ChatThreadActions } from "../../../../src/features/chat/threads/thread-actions";
import type { ChatThreadActions } from "../../../../src/features/chat/threads/actions";
import { renderChatPanelShell, unmountChatPanelShell } from "../../../../src/features/chat/ui/shell";
import { chatPanelToolbarRegionNode } from "../../../../src/features/chat/panel/regions/render";
import { installObsidianDomShims } from "../../../support/dom";

View file

@ -8,7 +8,7 @@ import {
} from "../../../src/app-server/runtime-config";
import { createChatState } from "../../../src/features/chat/state/reducer";
import { composerMetaViewModel, composerPlaceholder } from "../../../src/features/chat/panel/regions/composer";
import { effortStatusLines, modelStatusLines, statusSummaryLines } from "../../../src/features/chat/display/runtime-status";
import { effortStatusLines, modelStatusLines, statusSummaryLines } from "../../../src/features/chat/display/status/runtime";
import { runtimeComposerChoices } from "../../../src/features/chat/panel/regions/composer";
import { runtimeSnapshotForChatState } from "../../../src/features/chat/runtime/snapshot";
import { toolbarViewModel } from "../../../src/features/chat/panel/regions/toolbar";

View file

@ -34,7 +34,7 @@ import {
requestedTurnCollaborationModeSettings,
serviceTierRequestForThreadStart,
} from "../../src/features/chat/runtime/thread-settings";
import { contextSummary, runtimeConfigSections, rateLimitSummary } from "../../src/features/chat/display/runtime-status";
import { contextSummary, runtimeConfigSections, rateLimitSummary } from "../../src/features/chat/display/status/runtime";
describe("runtime settings", () => {
it("parses model overrides", () => {