Align chat naming conventions

This commit is contained in:
murashit 2026-06-18 20:58:25 +09:00
parent 0062590cba
commit 43e8d7fc20
35 changed files with 245 additions and 233 deletions

View file

@ -28,10 +28,18 @@ The source tree is organized by responsibility rather than by the original singl
- `src/domain/` contains panel-owned, generated-independent meaning models and pure derivations shared by app-server, features, workspace, and settings code. Domain code must not import app-server protocol, generated bindings, feature modules, UI, or Obsidian APIs; app-server protocol and service modules adapt generated payloads into domain models at the boundary.
- `src/styles/` contains the authored CSS modules and `order.json` concatenation order. `scripts/build-styles.mjs` concatenates them into the ignored root `styles.css`, which remains the Obsidian release asset.
## Implementation Conventions
Keep new code near the state or API it owns. A feature may import another feature only for a capability that feature owns; feature-neutral behavior belongs in `src/shared/`, `src/domain/`, or `src/app-server/`. Lower-level modules must not import `src/features/`, and generated app-server types should stay behind `src/app-server/`.
Codex Panel's runtime UI is Preact-owned. Use imperative DOM writes only for explicit bridge modules, Obsidian-owned API boundaries, or rendering and measurement code that cannot be expressed cleanly as Preact components. Chat panel-visible state belongs in `ChatStateStore` and should flow through named reducer actions and the shell-state adapter. Revisit the project design notes before adding a parallel UI state or runtime path.
## Naming Conventions
Name modules by responsibility: use `Controller` only for stateful lifecycle/control surfaces, `Handler` for inbound event or request entrypoints, `Actions` for caller-facing command or callback bundles without lifecycle ownership, `Coordinator` for stateful background or cross-surface coordination, `Service` for reusable domain capabilities, `Presenter` for UI state projection, `Renderer` for render-only UI contracts, and `Host`/`Ports` for dependency boundary objects. Use `State`, `Snapshot`, `Projection`, `ViewModel`, `Options`, `Context`, `Result`, `Target`, `Capabilities`, or `ActionTargets` for data/value objects; do not use `Actions` for passive data. Use boundary/infrastructure nouns such as `Client`, `Transport`, `Cache`, `Store`, `Catalog`, `Manager`, `Bridge`, `Tracker`, `Session`, `Runtime`, `Provider`, or `Adapter` only when the object owns that concrete boundary or lifecycle role.
Prefer functions and factory-created objects over classes. Use a class only when it owns mutable lifecycle/resource state, extends or implements an external class-based API such as Obsidian views/modals/tabs, or represents an `Error`; do not use a class merely to group pure functions or dependencies.
## App-Server Bindings
The app-server TypeScript bindings in `src/generated/app-server/` are generated from the installed Codex CLI:

View file

@ -170,15 +170,15 @@ const imperativeDomAssignmentProperties = new Set([
const pureChatStateRestrictions = [
{
selector: "CallExpression[callee.object.name='Date'][callee.property.name='now']",
message: "Keep chat state transforms deterministic; generate IDs or timestamps at the controller/view boundary.",
message: "Keep chat state transforms deterministic; generate IDs or timestamps at an application or UI boundary.",
},
{
selector: "NewExpression[callee.name='Date']",
message: "Keep chat state transforms deterministic; pass dates in from the controller/view boundary.",
message: "Keep chat state transforms deterministic; pass dates in from an application or UI boundary.",
},
{
selector: "CallExpression[callee.object.name='Math'][callee.property.name='random']",
message: "Keep chat state transforms deterministic; generate IDs at the controller/view boundary.",
message: "Keep chat state transforms deterministic; generate IDs at an application or UI boundary.",
},
{
selector: "NewExpression[callee.name=/^(AppServerClient|ConnectionManager|Notice)$/]",

View file

@ -15,7 +15,7 @@ export interface ComposerSubmitActionsHost {
readonly trimmedDraft: string;
setDraft(text: string, options?: { clearSuggestions?: boolean; focus?: boolean }): void;
};
slashCommands: {
slashCommandExecutor: {
execute(command: SlashCommandName, args: string): Promise<SlashCommandExecutionResult | undefined>;
};
turnSubmission: {
@ -58,7 +58,7 @@ async function sendMessage(host: ComposerSubmitActionsHost): Promise<void> {
const slashCommand = parseSlashCommand(text);
if (slashCommand) {
host.composer.setDraft("", { clearSuggestions: true });
const result = await host.slashCommands.execute(slashCommand.command, slashCommand.args);
const result = await host.slashCommandExecutor.execute(slashCommand.command, slashCommand.args);
if (result?.composerDraft !== undefined) {
host.composer.setDraft(result.composerDraft, { focus: true, clearSuggestions: true });
}

View file

@ -8,7 +8,7 @@ import type { ChatStateStore } from "../state/store";
import type { ThreadManagementActions } from "../threads/thread-management-actions";
import type { GoalActions } from "../threads/goal-actions";
import { submitComposer, type ComposerSubmitActions, type ComposerSubmitActionsHost } from "./composer-submit-actions";
import { executeSlashCommandWithState, type SlashCommandHandlerHost } from "./slash-command-handler";
import { executeSlashCommandWithState, type SlashCommandExecutorHost } from "./slash-command-executor";
import { createTurnSubmissionActions } from "./turn-submission-actions";
const IMPLEMENT_PLAN_PROMPT = "Please implement this plan.";
@ -99,7 +99,7 @@ export function createConversationTurnActions(
setStatus: status.set,
addSystemMessage: status.addSystemMessage,
});
const slashCommandHost: SlashCommandHandlerHost = {
const slashCommandExecutorHost: SlashCommandExecutorHost = {
stateStore,
currentClient: client.currentClient,
codexInput: composer.codexInput,
@ -136,8 +136,8 @@ export function createConversationTurnActions(
},
setDraft: composer.setDraft,
},
slashCommands: {
execute: (command, args) => executeSlashCommandWithState(slashCommandHost, command, args),
slashCommandExecutor: {
execute: (command, args) => executeSlashCommandWithState(slashCommandExecutorHost, command, args),
},
turnSubmission,
connection: {
@ -161,8 +161,8 @@ export function createConversationTurnActions(
};
}
async function startThreadForGoal(actions: ConversationThreadStarter, objective: string): Promise<string | null> {
const response = await actions.startThread(objective, { syncGoal: false });
async function startThreadForGoal(starter: ConversationThreadStarter, objective: string): Promise<string | null> {
const response = await starter.startThread(objective, { syncGoal: false });
return response?.threadId ?? null;
}

View file

@ -20,7 +20,7 @@ import type { MessageStreamAuditFact, MessageStreamNoticeSection } from "../../d
const DEFAULT_RUNTIME_SETTING_ALIASES = new Set(["default", "reset", "clear", "off"]);
export interface SlashCommandActionPorts {
export interface SlashCommandExecutionPorts {
startNewThread: () => Promise<void>;
startThreadForGoal: (objective: string) => Promise<string | null>;
resumeThread: (threadId: string) => Promise<void>;
@ -56,7 +56,7 @@ export interface SlashCommandActionPorts {
effortStatusLines: () => string[];
}
export interface SlashCommandExecutionContext extends SlashCommandActionPorts {
export interface SlashCommandExecutionContext extends SlashCommandExecutionPorts {
activeThreadId: string | null;
busy: boolean;
listedThreads: readonly Thread[];

View file

@ -6,7 +6,7 @@ import type { Thread } from "../../../../domain/threads/model";
import { shortThreadId } from "../../../../utils";
import {
executeSlashCommand as runSlashCommand,
type SlashCommandActionPorts,
type SlashCommandExecutionPorts,
type SlashCommandExecutionResult,
type ThreadReferenceInput,
} from "./slash-command-execution";
@ -18,7 +18,7 @@ import type { ChatStateStore } from "../state/store";
import { currentModel, runtimeConfigOrDefault } from "../../domain/runtime/effective";
import { runtimeSnapshotForChatState } from "../runtime/snapshot";
export interface SlashCommandHandlerHost extends SlashCommandActionPorts {
export interface SlashCommandExecutorHost extends SlashCommandExecutionPorts {
stateStore: ChatStateStore;
currentClient: () => AppServerClient | null;
codexInput: (text: string) => CodexInput;
@ -34,7 +34,7 @@ function referencedThreadUnreadableMessage(): string {
}
export async function executeSlashCommandWithState(
host: SlashCommandHandlerHost,
host: SlashCommandExecutorHost,
command: SlashCommandName,
args: string,
): Promise<SlashCommandExecutionResult | undefined> {
@ -61,7 +61,7 @@ function supportedReasoningEfforts(state: ReturnType<ChatStateStore["getState"]>
}
async function referencedThreadInput(
host: SlashCommandHandlerHost,
host: SlashCommandExecutorHost,
client: AppServerClient,
thread: Thread,
message: string,

View file

@ -709,7 +709,7 @@ export function cloneChatState(state: ChatState): ChatState {
archiveConfirmThreadId: state.ui.archiveConfirmThreadId,
rename: { ...state.ui.rename },
goalEditor: { ...state.ui.goalEditor },
messageActions: { ...state.ui.messageActions },
messageActionMenu: { ...state.ui.messageActionMenu },
disclosures: cloneDisclosureUiState(state.ui.disclosures),
},
};

View file

@ -24,8 +24,8 @@ type ChatGoalEditorUiState =
readonly tokenBudgetDraft: number | null;
};
interface ChatMessageActionsUiState {
readonly forkActionsItemId: string | null;
interface ChatMessageActionMenuUiState {
readonly forkMenuItemId: string | null;
}
const CHAT_DISCLOSURE_BUCKETS = [
@ -46,7 +46,7 @@ export interface ChatUiState {
readonly archiveConfirmThreadId: string | null;
readonly rename: ChatRenameUiState;
readonly goalEditor: ChatGoalEditorUiState;
readonly messageActions: ChatMessageActionsUiState;
readonly messageActionMenu: ChatMessageActionMenuUiState;
readonly disclosures: ChatDisclosureUiState;
}
@ -67,7 +67,7 @@ export type UiAction =
| { type: "ui/goal-editor-started"; threadId: string | null; objective: string; tokenBudget: number | null }
| { type: "ui/goal-editor-draft-updated"; objective: string }
| { type: "ui/goal-editor-closed" }
| { type: "ui/message-fork-actions-set"; itemId: string | null }
| { type: "ui/message-fork-menu-set"; itemId: string | null }
| DisclosureSetAction;
export function initialUiState(): ChatUiState {
@ -76,7 +76,7 @@ export function initialUiState(): ChatUiState {
archiveConfirmThreadId: null,
rename: initialRenameUiState(),
goalEditor: initialGoalEditorUiState(),
messageActions: initialMessageActionsUiState(),
messageActionMenu: initialMessageActionMenuUiState(),
disclosures: initialDisclosureUiState(),
};
}
@ -95,7 +95,7 @@ export function isUiAction(action: { type: string }): action is UiAction {
case "ui/goal-editor-started":
case "ui/goal-editor-draft-updated":
case "ui/goal-editor-closed":
case "ui/message-fork-actions-set":
case "ui/message-fork-menu-set":
case "ui/disclosure-set":
return true;
default:
@ -138,8 +138,8 @@ export function reduceUiSlice(state: ChatUiState, action: UiAction): ChatUiState
return patchObject(state, { goalEditor: goalEditorDraftUpdated(state.goalEditor, action.objective) });
case "ui/goal-editor-closed":
return patchObject(state, { goalEditor: initialGoalEditorUiState() });
case "ui/message-fork-actions-set":
return patchObject(state, { messageActions: { forkActionsItemId: action.itemId } });
case "ui/message-fork-menu-set":
return patchObject(state, { messageActionMenu: { forkMenuItemId: action.itemId } });
case "ui/disclosure-set":
return setDisclosureSlice(state, action.bucket, action.id, action.open);
}
@ -228,8 +228,8 @@ function initialGoalEditorUiState(): ChatGoalEditorUiState {
return { kind: "closed" };
}
function initialMessageActionsUiState(): ChatMessageActionsUiState {
return { forkActionsItemId: null };
function initialMessageActionMenuUiState(): ChatMessageActionMenuUiState {
return { forkMenuItemId: null };
}
function initialDisclosureUiState(): ChatDisclosureUiState {

View file

@ -5,19 +5,19 @@ import type { ThreadTitleService } from "../../../threads/thread-title-service";
import type { ChatState } from "../state/root-reducer";
import type { ChatStateStore } from "../state/store";
export interface AutoTitleActionsHost {
export interface AutoTitleCoordinatorHost {
stateStore: ChatStateStore;
completedTurnTitleContext: ThreadTitleService["completedTurnContext"];
generateTitleFromContext: ThreadTitleService["generate"];
renameGeneratedTitle(threadId: string, title: string, options: { shouldPublish: () => boolean }): Promise<boolean>;
}
export interface AutoTitleActions {
export interface AutoTitleCoordinator {
resetThreadTurnPresence(hadTurns: boolean): void;
maybeAutoTitleThread(threadId: string, turnId: string, completedSummary: ThreadConversationSummary | null): void;
}
export function createAutoTitleActions(host: AutoTitleActionsHost): AutoTitleActions {
export function createAutoTitleCoordinator(host: AutoTitleCoordinatorHost): AutoTitleCoordinator {
let activeThreadHadTurns = false;
const attemptedThreadIds = new Set<string>();
const inFlightThreadIds = new Set<string>();
@ -65,6 +65,6 @@ export function createAutoTitleActions(host: AutoTitleActionsHost): AutoTitleAct
};
}
function state(host: AutoTitleActionsHost): ChatState {
function state(host: AutoTitleCoordinatorHost): ChatState {
return host.stateStore.getState();
}

View file

@ -12,18 +12,18 @@ export interface PlanImplementationTarget {
export function forkCandidatesFromItems(items: readonly MessageStreamItem[]): readonly ForkCandidate[] {
const turnOutcomeItemsByTurn = new Map<string, ForkCandidate>();
for (const { item, actions } of messageStreamSemanticClassifications(items)) {
if (!item.turnId || !actions.isTurnOutcome) continue;
for (const { item, capabilities } of messageStreamSemanticClassifications(items)) {
if (!item.turnId || !capabilities.isTurnOutcome) continue;
turnOutcomeItemsByTurn.set(item.turnId, { itemId: item.id, turnId: item.turnId });
}
return [...turnOutcomeItemsByTurn.values()];
}
export function latestImplementablePlanTargetFromItems(items: readonly MessageStreamItem[]): PlanImplementationTarget | null {
const classification = [...messageStreamSemanticClassifications(items)].reverse().find((item) => item.actions.canImplementPlan);
const classification = [...messageStreamSemanticClassifications(items)].reverse().find((item) => item.capabilities.canImplementPlan);
return classification ? { itemId: classification.item.id } : null;
}
export function isCompletedTurnOutcomeMessage(item: MessageStreamItem): boolean {
return messageStreamSemanticClassifications([item])[0]?.actions.isTurnOutcome ?? false;
return messageStreamSemanticClassifications([item])[0]?.capabilities.isTurnOutcome ?? false;
}

View file

@ -4,7 +4,7 @@ import type {
MessageStreamLifecycle,
MessageStreamMeaning,
MessageStreamPlacement,
MessageStreamSemanticActions,
MessageStreamSemanticCapabilities,
MessageStreamSemanticClassification,
} from "./types";
@ -26,13 +26,13 @@ function messageStreamSemanticClassification(
placement,
meaning,
...definedProp("lifecycle", lifecycle),
actions: messageStreamSemanticActions({ item, placement, meaning, ...definedProp("lifecycle", lifecycle) }),
capabilities: messageStreamSemanticCapabilities({ item, placement, meaning, ...definedProp("lifecycle", lifecycle) }),
};
}
function messageStreamSemanticActions(
function messageStreamSemanticCapabilities(
classification: Pick<MessageStreamSemanticClassification, "item" | "placement" | "meaning" | "lifecycle">,
): MessageStreamSemanticActions {
): MessageStreamSemanticCapabilities {
const { item, placement, meaning, lifecycle } = classification;
const isDialogueOutcome =
placement.scope === "turn" &&

View file

@ -56,7 +56,7 @@ export interface MessageStreamLifecycle {
state: Exclude<ExecutionState, null>;
}
export interface MessageStreamSemanticActions {
export interface MessageStreamSemanticCapabilities {
canForkFromHere: boolean;
canRollbackToPrompt: boolean;
canImplementPlan: boolean;
@ -69,5 +69,5 @@ export interface MessageStreamSemanticClassification {
placement: MessageStreamPlacement;
meaning: MessageStreamMeaning;
lifecycle?: MessageStreamLifecycle;
actions: MessageStreamSemanticActions;
capabilities: MessageStreamSemanticCapabilities;
}

View file

@ -14,7 +14,7 @@ import type { ChatViewDeferredTasks } from "../application/lifecycle";
import type { ChatConnectionPhase } from "../application/state/root-reducer";
import type { ChatStateStore } from "../application/state/store";
import type { createThreadGoalSyncActions } from "../application/threads/goal-actions";
import type { AutoTitleActions } from "../application/threads/auto-title-actions";
import type { AutoTitleCoordinator } from "../application/threads/auto-title-coordinator";
import { runtimeSnapshotForChatState } from "../application/runtime/snapshot";
import { createChatServerDiagnosticsActions, type ChatServerDiagnosticsActions } from "../app-server/actions/diagnostics";
import { createChatServerMetadataActions, type ChatServerMetadataActions } from "../app-server/actions/metadata";
@ -39,7 +39,7 @@ interface ChatPanelConnectionBundleInput {
localItemIds: LocalIdSource;
status: ChatPanelConnectionStatus;
goalSync: ChatPanelGoalSyncActions;
autoTitle: AutoTitleActions;
autoTitleCoordinator: AutoTitleCoordinator;
}
interface ChatPanelConnectionBundleHost {
@ -97,7 +97,7 @@ export function createConnectionBundle(
input: ChatPanelConnectionBundleInput,
): ChatPanelConnectionBundle {
const { environment, stateStore } = host;
const { connection, currentClient, localItemIds, status, goalSync, autoTitle } = input;
const { connection, currentClient, localItemIds, status, goalSync, autoTitleCoordinator } = input;
const serverMetadata = createChatServerMetadataActions({
stateStore,
vaultPath: environment.plugin.settingsRef.vaultPath,
@ -149,7 +149,7 @@ export function createConnectionBundle(
serverMetadata.applyAppServerMetadataSnapshot();
},
maybeNameThread: (threadId, turnId, completedSummary) => {
autoTitle.maybeAutoTitleThread(threadId, turnId, completedSummary);
autoTitleCoordinator.maybeAutoTitleThread(threadId, turnId, completedSummary);
},
upsertActiveThread: (thread) => {
environment.plugin.threadCatalog.upsertFromAppServer(thread);
@ -179,7 +179,7 @@ export function createConnectionBundle(
},
setStatus: status.set,
resetThreadTurnPresence: (hadTurns: boolean) => {
autoTitle.resetThreadTurnPresence(hadTurns);
autoTitleCoordinator.resetThreadTurnPresence(hadTurns);
},
refreshLiveState: () => {
host.refreshLiveState();

View file

@ -22,7 +22,7 @@ import { messageStreamItems } from "../application/state/message-stream";
import type { ChatStateStore } from "../application/state/store";
import type { ChatResumeWorkTracker, ChatViewDeferredTasks } from "../application/lifecycle";
import { createGoalActions, createThreadGoalSyncActions } from "../application/threads/goal-actions";
import { createAutoTitleActions, type AutoTitleActions } from "../application/threads/auto-title-actions";
import { createAutoTitleCoordinator, type AutoTitleCoordinator } from "../application/threads/auto-title-coordinator";
import { HistoryController } from "../application/threads/history-controller";
import type { IdentitySync } from "../application/threads/identity-sync";
import { createThreadLifecycleParts } from "../application/threads/lifecycle-parts";
@ -171,8 +171,8 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
const currentClient = () => connection.currentClient();
const status = createSessionStatus(stateStore, localItemIds);
const titleService = createSessionThreadTitleService(host, currentClient);
const autoTitle = createSessionAutoTitleActions(host, currentClient, titleService);
const history = createSessionHistoryController(host, currentClient, status, autoTitle);
const autoTitleCoordinator = createSessionAutoTitleCoordinator(host, currentClient, titleService);
const history = createSessionHistoryController(host, currentClient, status, autoTitleCoordinator);
const invalidateThreadWork = () => {
host.resumeWork.invalidate();
history.invalidate();
@ -200,7 +200,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
currentClient,
localItemIds,
goalSync,
autoTitle,
autoTitleCoordinator,
status,
},
);
@ -222,7 +222,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
ensureConnected,
status,
goals,
autoTitle,
autoTitleCoordinator,
history,
invalidateThreadWork,
);
@ -263,7 +263,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
serverThreads,
serverDiagnostics,
goals,
autoTitle,
autoTitleCoordinator,
invalidateThreadWork,
startNewThread,
runtimeProjection: {
@ -421,12 +421,12 @@ function createSessionThreadTitleService(host: ChatPanelSessionGraphHost, curren
});
}
function createSessionAutoTitleActions(
function createSessionAutoTitleCoordinator(
host: ChatPanelSessionGraphHost,
currentClient: CurrentAppServerClient,
titleService: ThreadTitleService,
): AutoTitleActions {
return createAutoTitleActions({
): AutoTitleCoordinator {
return createAutoTitleCoordinator({
stateStore: host.stateStore,
completedTurnTitleContext: (turnId, completedSummary) => titleService.completedTurnContext(turnId, completedSummary),
generateTitleFromContext: (context) => titleService.generate(context),
@ -450,7 +450,7 @@ function createSessionHistoryController(
host: ChatPanelSessionGraphHost,
currentClient: CurrentAppServerClient,
status: ChatPanelSessionStatus,
autoTitle: AutoTitleActions,
autoTitleCoordinator: AutoTitleCoordinator,
): HistoryController {
return new HistoryController({
stateStore: host.stateStore,
@ -463,7 +463,7 @@ function createSessionHistoryController(
host.messageScrollIntent.forceBottom();
},
setThreadTurnPresence: (hadTurns) => {
autoTitle.resetThreadTurnPresence(hadTurns);
autoTitleCoordinator.resetThreadTurnPresence(hadTurns);
},
});
}
@ -584,7 +584,7 @@ function createSessionThreadLifecycle(
ensureConnected: () => Promise<void>,
status: ChatPanelSessionStatus,
goals: ChatPanelGoalActions,
autoTitle: AutoTitleActions,
autoTitleCoordinator: AutoTitleCoordinator,
history: HistoryController,
invalidateThreadWork: () => void,
): ChatPanelThreadLifecycle {
@ -619,7 +619,7 @@ function createSessionThreadLifecycle(
},
goals,
resetThreadTurnPresence: (hadTurns) => {
autoTitle.resetThreadTurnPresence(hadTurns);
autoTitleCoordinator.resetThreadTurnPresence(hadTurns);
},
});
}
@ -747,7 +747,7 @@ function createComposerAndTurnActions(
serverThreads: ChatServerThreadActions;
serverDiagnostics: ChatServerDiagnosticsActions;
goals: ChatPanelGoalActions;
autoTitle: AutoTitleActions;
autoTitleCoordinator: AutoTitleCoordinator;
invalidateThreadWork: () => void;
startNewThread: () => Promise<void>;
runtimeProjection: {
@ -773,7 +773,7 @@ function createComposerAndTurnActions(
serverThreads,
serverDiagnostics,
goals,
autoTitle,
autoTitleCoordinator,
invalidateThreadWork,
startNewThread,
runtimeProjection,
@ -836,7 +836,7 @@ function createComposerAndTurnActions(
notifyActiveThreadIdentityChanged(host);
},
resetTurnPresence: (hadTurns) => {
autoTitle.resetThreadTurnPresence(hadTurns);
autoTitleCoordinator.resetThreadTurnPresence(hadTurns);
},
},
composer: {

View file

@ -191,7 +191,7 @@ export class ChatPanelSession implements ChatSurfaceHandle {
goal: this.graph.surface.goal,
messageStream: this.graph.render.messageStreamPresenter,
composer: {
controller: this.graph.composer.controller,
renderer: this.graph.composer.controller,
actions: {
submit: () => void this.graph.composer.submission.submit(),
},

View file

@ -5,7 +5,7 @@ import { ChatPanelToolbar, type ChatPanelToolbarSurface } from "./surface/toolba
import type { ToolbarActions } from "../ui/toolbar";
import { ChatPanelGoal, type ChatPanelGoalSurface } from "./surface/goal-projection";
import { ChatPanelMessageStream, type ChatPanelMessageStreamPresenter } from "./surface/message-stream-presenter";
import { ChatPanelComposer, type ChatPanelComposerActions, type ChatPanelComposerController } from "./surface/composer-projection";
import { ChatPanelComposer, type ChatPanelComposerActions, type ChatPanelComposerRenderer } from "./surface/composer-projection";
import { ChatPanelShellStateContext, createChatPanelShellState, syncChatPanelShellState, type ChatPanelShellState } from "./shell-state";
export interface ChatPanelShellParts {
@ -16,7 +16,7 @@ export interface ChatPanelShellParts {
goal: ChatPanelGoalSurface;
messageStream: ChatPanelMessageStreamPresenter;
composer: {
controller: ChatPanelComposerController;
renderer: ChatPanelComposerRenderer;
actions: ChatPanelComposerActions;
};
}
@ -117,7 +117,7 @@ function ChatPanelShell({ showToolbar, parts, shellState }: ChatPanelShellProps
</div>
<ChatPanelMessageStream presenter={parts.messageStream} />
<div className="codex-panel__region codex-panel__region--composer" data-codex-panel-shell-region="composer">
<ChatPanelComposer controller={parts.composer.controller} actions={parts.composer.actions} />
<ChatPanelComposer renderer={parts.composer.renderer} actions={parts.composer.actions} />
</div>
</div>
</ChatPanelShellStateContext.Provider>

View file

@ -71,7 +71,7 @@ export interface ChatPanelComposerSurface {
};
}
export interface ChatPanelComposerController {
export interface ChatPanelComposerRenderer {
renderState(state: ChatPanelComposerShellState, actions: ChatPanelComposerActions): ComposerShellProps;
}
@ -91,14 +91,14 @@ function composerPlaceholder(threadName: string | null): string {
}
export function ChatPanelComposer({
controller,
renderer,
actions,
}: {
controller: ChatPanelComposerController;
renderer: ChatPanelComposerRenderer;
actions: ChatPanelComposerActions;
}): UiNode {
const state = composerStateFromShellState(useChatPanelShellState());
return h(ComposerShell, controller.renderState(state, actions));
return h(ComposerShell, renderer.renderState(state, actions));
}
export function chatPanelComposerProjection(

View file

@ -21,7 +21,7 @@ import type { ChatPanelMessageStreamShellState } from "../shell-state";
import { pendingRequestBlockSnapshotFromState } from "../../presentation/pending-requests/snapshot";
import type { PendingRequestBlockActions, PendingRequestBlockState } from "../../application/pending-requests/block";
import type { ChatTurnDiffViewState } from "../../domain/turn-diff";
import type { MessageStreamTextActions } from "../../presentation/message-stream/text-view";
import type { MessageStreamTextActionTargets } from "../../presentation/message-stream/text-view";
interface ChatMessageStreamActions {
rollbackThread: (threadId: string) => void;
@ -40,7 +40,7 @@ interface ChatMessageStreamRequests {
export interface ChatMessageStreamSurfaceContext {
vaultPath: string;
setDisclosureOpen: (bucket: ChatDisclosureBucket, id: string, open: boolean) => void;
setForkActionsItem: (itemId: string | null) => void;
setForkMenuItem: (itemId: string | null) => void;
loadOlderTurns: () => void;
renderObsidianMarkdown: (element: HTMLElement, text: string) => void;
renderStreamMarkdown: (element: HTMLElement, text: string) => void;
@ -64,7 +64,7 @@ interface MessageStreamStateProjection {
activeThreadId: string | null;
workspaceRoot: string;
disclosures: ChatDisclosureUiState;
forkActionsItemId: string | null;
forkMenuItemId: string | null;
viewBlocks: readonly MessageStreamViewBlock[];
}
@ -79,8 +79,8 @@ export function createMessageStreamSurfaceContext(options: MessageStreamSurfaceC
setDisclosureOpen: (bucket, id, open) => {
options.dispatch({ type: "ui/disclosure-set", bucket, id, open });
},
setForkActionsItem: (itemId) => {
options.dispatch({ type: "ui/message-fork-actions-set", itemId });
setForkMenuItem: (itemId) => {
options.dispatch({ type: "ui/message-fork-menu-set", itemId });
},
loadOlderTurns: options.loadOlderTurns,
renderObsidianMarkdown: options.renderObsidianMarkdown,
@ -111,8 +111,8 @@ function messageStreamContextFromProjection(
workspaceRoot: projection.workspaceRoot,
disclosures: projection.disclosures,
onDisclosureToggle: context.setDisclosureOpen,
forkActionsItemId: projection.forkActionsItemId,
onForkActionsToggle: context.setForkActionsItem,
forkMenuItemId: projection.forkMenuItemId,
onForkMenuToggle: context.setForkMenuItem,
loadOlderTurns: context.loadOlderTurns,
renderObsidianMarkdown: context.renderObsidianMarkdown,
renderStreamMarkdown: context.renderStreamMarkdown,
@ -152,14 +152,14 @@ function messageStreamStateProjection(
const rollbackCandidate = busy ? null : messageStreamRollbackCandidate(state.messageStream);
const forkCandidates = busy ? [] : forkCandidatesFromItems(items);
const implementPlanTarget = implementPlanTargetFromState(state);
const textActionsByItemId = textActionsForMessageStreamItems(rollbackCandidate, forkCandidates, implementPlanTarget);
const textActionTargetsByItemId = textActionTargetsForMessageStreamItems(rollbackCandidate, forkCandidates, implementPlanTarget);
const activeTurn = activeTurnId({ lifecycle: state.turn.lifecycle });
return {
activeThreadId: state.activeThread.id,
workspaceRoot,
disclosures: state.ui.disclosures,
forkActionsItemId: state.ui.messageActions.forkActionsItemId,
forkMenuItemId: state.ui.messageActionMenu.forkMenuItemId,
viewBlocks: messageStreamViewBlocks({
activeThreadId: state.activeThread.id,
activeTurnId: activeTurn,
@ -170,31 +170,35 @@ function messageStreamStateProjection(
activeItems,
workspaceRoot,
turnDiffs: state.messageStream.turnDiffs,
textActionsByItemId,
textActionTargetsByItemId,
pendingRequests: messageStreamBlockItemsEmpty(stableItems, activeItems) ? null : pendingRequestBlockFromContext(context),
}),
};
}
function textActionsForMessageStreamItems(
function textActionTargetsForMessageStreamItems(
rollbackCandidate: MessageStreamRollbackCandidate | null,
forkCandidates: readonly ForkCandidate[],
implementPlanTarget: PlanImplementationTarget | null,
): ReadonlyMap<string, MessageStreamTextActions> {
const byItemId = new Map<string, MessageStreamTextActions>();
): ReadonlyMap<string, MessageStreamTextActionTargets> {
const byItemId = new Map<string, MessageStreamTextActionTargets>();
for (const candidate of forkCandidates) {
patchTextActions(byItemId, candidate.itemId, { fork: { itemId: candidate.itemId, turnId: candidate.turnId } });
patchTextActionTargets(byItemId, candidate.itemId, { fork: { itemId: candidate.itemId, turnId: candidate.turnId } });
}
if (rollbackCandidate) {
patchTextActions(byItemId, rollbackCandidate.itemId, { rollback: true });
patchTextActionTargets(byItemId, rollbackCandidate.itemId, { rollback: true });
}
if (implementPlanTarget) {
patchTextActions(byItemId, implementPlanTarget.itemId, { implementPlan: implementPlanTarget });
patchTextActionTargets(byItemId, implementPlanTarget.itemId, { implementPlan: implementPlanTarget });
}
return byItemId;
}
function patchTextActions(byItemId: Map<string, MessageStreamTextActions>, itemId: string, patch: MessageStreamTextActions): void {
function patchTextActionTargets(
byItemId: Map<string, MessageStreamTextActionTargets>,
itemId: string,
patch: MessageStreamTextActionTargets,
): void {
byItemId.set(itemId, { ...byItemId.get(itemId), ...patch });
}

View file

@ -144,8 +144,8 @@ function isCompletedTurnDetailItem(classification: MessageStreamSemanticClassifi
function turnOutcomeItemsByTurn(items: readonly MessageStreamSemanticClassification[]): Map<string, string> {
const turnOutcomeIdByTurn = new Map<string, string>();
for (const { item, actions } of items) {
if (!item.turnId || !actions.isTurnOutcome) continue;
for (const { item, capabilities } of items) {
if (!item.turnId || !capabilities.isTurnOutcome) continue;
turnOutcomeIdByTurn.set(item.turnId, item.id);
}
return turnOutcomeIdByTurn;

View file

@ -14,7 +14,7 @@ export interface MessageStreamForkTarget {
export type MessageStreamPlanImplementationTarget = PlanImplementationTarget;
export interface MessageStreamTextActions {
export interface MessageStreamTextActionTargets {
fork?: MessageStreamForkTarget;
rollback?: true;
implementPlan?: MessageStreamPlanImplementationTarget;
@ -66,7 +66,7 @@ export interface MessageStreamTextView {
renderMode: MessageStreamTextRenderMode;
collapsible: boolean;
copyText?: string;
actions: MessageStreamTextActions;
actionTargets: MessageStreamTextActionTargets;
metadata: MessageStreamTextMetadataView;
}
@ -75,7 +75,7 @@ type MessageStreamTextRenderMode = "text" | "streamMarkdown" | "obsidianMarkdown
export function messageStreamTextView(
item: MessageStreamItem,
annotations?: MessageStreamItemAnnotations,
options: { activeTurnId?: string | null; actions?: MessageStreamTextActions } = {},
options: { activeTurnId?: string | null; actionTargets?: MessageStreamTextActionTargets } = {},
): MessageStreamTextView {
const renderMode = textRenderMode(item);
const body = bodyForTextItem(item);
@ -88,7 +88,7 @@ export function messageStreamTextView(
renderMode,
collapsible: item.kind === "message" && item.role === "user",
...definedProp("copyText", copyTextForTextItem(item, options.activeTurnId ?? null)),
actions: options.actions ?? {},
actionTargets: options.actionTargets ?? {},
metadata: textMetadataView(item, annotations),
};
}

View file

@ -7,7 +7,7 @@ import type { AgentRunSummary, MessageStreamItem, TaskProgressMessageStreamItem
import { messageStreamLayoutBlocks, type MessageStreamItemAnnotations, type MessageStreamLayoutBlock } from "./layout";
import { detailView, type DetailView } from "./detail-view";
import { messageStreamRenderFamily } from "./render-family";
import { messageStreamTextView, type MessageStreamTextActions, type MessageStreamTextView } from "./text-view";
import { messageStreamTextView, type MessageStreamTextActionTargets, type MessageStreamTextView } from "./text-view";
import {
activeAgentRunSummary,
agentRunSummaryView,
@ -32,7 +32,7 @@ export interface MessageStreamPresentationBlockInput {
activeItems?: readonly MessageStreamItem[] | undefined;
workspaceRoot?: string | null | undefined;
turnDiffs?: ReadonlyMap<string, string> | undefined;
textActionsByItemId?: ReadonlyMap<string, MessageStreamTextActions> | undefined;
textActionTargetsByItemId?: ReadonlyMap<string, MessageStreamTextActionTargets> | undefined;
pendingRequests?: PendingRequestMessageStreamBlockInput | null | undefined;
}
@ -300,7 +300,7 @@ function messageStreamRenderedItemView(
kind: "text",
view: messageStreamTextView(classification.item, annotations, {
activeTurnId: input.activeTurnId,
...definedProp("actions", input.textActionsByItemId?.get(classification.item.id)),
...definedProp("actionTargets", input.textActionTargetsByItemId?.get(classification.item.id)),
}),
};
case "detail":

View file

@ -46,8 +46,8 @@ export interface TextItemContentContext extends TextItemDetailStateContext {
}
export interface TextItemActionContext extends TextItemDetailStateContext {
forkActionsItemId: string | null;
onForkActionsToggle?: (itemId: string | null) => void;
forkMenuItemId: string | null;
onForkMenuToggle?: (itemId: string | null) => void;
copyText?: (text: string) => void;
onImplementPlan?: (target: MessageStreamPlanImplementationTarget) => void;
onRollback?: () => void;

View file

@ -41,23 +41,23 @@ function Text({ view, context }: { view: MessageStreamTextView; context: TextIte
}
function TextHeader({ view, context }: { view: MessageStreamTextView; context: TextItemActionContext }): UiNode {
const forkActionsOpen = context.forkActionsItemId === view.id;
const forkMenuOpen = context.forkMenuItemId === view.id;
const roleRef = useRef<HTMLDivElement | null>(null);
const { fork, implementPlan, rollback } = view.actions;
const { fork, implementPlan, rollback } = view.actionTargets;
useEffect(() => {
if (!forkActionsOpen) return;
if (!forkMenuOpen) return;
const doc = roleRef.current?.ownerDocument;
if (!doc) return;
const closeOnOutsidePointer = (event: PointerEvent) => {
if (event.target instanceof Node && roleRef.current?.contains(event.target)) return;
context.onForkActionsToggle?.(null);
context.onForkMenuToggle?.(null);
};
return listenDomEvent(doc, "pointerdown", closeOnOutsidePointer, true);
}, [context, forkActionsOpen]);
}, [context, forkMenuOpen]);
const copyAction =
view.copyText !== undefined && context.copyText && !forkActionsOpen ? (
view.copyText !== undefined && context.copyText && !forkMenuOpen ? (
<TextAction
icon="copy"
label="Copy message"
@ -67,15 +67,15 @@ function TextHeader({ view, context }: { view: MessageStreamTextView; context: T
) : null;
return (
<div ref={roleRef} className={`codex-panel__message-role${forkActionsOpen ? " codex-panel__message-role--fork-open" : ""}`}>
<div ref={roleRef} className={`codex-panel__message-role${forkMenuOpen ? " codex-panel__message-role--fork-open" : ""}`}>
<span>{view.roleLabel}</span>
{forkActionsOpen && fork ? (
{forkMenuOpen && fork ? (
<TextAction
icon="archive"
label="Fork and archive"
className="codex-panel__fork-and-archive-message"
onClick={() => {
context.onForkActionsToggle?.(null);
context.onForkMenuToggle?.(null);
context.onFork?.(fork, true);
}}
/>
@ -84,15 +84,15 @@ function TextHeader({ view, context }: { view: MessageStreamTextView; context: T
)}
{fork ? (
<TextAction
icon={forkActionsOpen ? "file-plus-corner" : "lucide-split"}
label={forkActionsOpen ? "Fork" : "Fork from here"}
icon={forkMenuOpen ? "file-plus-corner" : "lucide-split"}
label={forkMenuOpen ? "Fork" : "Fork from here"}
className="codex-panel__fork-message"
onClick={() => {
if (forkActionsOpen) {
context.onForkActionsToggle?.(null);
if (forkMenuOpen) {
context.onForkMenuToggle?.(null);
context.onFork?.(fork, false);
} else {
context.onForkActionsToggle?.(view.id);
context.onForkMenuToggle?.(view.id);
}
}}
/>

View file

@ -33,7 +33,7 @@ function createHost(draft: string) {
},
setDraft,
},
slashCommands: { execute },
slashCommandExecutor: { execute },
turnSubmission: { sendTurnText },
connection: {
currentClient: () => client,

View file

@ -7,8 +7,8 @@ import { createChatState } from "../../../../../src/features/chat/application/st
import { createChatStateStore } from "../../../../../src/features/chat/application/state/store";
import {
executeSlashCommandWithState,
type SlashCommandHandlerHost,
} from "../../../../../src/features/chat/application/conversation/slash-command-handler";
type SlashCommandExecutorHost,
} from "../../../../../src/features/chat/application/conversation/slash-command-executor";
import type { Thread } from "../../../../../src/domain/threads/model";
const textInput = (text: string): CodexInput => [{ type: "text", text }];
@ -24,14 +24,14 @@ function thread(id: string, name: string | null = null): Thread {
};
}
type SlashCommandHostOverrides = Partial<SlashCommandHandlerHost>;
type SlashCommandExecutorHostOverrides = Partial<SlashCommandExecutorHost>;
function createHost(overrides: SlashCommandHostOverrides = {}) {
function createHost(overrides: SlashCommandExecutorHostOverrides = {}) {
const stateStore = createChatStateStore(createChatState());
const threadTurnsList = vi.fn().mockResolvedValue({ data: [] });
const client = { threadTurnsList } as unknown as AppServerClient;
const compactThread = vi.fn().mockResolvedValue(undefined);
const host: SlashCommandHandlerHost = {
const host: SlashCommandExecutorHost = {
stateStore,
currentClient: () => client,
codexInput: vi.fn((text: string) => textInput(text)),

View file

@ -34,7 +34,7 @@ describe("message stream semantic classification", () => {
{ scope: "turn", turnId: "turn", turnRole: "steer" },
{ scope: "turn", turnId: "turn", turnRole: "outcome" },
]);
expect(semantic[2]?.actions).toMatchObject({ isTurnOutcome: true, canForkFromHere: true });
expect(semantic[2]?.capabilities).toMatchObject({ isTurnOutcome: true, canForkFromHere: true });
});
it("uses local steer provenance before falling back to turn order", () => {
@ -175,13 +175,13 @@ describe("message stream semantic classification", () => {
meaning: { plane: "dialogue", event: "proposal" },
placement: { scope: "turn", turnRole: "detail" },
lifecycle: { state: "running" },
actions: { canImplementPlan: false, isTurnOutcome: false },
capabilities: { canImplementPlan: false, isTurnOutcome: false },
});
expect(completed).toMatchObject({
meaning: { plane: "dialogue", event: "proposal" },
placement: { scope: "turn", turnRole: "outcome" },
lifecycle: { state: "completed" },
actions: { canImplementPlan: true, isTurnOutcome: true },
capabilities: { canImplementPlan: true, isTurnOutcome: true },
});
});

View file

@ -211,7 +211,7 @@ function shellParts(): ChatPanelShellParts {
}),
},
composer: {
controller: {
renderer: {
renderState: (state) => ({
viewId: "view",
draft: state.composer.draft,

View file

@ -64,7 +64,7 @@ describe("MessageStreamPresenter scroll pinning", () => {
expect(projection.context.workspaceRoot).toBe("/repo");
expect(projection.blocks).toEqual([{ kind: "empty", key: "empty" }]);
expect(projection.context.disclosures.textDetails.size).toBe(0);
expect(projection.context.forkActionsItemId).toBeNull();
expect(projection.context.forkMenuItemId).toBeNull();
});
it("wires message stream disclosure actions through the surface context", () => {
@ -578,8 +578,8 @@ function messageStreamSurfaceContext(options: {
setDisclosureOpen: (bucket, id, open) => {
options.dispatch({ type: "ui/disclosure-set", bucket, id, open });
},
setForkActionsItem: (itemId) => {
options.dispatch({ type: "ui/message-fork-actions-set", itemId });
setForkMenuItem: (itemId) => {
options.dispatch({ type: "ui/message-fork-menu-set", itemId });
},
loadOlderTurns: vi.fn(),
renderObsidianMarkdown: vi.fn(),

View file

@ -100,7 +100,7 @@ function shellParts(store: ReturnType<typeof createChatStateStore>, toolbarActio
}),
},
composer: {
controller: {
renderer: {
renderState: () => ({
viewId: "view",
draft: "",

View file

@ -42,7 +42,7 @@ describe("chatReducer", () => {
state = chatStateWith(state, { composer: { suggestionsDismissedSignature: "dismissed" } });
state = chatStateWith(state, { ui: { disclosures: { approvalDetails: new Set(["1:details"]) } } });
state = chatStateWith(state, { ui: { disclosures: { textDetails: new Set(["previous:details"]) } } });
state = chatStateWith(state, { ui: { messageActions: { forkActionsItemId: "previous" } } });
state = chatStateWith(state, { ui: { messageActionMenu: { forkMenuItemId: "previous" } } });
state = chatStateWith(state, {
ui: { goalEditor: { kind: "editing", threadId: "thread", objectiveDraft: "draft", tokenBudgetDraft: null } },
});
@ -79,7 +79,7 @@ describe("chatReducer", () => {
expect(next.composer.suggestions).toEqual([]);
expect(next.composer.suggestionsDismissedSignature).toBeNull();
expect(uiDisclosureCount(next)).toBe(0);
expect(next.ui.messageActions.forkActionsItemId).toBeNull();
expect(next.ui.messageActionMenu.forkMenuItemId).toBeNull();
expect(next.ui.goalEditor.kind).toBe("closed");
});
@ -103,7 +103,7 @@ describe("chatReducer", () => {
state = chatStateWith(state, { runtime: { selectedCollaborationMode: "plan" } });
state = chatStateWith(state, { ui: { disclosures: { approvalDetails: new Set(["1:details"]) } } });
state = chatStateWith(state, { ui: { disclosures: { textDetails: new Set(["previous:details"]) } } });
state = chatStateWith(state, { ui: { messageActions: { forkActionsItemId: "previous" } } });
state = chatStateWith(state, { ui: { messageActionMenu: { forkMenuItemId: "previous" } } });
state = chatStateWith(state, {
ui: { goalEditor: { kind: "editing", threadId: "previous-thread", objectiveDraft: "draft", tokenBudgetDraft: null } },
});
@ -139,7 +139,7 @@ describe("chatReducer", () => {
expect(next.runtime.activeCollaborationMode).toBeNull();
expect(next.runtime.selectedCollaborationMode).toBe("default");
expect(uiDisclosureCount(next)).toBe(0);
expect(next.ui.messageActions.forkActionsItemId).toBeNull();
expect(next.ui.messageActionMenu.forkMenuItemId).toBeNull();
expect(next.ui.goalEditor.kind).toBe("closed");
});
@ -590,7 +590,7 @@ describe("chatReducer", () => {
expect(state.ui.toolbarPanel).toBe("status-panel");
});
it("updates typed disclosures, message actions, goal editor, and user input drafts through typed UI actions", () => {
it("updates typed disclosures, message action menu, goal editor, and user input drafts through typed UI actions", () => {
let state = chatStateFixture();
state = chatReducer(state, { type: "ui/disclosure-set", bucket: "approvalDetails", id: "1:details", open: true });
@ -598,8 +598,8 @@ describe("chatReducer", () => {
state = chatReducer(state, { type: "ui/disclosure-set", bucket: "goalObjectiveExpanded", id: "thread", open: true });
expect(state.ui.disclosures.goalObjectiveExpanded.has("thread")).toBe(true);
state = chatReducer(state, { type: "ui/message-fork-actions-set", itemId: "message-1" });
expect(state.ui.messageActions.forkActionsItemId).toBe("message-1");
state = chatReducer(state, { type: "ui/message-fork-menu-set", itemId: "message-1" });
expect(state.ui.messageActionMenu.forkMenuItemId).toBe("message-1");
state = chatReducer(state, { type: "ui/goal-editor-started", threadId: "thread", objective: "old", tokenBudget: 10 });
state = chatReducer(state, { type: "ui/goal-editor-draft-updated", objective: "new" });

View file

@ -4,10 +4,10 @@ import type { AppServerClient } from "../../../../src/app-server/connection/clie
import { createChatStateStore } from "../../../../src/features/chat/application/state/store";
import { messageStreamItems } from "../../../../src/features/chat/application/state/message-stream";
import {
createAutoTitleActions,
type AutoTitleActions,
type AutoTitleActionsHost,
} from "../../../../src/features/chat/application/threads/auto-title-actions";
createAutoTitleCoordinator,
type AutoTitleCoordinator,
type AutoTitleCoordinatorHost,
} from "../../../../src/features/chat/application/threads/auto-title-coordinator";
import { threadTitleContextFromMessageStreamItems } from "../../../../src/features/chat/application/threads/title-context";
import { createThreadTitleService } from "../../../../src/features/threads/thread-title-service";
import type { Thread } from "../../../../src/domain/threads/model";
@ -15,11 +15,11 @@ import type { ThreadTitleContext } from "../../../../src/domain/threads/title-ge
import { DEFAULT_SETTINGS } from "../../../../src/settings/model";
import { deferred } from "../../../support/async";
describe("AutoTitleActions", () => {
describe("AutoTitleCoordinator", () => {
it("prefers visible turn items over completed turn summaries for active streamed turns", async () => {
const setThreadName = vi.fn().mockResolvedValue({});
const generateThreadTitle = vi.fn().mockResolvedValue("Visible context title");
const { actions, stateStore } = actionsFixture({
const { coordinator, stateStore } = coordinatorFixture({
currentClient: () => fakeClient({ setThreadName }),
generateThreadTitle,
});
@ -39,7 +39,7 @@ describe("AutoTitleActions", () => {
],
});
actions.maybeAutoTitleThread("thread", "turn", {
coordinator.maybeAutoTitleThread("thread", "turn", {
userText: "Completed payload request.",
assistantText: "Completed payload response.",
});
@ -55,7 +55,7 @@ describe("AutoTitleActions", () => {
it("uses visible turn items when completed turn summaries are unavailable", async () => {
const setThreadName = vi.fn().mockResolvedValue({});
const generateThreadTitle = vi.fn().mockResolvedValue("Visible context title");
const { actions, stateStore } = actionsFixture({
const { coordinator, stateStore } = coordinatorFixture({
currentClient: () => fakeClient({ setThreadName }),
generateThreadTitle,
});
@ -75,7 +75,7 @@ describe("AutoTitleActions", () => {
],
});
actions.maybeAutoTitleThread("thread", "turn", null);
coordinator.maybeAutoTitleThread("thread", "turn", null);
await flushPromises();
expect(generateThreadTitle).toHaveBeenCalledWith({
@ -88,12 +88,12 @@ describe("AutoTitleActions", () => {
it("does not apply a completed auto-title after the thread leaves the list", async () => {
const generatedTitle = deferred<string | null>();
const setThreadName = vi.fn().mockResolvedValue({});
const { actions, stateStore, notifyThreadRenamed } = actionsFixture({
const { coordinator, stateStore, notifyThreadRenamed } = coordinatorFixture({
currentClient: () => fakeClient({ setThreadName }),
generateThreadTitle: vi.fn(() => generatedTitle.promise),
});
actions.maybeAutoTitleThread("thread", "turn", { userText: "Please name this.", assistantText: "Done." });
coordinator.maybeAutoTitleThread("thread", "turn", { userText: "Please name this.", assistantText: "Done." });
await flushPromises();
stateStore.dispatch({ type: "thread-list/applied", threads: [] });
@ -107,12 +107,12 @@ describe("AutoTitleActions", () => {
it("does not overwrite a manual name when auto-title save finishes later", async () => {
const savedName = deferred<object>();
const setThreadName = vi.fn(() => savedName.promise);
const { actions, stateStore, notifyThreadRenamed } = actionsFixture({
const { coordinator, stateStore, notifyThreadRenamed } = coordinatorFixture({
currentClient: () => fakeClient({ setThreadName }),
generateThreadTitle: vi.fn().mockResolvedValue("Generated title"),
});
actions.maybeAutoTitleThread("thread", "turn", { userText: "Please name this.", assistantText: "Done." });
coordinator.maybeAutoTitleThread("thread", "turn", { userText: "Please name this.", assistantText: "Done." });
await flushPromises();
expect(setThreadName).toHaveBeenCalledWith("thread", "Generated title");
@ -125,13 +125,13 @@ describe("AutoTitleActions", () => {
});
});
function actionsFixture(
function coordinatorFixture(
overrides: {
currentClient?: () => AppServerClient;
generateThreadTitle?: (context: ThreadTitleContext) => Promise<string | null>;
} = {},
): AutoTitleActionsHost & {
actions: AutoTitleActions;
): AutoTitleCoordinatorHost & {
coordinator: AutoTitleCoordinator;
notifyThreadRenamed: ReturnType<typeof vi.fn>;
} {
const stateStore = createChatStateStore();
@ -159,8 +159,8 @@ function actionsFixture(
if (options.shouldPublish()) notifyThreadRenamed(threadId, value);
return true;
},
} satisfies AutoTitleActionsHost;
return { ...host, notifyThreadRenamed, actions: createAutoTitleActions(host) };
} satisfies AutoTitleCoordinatorHost;
return { ...host, notifyThreadRenamed, coordinator: createAutoTitleCoordinator(host) };
}
function fakeClient(options: { setThreadName?: ReturnType<typeof vi.fn> } = {}): AppServerClient {

View file

@ -25,7 +25,7 @@ import {
withMessageContentScrollHeight,
} from "./test-helpers";
describe("message stream rendering and message actions", () => {
describe("message stream rendering and message action menu", () => {
it("inserts completed-turn activity groups between conversation blocks", () => {
const parent = document.createElement("div");
const baseContext = {
@ -34,7 +34,7 @@ describe("message stream rendering and message actions", () => {
historyCursor: null,
loadingHistory: false,
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (element: HTMLElement, text: string) => element.createDiv({ text }),
};
@ -125,7 +125,7 @@ describe("message stream rendering and message actions", () => {
},
],
disclosures: testDisclosures({ activityGroups: ["t1"] }),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (element: HTMLElement, text: string) => element.createDiv({ text }),
});
@ -163,7 +163,7 @@ describe("message stream rendering and message actions", () => {
loadingHistory: false,
items: [{ id: "review-1", kind: "reviewResult", role: "tool", text: "Auto-review denied this command." }],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -201,7 +201,7 @@ describe("message stream rendering and message actions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -243,7 +243,7 @@ describe("message stream rendering and message actions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown,
})[0];
@ -277,7 +277,7 @@ describe("message stream rendering and message actions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (element, text) => element.createDiv({ text }),
})[0];
@ -315,10 +315,10 @@ describe("message stream rendering and message actions", () => {
loadingHistory: false,
items: [...items],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
textActionsByItemId: new Map([["u2", { rollback: true }]]),
textActionTargetsByItemId: new Map([["u2", { rollback: true }]]),
onRollback,
});
@ -353,7 +353,7 @@ describe("message stream rendering and message actions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
copyText,
@ -372,7 +372,7 @@ describe("message stream rendering and message actions", () => {
});
it("expands assistant fork actions in the copy action region and defaults repeat clicks to plain fork", () => {
const onForkActionsToggle = vi.fn();
const onForkMenuToggle = vi.fn();
const onFork = vi.fn();
const item: MessageStreamItem = {
id: "a1",
@ -392,12 +392,12 @@ describe("message stream rendering and message actions", () => {
loadingHistory: false,
items: [item],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
onForkActionsToggle,
forkMenuItemId: null,
onForkMenuToggle,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
copyText: vi.fn(),
textActionsByItemId: new Map([["a1", { fork: { itemId: "a1", turnId: "turn-1" } }]]),
textActionTargetsByItemId: new Map([["a1", { fork: { itemId: "a1", turnId: "turn-1" } }]]),
onFork,
})[0];
@ -407,7 +407,7 @@ describe("message stream rendering and message actions", () => {
expect(initialFork.getAttribute("aria-label")).toBe("Fork from here");
expect(initialFork.getAttribute("data-icon")).toBe("lucide-split");
initialFork.click();
expect(onForkActionsToggle).toHaveBeenCalledWith("a1");
expect(onForkMenuToggle).toHaveBeenCalledWith("a1");
const openBlock = messageStreamBlocks({
activeThreadId: "thread",
@ -416,12 +416,12 @@ describe("message stream rendering and message actions", () => {
loadingHistory: false,
items: [item],
disclosures: emptyDisclosures(),
forkActionsItemId: "a1",
onForkActionsToggle,
forkMenuItemId: "a1",
onForkMenuToggle,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
copyText: vi.fn(),
textActionsByItemId: new Map([["a1", { fork: { itemId: "a1", turnId: "turn-1" } }]]),
textActionTargetsByItemId: new Map([["a1", { fork: { itemId: "a1", turnId: "turn-1" } }]]),
onFork,
})[0];
@ -456,12 +456,12 @@ describe("message stream rendering and message actions", () => {
loadingHistory: false,
items: [item],
disclosures: emptyDisclosures(),
forkActionsItemId: "a1",
onForkActionsToggle: vi.fn(),
forkMenuItemId: "a1",
onForkMenuToggle: vi.fn(),
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
copyText: vi.fn(),
textActionsByItemId: new Map([["a1", { fork: { itemId: "a1", turnId: "turn-1" } }]]),
textActionTargetsByItemId: new Map([["a1", { fork: { itemId: "a1", turnId: "turn-1" } }]]),
onFork,
})[0];
@ -479,7 +479,7 @@ describe("message stream rendering and message actions", () => {
historyCursor: null,
loadingHistory: false,
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (element: HTMLElement, text: string) => element.createDiv({ text: `markdown:${text}` }),
};
@ -537,7 +537,7 @@ describe("message stream rendering and message actions", () => {
historyCursor: null,
loadingHistory: false,
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown,
};
@ -612,7 +612,7 @@ describe("message stream rendering and message actions", () => {
historyCursor: null,
loadingHistory: false,
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (element: HTMLElement, text: string) => {
markdownRenderer.renderObsidianMarkdown(element, text);
@ -686,7 +686,7 @@ describe("message stream rendering and message actions", () => {
historyCursor: null,
loadingHistory: false,
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderObsidianMarkdown: vi.fn(),
renderStreamMarkdown,
@ -739,7 +739,7 @@ describe("message stream rendering and message actions", () => {
historyCursor: null,
loadingHistory: false,
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderObsidianMarkdown: (element: HTMLElement, text: string) => {
markdownRenderer.renderObsidianMarkdown(element, text);
@ -820,7 +820,7 @@ describe("message stream rendering and message actions", () => {
loadingHistory: false,
items: [item],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent: HTMLElement, text: string) => parent.createDiv({ text }),
copyText: vi.fn(),
@ -853,11 +853,11 @@ describe("message stream rendering and message actions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
copyText: vi.fn(),
textActionsByItemId: new Map([["p1", { implementPlan: { itemId: "p1" } }]]),
textActionTargetsByItemId: new Map([["p1", { implementPlan: { itemId: "p1" } }]]),
onImplementPlan,
})[0];
@ -931,7 +931,7 @@ describe("message stream rendering and message actions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
copyText: vi.fn(),
@ -950,11 +950,11 @@ describe("message stream rendering and message actions", () => {
loadingHistory: false,
items: [{ id: "u1", kind: "message", messageKind: "user", role: "user", text: "latest", copyText: "latest", turnId: "turn-1" }],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
copyText,
textActionsByItemId: new Map([["u1", { rollback: true }]]),
textActionTargetsByItemId: new Map([["u1", { rollback: true }]]),
onRollback,
})[0];
@ -999,7 +999,7 @@ describe("message stream rendering and message actions", () => {
},
],
disclosures: testDisclosures({ userMessageExpanded: [...expandedMessages] }),
forkActionsItemId: null,
forkMenuItemId: null,
onDisclosureToggle,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
@ -1054,7 +1054,7 @@ describe("message stream rendering and message actions", () => {
loadingHistory: false,
items: [{ id: "u1", kind: "message", messageKind: "user", role: "user", text: "short", turnId: "turn-1" }],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -1081,7 +1081,7 @@ describe("message stream rendering and message actions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -1099,7 +1099,7 @@ describe("message stream rendering and message actions", () => {
loadingHistory: false,
items: [{ id: "u1", kind: "message", messageKind: "user", role: "user", text: "running", turnId: "turn-1" }],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -1129,7 +1129,7 @@ describe("message stream rendering and message actions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -1167,7 +1167,7 @@ describe("message stream rendering and message actions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -1204,7 +1204,7 @@ describe("message stream rendering and message actions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -1235,7 +1235,7 @@ describe("message stream rendering and message actions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -1264,7 +1264,7 @@ describe("message stream rendering and message actions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -1299,7 +1299,7 @@ describe("message stream rendering and message actions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -1338,7 +1338,7 @@ describe("message stream rendering and message actions", () => {
],
turnDiffs: new Map([["turn", "diff --git a/src/main.ts b/src/main.ts\n@@\n-old\n+new"]]),
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
openTurnDiff,
@ -1386,7 +1386,7 @@ describe("message stream rendering and message actions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
});
@ -1417,7 +1417,7 @@ describe("message stream rendering and message actions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
});
@ -1458,7 +1458,7 @@ describe("message stream rendering and message actions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
openTurnDiff: vi.fn(),

View file

@ -373,7 +373,7 @@ describe("pending request renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown,
})[0];
@ -412,7 +412,7 @@ describe("pending request renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -464,7 +464,7 @@ describe("pending request renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
});
@ -485,7 +485,7 @@ describe("pending request renderer decisions", () => {
loadingHistory: false,
items: [{ id: "a1", kind: "message", role: "assistant", text: "Done", messageKind: "assistantResponse", messageState: "completed" }],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
pendingRequests: pendingRequestContext({
@ -507,7 +507,7 @@ describe("pending request renderer decisions", () => {
loadingHistory: false,
items: [{ id: "a1", kind: "message", role: "assistant", text: "Done", messageKind: "assistantResponse", messageState: "completed" }],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
pendingRequests: pendingRequestContext({
@ -537,7 +537,7 @@ describe("pending request renderer decisions", () => {
{ id: "a1", kind: "message", role: "assistant", text: "Done", messageKind: "assistantResponse", messageState: "completed" },
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (element, text) => element.createDiv({ text }),
pendingRequests: pendingRequestContext({
@ -567,7 +567,7 @@ describe("pending request renderer decisions", () => {
loadingHistory: false,
items: [],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
pendingRequests: pendingRequestContext({
@ -593,7 +593,7 @@ describe("pending request renderer decisions", () => {
{ id: "a1", kind: "message", role: "assistant", text: "Done", messageKind: "assistantResponse", messageState: "completed" },
] satisfies MessageStreamItem[],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (element: HTMLElement, text: string) => element.createDiv({ text }),
};

View file

@ -40,7 +40,7 @@ describe("message stream item renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -85,7 +85,7 @@ describe("message stream item renderer decisions", () => {
},
],
disclosures: testDisclosures({ activityGroups: ["turn"] }),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
}).find((item) => item.key === "activity:turn-turn-activity");
@ -115,7 +115,7 @@ describe("message stream item renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -146,7 +146,7 @@ describe("message stream item renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -174,7 +174,7 @@ describe("message stream item renderer decisions", () => {
loadingHistory: false,
items: [item] satisfies MessageStreamItem[],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (element: HTMLElement, text: string) => element.createDiv({ text }),
};
@ -206,7 +206,7 @@ describe("message stream item renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -234,7 +234,7 @@ describe("message stream item renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -269,7 +269,7 @@ describe("message stream item renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -322,7 +322,7 @@ describe("message stream item renderer decisions", () => {
},
],
disclosures: testDisclosures({ activityGroups: ["turn"], details: ["hook-1:details"] }),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
});
@ -361,7 +361,7 @@ describe("message stream item renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -419,7 +419,7 @@ describe("message stream item renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
pendingRequests: {
@ -485,7 +485,7 @@ describe("message stream item renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
});
@ -543,7 +543,7 @@ describe("message stream item renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
});
@ -581,7 +581,7 @@ describe("message stream item renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -623,7 +623,7 @@ describe("message stream item renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
})[0];
@ -661,7 +661,7 @@ describe("message stream item renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
onDisclosureToggle,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
@ -712,7 +712,7 @@ describe("message stream item renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
});
@ -745,7 +745,7 @@ describe("message stream item renderer decisions", () => {
loadingHistory: false,
items: [item],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (element, text) => element.createDiv({ text }),
}),
@ -769,7 +769,7 @@ describe("message stream item renderer decisions", () => {
loadingHistory: false,
items: [item],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (element, text) => element.createDiv({ text }),
}),
@ -807,7 +807,7 @@ describe("message stream item renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
});
@ -842,7 +842,7 @@ describe("message stream item renderer decisions", () => {
},
],
disclosures: emptyDisclosures(),
forkActionsItemId: null,
forkMenuItemId: null,
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
});

View file

@ -15,7 +15,7 @@ import type {
} from "../../../../../src/features/chat/ui/message-stream/context";
import type { MessageStreamItem } from "../../../../../src/features/chat/domain/message-stream/items";
import { messageStreamViewBlocks } from "../../../../../src/features/chat/presentation/message-stream/view-model";
import type { MessageStreamTextActions } from "../../../../../src/features/chat/presentation/message-stream/text-view";
import type { MessageStreamTextActionTargets } from "../../../../../src/features/chat/presentation/message-stream/text-view";
import { MessageStreamViewport } from "../../../../../src/features/chat/ui/message-stream/viewport";
import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/ui/ui-root";
@ -33,7 +33,7 @@ export function messageStreamBlocks(
activeItems: context.activeItems,
workspaceRoot: normalized.workspaceRoot,
turnDiffs: context.turnDiffs,
textActionsByItemId: context.textActionsByItemId,
textActionTargetsByItemId: context.textActionTargetsByItemId,
pendingRequests: pendingRequestBlockInput(context),
});
const blocks = rawMessageStreamBlocks(viewBlocks, normalized);
@ -61,9 +61,9 @@ function messageStreamBlockItemsEmpty(context: TestMessageStreamContext): boolea
type TestMessageStreamContext = Omit<
MessageStreamContext,
"disclosures" | "forkActionsItemId" | "renderObsidianMarkdown" | "renderStreamMarkdown"
"disclosures" | "forkMenuItemId" | "renderObsidianMarkdown" | "renderStreamMarkdown"
> &
Partial<Pick<MessageStreamContext, "disclosures" | "forkActionsItemId" | "renderObsidianMarkdown" | "renderStreamMarkdown">> & {
Partial<Pick<MessageStreamContext, "disclosures" | "forkMenuItemId" | "renderObsidianMarkdown" | "renderStreamMarkdown">> & {
renderMarkdown?: (parent: HTMLElement, text: string) => void;
turnLifecycle: MessageStreamTurnLifecycleState;
historyCursor: string | null;
@ -72,7 +72,7 @@ type TestMessageStreamContext = Omit<
stableItems?: readonly MessageStreamItem[];
activeItems?: readonly MessageStreamItem[];
turnDiffs?: ReadonlyMap<string, string>;
textActionsByItemId?: ReadonlyMap<string, MessageStreamTextActions>;
textActionTargetsByItemId?: ReadonlyMap<string, MessageStreamTextActionTargets>;
};
type MessageStreamTurnLifecycleState =
@ -108,7 +108,7 @@ function normalizeMessageStreamContext(context: TestMessageStreamContext): Messa
return {
...context,
disclosures: context.disclosures ?? emptyDisclosures(),
forkActionsItemId: context.forkActionsItemId ?? null,
forkMenuItemId: context.forkMenuItemId ?? null,
renderObsidianMarkdown,
renderStreamMarkdown,
};