From 43e8d7fc208eb523886db592d037740f4539db69 Mon Sep 17 00:00:00 2001 From: murashit Date: Thu, 18 Jun 2026 20:58:25 +0900 Subject: [PATCH] Align chat naming conventions --- docs/development.md | 8 ++ eslint.config.mjs | 6 +- .../conversation/composer-submit-actions.ts | 4 +- .../application/conversation/composition.ts | 12 +-- .../conversation/slash-command-execution.ts | 4 +- ...d-handler.ts => slash-command-executor.ts} | 8 +- .../chat/application/state/root-reducer.ts | 2 +- .../chat/application/state/ui-state.ts | 20 ++-- ...e-actions.ts => auto-title-coordinator.ts} | 8 +- .../chat/domain/message-stream/selectors.ts | 8 +- .../message-stream/semantics/classify.ts | 8 +- .../domain/message-stream/semantics/types.ts | 4 +- src/features/chat/host/connection-bundle.ts | 10 +- src/features/chat/host/session-graph.ts | 32 +++---- src/features/chat/host/session.ts | 2 +- src/features/chat/panel/shell.tsx | 6 +- .../panel/surface/composer-projection.tsx | 8 +- .../surface/message-stream-projection.ts | 38 ++++---- .../presentation/message-stream/layout.ts | 4 +- .../presentation/message-stream/text-view.ts | 8 +- .../presentation/message-stream/view-model.ts | 6 +- .../chat/ui/message-stream/context.ts | 4 +- src/features/chat/ui/message-stream/text.tsx | 28 +++--- .../turns/composer-submit-actions.test.ts | 2 +- ...test.ts => slash-command-executor.test.ts} | 10 +- .../chat/message-stream/semantics.test.ts | 6 +- tests/features/chat/panel/shell.test.tsx | 2 +- .../surface/message-stream-presenter.test.ts | 6 +- .../chat/panel/toolbar-archive-state.test.tsx | 2 +- tests/features/chat/state-reducer.test.ts | 14 +-- ...test.ts => auto-title-coordinator.test.ts} | 36 ++++---- .../blocks-and-messages.test.tsx | 92 +++++++++---------- .../message-stream/pending-requests.test.tsx | 16 ++-- .../ui/message-stream/stream-items.test.tsx | 42 ++++----- .../chat/ui/message-stream/test-helpers.tsx | 12 +-- 35 files changed, 245 insertions(+), 233 deletions(-) rename src/features/chat/application/conversation/{slash-command-handler.ts => slash-command-executor.ts} (95%) rename src/features/chat/application/threads/{auto-title-actions.ts => auto-title-coordinator.ts} (91%) rename tests/features/chat/conversation/turns/{slash-command-handler.test.ts => slash-command-executor.test.ts} (96%) rename tests/features/chat/threads/{auto-title-actions.test.ts => auto-title-coordinator.test.ts} (86%) diff --git a/docs/development.md b/docs/development.md index 599502b0..20092ad4 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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: diff --git a/eslint.config.mjs b/eslint.config.mjs index 52ce1bb6..63531236 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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)$/]", diff --git a/src/features/chat/application/conversation/composer-submit-actions.ts b/src/features/chat/application/conversation/composer-submit-actions.ts index 14a65bee..b8092882 100644 --- a/src/features/chat/application/conversation/composer-submit-actions.ts +++ b/src/features/chat/application/conversation/composer-submit-actions.ts @@ -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; }; turnSubmission: { @@ -58,7 +58,7 @@ async function sendMessage(host: ComposerSubmitActionsHost): Promise { 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 }); } diff --git a/src/features/chat/application/conversation/composition.ts b/src/features/chat/application/conversation/composition.ts index 78525d98..80052aaa 100644 --- a/src/features/chat/application/conversation/composition.ts +++ b/src/features/chat/application/conversation/composition.ts @@ -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 { - const response = await actions.startThread(objective, { syncGoal: false }); +async function startThreadForGoal(starter: ConversationThreadStarter, objective: string): Promise { + const response = await starter.startThread(objective, { syncGoal: false }); return response?.threadId ?? null; } diff --git a/src/features/chat/application/conversation/slash-command-execution.ts b/src/features/chat/application/conversation/slash-command-execution.ts index 3a7764be..ce89fb3c 100644 --- a/src/features/chat/application/conversation/slash-command-execution.ts +++ b/src/features/chat/application/conversation/slash-command-execution.ts @@ -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; startThreadForGoal: (objective: string) => Promise; resumeThread: (threadId: string) => Promise; @@ -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[]; diff --git a/src/features/chat/application/conversation/slash-command-handler.ts b/src/features/chat/application/conversation/slash-command-executor.ts similarity index 95% rename from src/features/chat/application/conversation/slash-command-handler.ts rename to src/features/chat/application/conversation/slash-command-executor.ts index 4b32bf30..2807342f 100644 --- a/src/features/chat/application/conversation/slash-command-handler.ts +++ b/src/features/chat/application/conversation/slash-command-executor.ts @@ -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 { @@ -61,7 +61,7 @@ function supportedReasoningEfforts(state: ReturnType } async function referencedThreadInput( - host: SlashCommandHandlerHost, + host: SlashCommandExecutorHost, client: AppServerClient, thread: Thread, message: string, diff --git a/src/features/chat/application/state/root-reducer.ts b/src/features/chat/application/state/root-reducer.ts index e02ad350..6a9e4ed2 100644 --- a/src/features/chat/application/state/root-reducer.ts +++ b/src/features/chat/application/state/root-reducer.ts @@ -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), }, }; diff --git a/src/features/chat/application/state/ui-state.ts b/src/features/chat/application/state/ui-state.ts index ecaf7f9e..1fc3629d 100644 --- a/src/features/chat/application/state/ui-state.ts +++ b/src/features/chat/application/state/ui-state.ts @@ -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 { diff --git a/src/features/chat/application/threads/auto-title-actions.ts b/src/features/chat/application/threads/auto-title-coordinator.ts similarity index 91% rename from src/features/chat/application/threads/auto-title-actions.ts rename to src/features/chat/application/threads/auto-title-coordinator.ts index 5c7127e0..9bd8697e 100644 --- a/src/features/chat/application/threads/auto-title-actions.ts +++ b/src/features/chat/application/threads/auto-title-coordinator.ts @@ -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; } -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(); const inFlightThreadIds = new Set(); @@ -65,6 +65,6 @@ export function createAutoTitleActions(host: AutoTitleActionsHost): AutoTitleAct }; } -function state(host: AutoTitleActionsHost): ChatState { +function state(host: AutoTitleCoordinatorHost): ChatState { return host.stateStore.getState(); } diff --git a/src/features/chat/domain/message-stream/selectors.ts b/src/features/chat/domain/message-stream/selectors.ts index 39cb25d3..f9acce0e 100644 --- a/src/features/chat/domain/message-stream/selectors.ts +++ b/src/features/chat/domain/message-stream/selectors.ts @@ -12,18 +12,18 @@ export interface PlanImplementationTarget { export function forkCandidatesFromItems(items: readonly MessageStreamItem[]): readonly ForkCandidate[] { const turnOutcomeItemsByTurn = new Map(); - 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; } diff --git a/src/features/chat/domain/message-stream/semantics/classify.ts b/src/features/chat/domain/message-stream/semantics/classify.ts index 780639d0..7e430076 100644 --- a/src/features/chat/domain/message-stream/semantics/classify.ts +++ b/src/features/chat/domain/message-stream/semantics/classify.ts @@ -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, -): MessageStreamSemanticActions { +): MessageStreamSemanticCapabilities { const { item, placement, meaning, lifecycle } = classification; const isDialogueOutcome = placement.scope === "turn" && diff --git a/src/features/chat/domain/message-stream/semantics/types.ts b/src/features/chat/domain/message-stream/semantics/types.ts index a0fdd743..292926c2 100644 --- a/src/features/chat/domain/message-stream/semantics/types.ts +++ b/src/features/chat/domain/message-stream/semantics/types.ts @@ -56,7 +56,7 @@ export interface MessageStreamLifecycle { state: Exclude; } -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; } diff --git a/src/features/chat/host/connection-bundle.ts b/src/features/chat/host/connection-bundle.ts index e089201a..cf707d1b 100644 --- a/src/features/chat/host/connection-bundle.ts +++ b/src/features/chat/host/connection-bundle.ts @@ -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(); diff --git a/src/features/chat/host/session-graph.ts b/src/features/chat/host/session-graph.ts index 3f577d4e..b73255d6 100644 --- a/src/features/chat/host/session-graph.ts +++ b/src/features/chat/host/session-graph.ts @@ -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, 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; 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: { diff --git a/src/features/chat/host/session.ts b/src/features/chat/host/session.ts index b4ed7c52..02703aac 100644 --- a/src/features/chat/host/session.ts +++ b/src/features/chat/host/session.ts @@ -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(), }, diff --git a/src/features/chat/panel/shell.tsx b/src/features/chat/panel/shell.tsx index 7cf5b947..55e5040f 100644 --- a/src/features/chat/panel/shell.tsx +++ b/src/features/chat/panel/shell.tsx @@ -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
- +
diff --git a/src/features/chat/panel/surface/composer-projection.tsx b/src/features/chat/panel/surface/composer-projection.tsx index 1c607dae..2cecdc6d 100644 --- a/src/features/chat/panel/surface/composer-projection.tsx +++ b/src/features/chat/panel/surface/composer-projection.tsx @@ -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( diff --git a/src/features/chat/panel/surface/message-stream-projection.ts b/src/features/chat/panel/surface/message-stream-projection.ts index 0e256b6e..a4ae069e 100644 --- a/src/features/chat/panel/surface/message-stream-projection.ts +++ b/src/features/chat/panel/surface/message-stream-projection.ts @@ -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 { - const byItemId = new Map(); +): ReadonlyMap { + const byItemId = new Map(); 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, itemId: string, patch: MessageStreamTextActions): void { +function patchTextActionTargets( + byItemId: Map, + itemId: string, + patch: MessageStreamTextActionTargets, +): void { byItemId.set(itemId, { ...byItemId.get(itemId), ...patch }); } diff --git a/src/features/chat/presentation/message-stream/layout.ts b/src/features/chat/presentation/message-stream/layout.ts index f567d197..024a3ad4 100644 --- a/src/features/chat/presentation/message-stream/layout.ts +++ b/src/features/chat/presentation/message-stream/layout.ts @@ -144,8 +144,8 @@ function isCompletedTurnDetailItem(classification: MessageStreamSemanticClassifi function turnOutcomeItemsByTurn(items: readonly MessageStreamSemanticClassification[]): Map { const turnOutcomeIdByTurn = new Map(); - 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; diff --git a/src/features/chat/presentation/message-stream/text-view.ts b/src/features/chat/presentation/message-stream/text-view.ts index 81780fed..67df133c 100644 --- a/src/features/chat/presentation/message-stream/text-view.ts +++ b/src/features/chat/presentation/message-stream/text-view.ts @@ -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), }; } diff --git a/src/features/chat/presentation/message-stream/view-model.ts b/src/features/chat/presentation/message-stream/view-model.ts index 9abfa143..aa6dfe7e 100644 --- a/src/features/chat/presentation/message-stream/view-model.ts +++ b/src/features/chat/presentation/message-stream/view-model.ts @@ -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 | undefined; - textActionsByItemId?: ReadonlyMap | undefined; + textActionTargetsByItemId?: ReadonlyMap | 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": diff --git a/src/features/chat/ui/message-stream/context.ts b/src/features/chat/ui/message-stream/context.ts index 97dd002c..7974b779 100644 --- a/src/features/chat/ui/message-stream/context.ts +++ b/src/features/chat/ui/message-stream/context.ts @@ -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; diff --git a/src/features/chat/ui/message-stream/text.tsx b/src/features/chat/ui/message-stream/text.tsx index 4c1fcd9c..884ef1f4 100644 --- a/src/features/chat/ui/message-stream/text.tsx +++ b/src/features/chat/ui/message-stream/text.tsx @@ -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(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 ? ( +
{view.roleLabel} - {forkActionsOpen && fork ? ( + {forkMenuOpen && fork ? ( { - context.onForkActionsToggle?.(null); + context.onForkMenuToggle?.(null); context.onFork?.(fork, true); }} /> @@ -84,15 +84,15 @@ function TextHeader({ view, context }: { view: MessageStreamTextView; context: T )} {fork ? ( { - if (forkActionsOpen) { - context.onForkActionsToggle?.(null); + if (forkMenuOpen) { + context.onForkMenuToggle?.(null); context.onFork?.(fork, false); } else { - context.onForkActionsToggle?.(view.id); + context.onForkMenuToggle?.(view.id); } }} /> diff --git a/tests/features/chat/conversation/turns/composer-submit-actions.test.ts b/tests/features/chat/conversation/turns/composer-submit-actions.test.ts index fddfe95f..1457b1c7 100644 --- a/tests/features/chat/conversation/turns/composer-submit-actions.test.ts +++ b/tests/features/chat/conversation/turns/composer-submit-actions.test.ts @@ -33,7 +33,7 @@ function createHost(draft: string) { }, setDraft, }, - slashCommands: { execute }, + slashCommandExecutor: { execute }, turnSubmission: { sendTurnText }, connection: { currentClient: () => client, diff --git a/tests/features/chat/conversation/turns/slash-command-handler.test.ts b/tests/features/chat/conversation/turns/slash-command-executor.test.ts similarity index 96% rename from tests/features/chat/conversation/turns/slash-command-handler.test.ts rename to tests/features/chat/conversation/turns/slash-command-executor.test.ts index d7ca6f53..f844471d 100644 --- a/tests/features/chat/conversation/turns/slash-command-handler.test.ts +++ b/tests/features/chat/conversation/turns/slash-command-executor.test.ts @@ -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; +type SlashCommandExecutorHostOverrides = Partial; -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)), diff --git a/tests/features/chat/message-stream/semantics.test.ts b/tests/features/chat/message-stream/semantics.test.ts index 8a2fd92d..ac41e29e 100644 --- a/tests/features/chat/message-stream/semantics.test.ts +++ b/tests/features/chat/message-stream/semantics.test.ts @@ -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 }, }); }); diff --git a/tests/features/chat/panel/shell.test.tsx b/tests/features/chat/panel/shell.test.tsx index 7e0954d5..d3c36acc 100644 --- a/tests/features/chat/panel/shell.test.tsx +++ b/tests/features/chat/panel/shell.test.tsx @@ -211,7 +211,7 @@ function shellParts(): ChatPanelShellParts { }), }, composer: { - controller: { + renderer: { renderState: (state) => ({ viewId: "view", draft: state.composer.draft, diff --git a/tests/features/chat/panel/surface/message-stream-presenter.test.ts b/tests/features/chat/panel/surface/message-stream-presenter.test.ts index d45537a0..adb17d2d 100644 --- a/tests/features/chat/panel/surface/message-stream-presenter.test.ts +++ b/tests/features/chat/panel/surface/message-stream-presenter.test.ts @@ -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(), diff --git a/tests/features/chat/panel/toolbar-archive-state.test.tsx b/tests/features/chat/panel/toolbar-archive-state.test.tsx index 189150b6..8a6ef47f 100644 --- a/tests/features/chat/panel/toolbar-archive-state.test.tsx +++ b/tests/features/chat/panel/toolbar-archive-state.test.tsx @@ -100,7 +100,7 @@ function shellParts(store: ReturnType, toolbarActio }), }, composer: { - controller: { + renderer: { renderState: () => ({ viewId: "view", draft: "", diff --git a/tests/features/chat/state-reducer.test.ts b/tests/features/chat/state-reducer.test.ts index 8108789b..fef46962 100644 --- a/tests/features/chat/state-reducer.test.ts +++ b/tests/features/chat/state-reducer.test.ts @@ -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" }); diff --git a/tests/features/chat/threads/auto-title-actions.test.ts b/tests/features/chat/threads/auto-title-coordinator.test.ts similarity index 86% rename from tests/features/chat/threads/auto-title-actions.test.ts rename to tests/features/chat/threads/auto-title-coordinator.test.ts index 4d9feb37..a9193e42 100644 --- a/tests/features/chat/threads/auto-title-actions.test.ts +++ b/tests/features/chat/threads/auto-title-coordinator.test.ts @@ -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(); 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(); 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; } = {}, -): AutoTitleActionsHost & { - actions: AutoTitleActions; +): AutoTitleCoordinatorHost & { + coordinator: AutoTitleCoordinator; notifyThreadRenamed: ReturnType; } { 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 } = {}): AppServerClient { diff --git a/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx b/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx index db8fbea4..82747abf 100644 --- a/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx +++ b/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx @@ -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(), diff --git a/tests/features/chat/ui/message-stream/pending-requests.test.tsx b/tests/features/chat/ui/message-stream/pending-requests.test.tsx index 3d75b541..4d512ced 100644 --- a/tests/features/chat/ui/message-stream/pending-requests.test.tsx +++ b/tests/features/chat/ui/message-stream/pending-requests.test.tsx @@ -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 }), }; diff --git a/tests/features/chat/ui/message-stream/stream-items.test.tsx b/tests/features/chat/ui/message-stream/stream-items.test.tsx index 5c3ae6f8..ac97c2d6 100644 --- a/tests/features/chat/ui/message-stream/stream-items.test.tsx +++ b/tests/features/chat/ui/message-stream/stream-items.test.tsx @@ -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 }), }); diff --git a/tests/features/chat/ui/message-stream/test-helpers.tsx b/tests/features/chat/ui/message-stream/test-helpers.tsx index 2921db35..0c16fad7 100644 --- a/tests/features/chat/ui/message-stream/test-helpers.tsx +++ b/tests/features/chat/ui/message-stream/test-helpers.tsx @@ -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> & { + Partial> & { 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; - textActionsByItemId?: ReadonlyMap; + textActionTargetsByItemId?: ReadonlyMap; }; 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, };