diff --git a/eslint.config.mjs b/eslint.config.mjs index b514b7dc..ba79f9d5 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -25,7 +25,7 @@ const removedChatStateEscapeHatchRestrictions = [ const chatSignalAdapterRestrictions = [ { selector: "ImportDeclaration[source.value='@preact/signals']", - message: "Keep chat signals in ui/shell-state.tsx as a reducer-to-Preact notification adapter.", + message: "Keep chat signals in panel/shell-state.tsx as a reducer-to-Preact notification adapter.", }, ]; const uiRootImportRestrictions = [ @@ -154,12 +154,12 @@ const chatPreactDomBridgeFiles = [ "src/features/chat/ui/message-stream/tool-result.tsx", "src/features/chat/ui/message-stream/viewport.tsx", "src/features/chat/ui/composer-dom.ts", - "src/features/chat/ui/shell.tsx", + "src/features/chat/panel/shell.tsx", "src/features/chat/ui/turn-diff/render.tsx", ]; const chatImperativeDomBridgeFiles = [...chatExternalDomBridgeFiles, ...chatPreactDomBridgeFiles]; const uiRootBridgeFiles = [ - "src/features/chat/ui/shell.tsx", + "src/features/chat/panel/shell.tsx", "src/features/chat/ui/turn-diff/render.tsx", "src/features/chat/ui/turn-diff/view.ts", "src/features/selection-rewrite/popover.tsx", @@ -639,7 +639,7 @@ export default defineConfig([ }, { files: ["src/features/chat/**/*.{ts,tsx}"], - ignores: ["src/features/chat/ui/shell-state.tsx", ...chatImperativeDomBridgeFiles], + ignores: ["src/features/chat/panel/shell-state.tsx", ...chatImperativeDomBridgeFiles], rules: { ...restrictedSyntaxRule(chatSourceSyntaxRestrictions), "codex-panel/no-imperative-dom": "error", @@ -647,7 +647,7 @@ export default defineConfig([ }, }, { - files: ["src/features/chat/ui/shell-state.tsx"], + files: ["src/features/chat/panel/shell-state.tsx"], rules: { ...restrictedSyntaxRule(baseSourceSyntaxRestrictions), "codex-panel/no-imperative-dom": "error", diff --git a/src/features/chat/application/state/reducer.ts b/src/features/chat/application/state/reducer.ts index ebcb97e7..60809759 100644 --- a/src/features/chat/application/state/reducer.ts +++ b/src/features/chat/application/state/reducer.ts @@ -76,7 +76,6 @@ export { chatTurnBusy, pendingTurnStart, transitionChatTurnLifecycleState, - type ChatTurnLifecycleState, type ChatTurnState, type PendingTurnStart, } from "../conversation/turn-state"; diff --git a/src/features/chat/host/composer.ts b/src/features/chat/host/composer.ts index 0390581d..7f7322fa 100644 --- a/src/features/chat/host/composer.ts +++ b/src/features/chat/host/composer.ts @@ -5,11 +5,11 @@ import { MessageStreamScrollBridge } from "../panel/surface/message-stream-scrol import { currentModel, runtimeConfigOrDefault } from "../domain/runtime/effective"; import { runtimeSnapshotForChatState } from "../application/runtime/snapshot"; import { activeTurnId, type ChatStateStore } from "../application/state/reducer"; -import type { ComposerMetaViewModel } from "../ui/composer"; -import type { ChatPanelComposerShellState } from "../ui/shell-state"; +import type { ChatPanelComposerShellState } from "../panel/shell-state"; import type { CodexChatHost } from "../application/chat-host"; import type { ChatRuntimeSettingsActions } from "../application/runtime/settings-actions"; -import { ChatComposerController } from "../ui/composer-controller"; +import { ChatComposerController } from "../panel/composer-controller"; +import type { ChatPanelComposerProjection } from "../panel/surface/model"; export interface ConversationComposerContext { app: App; @@ -17,10 +17,7 @@ export interface ConversationComposerContext { stateStore: ChatStateStore; viewId: string; surface: { - composerProjection: (state: ChatPanelComposerShellState) => { - placeholder: string; - meta: ComposerMetaViewModel; - }; + composerProjection: (state: ChatPanelComposerShellState) => ChatPanelComposerProjection; }; liveState: { refresh: () => void; diff --git a/src/features/chat/host/conversation.ts b/src/features/chat/host/conversation.ts index 427dc3b7..9594c269 100644 --- a/src/features/chat/host/conversation.ts +++ b/src/features/chat/host/conversation.ts @@ -10,11 +10,11 @@ import type { GoalActions } from "../application/threads/goal-actions"; import type { HistoryController } from "../application/threads/history-controller"; import type { ChatInboundController } from "../app-server/inbound/controller"; import type { MessageStreamItem, MessageStreamNoticeSection } from "../domain/message-stream/items"; -import type { ChatMessageScrollIntentState } from "../ui/message-stream/scroll-intent-state"; -import type { ComposerMetaViewModel } from "../ui/composer"; -import type { ChatPanelComposerShellState } from "../ui/shell-state"; +import type { ChatPanelComposerShellState } from "../panel/shell-state"; import type { CodexChatHost } from "../application/chat-host"; import { MessageStreamPresenter } from "../panel/surface/message-stream-presenter"; +import type { ChatMessageScrollIntentState } from "../panel/surface/message-stream-scroll-intent"; +import type { ChatPanelComposerProjection } from "../panel/surface/model"; import { createConversationComposer } from "./composer"; import { createConversationTurnActions } from "../application/conversation/composition"; @@ -33,10 +33,7 @@ interface ConversationPartsContext { }; surface: { pendingRequestsSignature: () => string; - composerProjection: (state: ChatPanelComposerShellState) => { - placeholder: string; - meta: ComposerMetaViewModel; - }; + composerProjection: (state: ChatPanelComposerShellState) => ChatPanelComposerProjection; }; runtime: { connectionDiagnosticDetails: () => MessageStreamNoticeSection[]; diff --git a/src/features/chat/host/session.ts b/src/features/chat/host/session.ts index 003c4aa0..f8ba9465 100644 --- a/src/features/chat/host/session.ts +++ b/src/features/chat/host/session.ts @@ -15,7 +15,7 @@ import { createChatReconnectActions } from "../application/connection/reconnect- import { createChatServerDiagnosticsActions, type ChatServerDiagnosticsActions } from "../app-server/actions/diagnostics"; import { createChatServerMetadataActions, type ChatServerMetadataActions } from "../app-server/actions/metadata"; import { createChatServerThreadActions, type ChatServerThreadActions } from "../app-server/actions/threads"; -import type { ChatComposerController } from "../ui/composer-controller"; +import type { ChatComposerController } from "../panel/composer-controller"; import { createConversationParts } from "./conversation"; import type { ComposerSubmitActions } from "../application/conversation/composer-submit-actions"; import { codexPanelDisplayTitle } from "../application/threads/title-display"; @@ -36,16 +36,16 @@ import { import { applyChatViewState } from "../panel/view-state"; import { closeChatView, openChatView, type ChatViewLifecycleHost } from "../panel/view-lifecycle"; import { createToolbarPanelActions, type ToolbarPanelActions } from "../panel/toolbar-actions"; -import { connectionDiagnosticsModel } from "../panel/surface/toolbar"; +import { connectionDiagnosticsModel } from "../panel/surface/toolbar-projection"; import { openPanelTurnLifecycle } from "../panel/snapshot"; import { ChatInboundController } from "../app-server/inbound/controller"; import { rejectServerRequest, respondToServerRequest } from "../app-server/requests/responder"; import { collaborationModeLabel as formatCollaborationModeLabel } from "../presentation/runtime/messages"; import { createChatRuntimeSettingsActions } from "../application/runtime/settings-actions"; import { runtimeSnapshotForChatState, type RuntimeSnapshot } from "../application/runtime/snapshot"; -import { chatPanelComposerProjection } from "../panel/surface/composer"; -import { createChatMessageScrollIntentState, type ChatMessageScrollIntentState } from "../ui/message-stream/scroll-intent-state"; -import { renderChatPanelShell } from "../ui/shell"; +import { chatPanelComposerProjection } from "../panel/surface/composer-projection"; +import { createChatMessageScrollIntentState, type ChatMessageScrollIntentState } from "../panel/surface/message-stream-scroll-intent"; +import { renderChatPanelShell } from "../panel/shell"; import { chatTurnBusy, createChatStateStore, diff --git a/src/features/chat/ui/composer-controller.ts b/src/features/chat/panel/composer-controller.ts similarity index 96% rename from src/features/chat/ui/composer-controller.ts rename to src/features/chat/panel/composer-controller.ts index f86adb03..937c3460 100644 --- a/src/features/chat/ui/composer-controller.ts +++ b/src/features/chat/panel/composer-controller.ts @@ -4,14 +4,11 @@ import type { CodexInput } from "../../../domain/chat/input"; import { isComposerSendKey, type SendShortcut } from "../../../shared/ui/keyboard"; import { textareaCursorAtVisualBoundary } from "../../../shared/ui/textarea-caret"; import { chatTurnBusy, type ChatAction, type ChatState, type ChatStateStore } from "../application/state/reducer"; -import type { ComposerMetaViewModel, ComposerShellProps } from "./composer"; -import { syncComposerHeight, type ComposerCallbacks } from "./composer"; +import type { ComposerShellProps } from "../ui/composer"; +import { syncComposerHeight, type ComposerCallbacks } from "../ui/composer"; import type { ChatPanelComposerShellState } from "./shell-state"; import { composerBoundaryScrollDirection, type ComposerBoundaryScrollAction } from "../application/composer/boundary-scroll"; -import { - noteCandidates as appNoteCandidates, - resolveWikiLinkMention as resolveAppWikiLinkMention, -} from "../application/composer/obsidian-context"; +import { noteCandidates as appNoteCandidates, resolveWikiLinkMention as resolveAppWikiLinkMention } from "./composer-obsidian-context"; import { activeComposerSuggestions, applyComposerSuggestionInsertion, @@ -22,6 +19,7 @@ import { type NoteCandidate, } from "../application/composer/suggestions"; import { userInputWithWikiLinkMentionsAndSkills } from "../application/composer/wikilink-context"; +import type { ChatPanelComposerProjection } from "./surface/model"; export interface ChatComposerControllerOptions { app: App; @@ -30,10 +28,7 @@ export interface ChatComposerControllerOptions { sendShortcut: () => SendShortcut; scrollThreadFromComposerEdges: () => boolean; canInterrupt: (state: ChatPanelComposerShellState) => boolean; - composerProjection: (state: ChatPanelComposerShellState) => { - placeholder: string; - meta: ComposerMetaViewModel; - }; + composerProjection: (state: ChatPanelComposerShellState) => ChatPanelComposerProjection; currentModelForSuggestions: () => string | null; threadScrollFromComposer: (action: ComposerBoundaryScrollAction) => void; togglePlan: () => void; diff --git a/src/features/chat/application/composer/obsidian-context.ts b/src/features/chat/panel/composer-obsidian-context.ts similarity index 96% rename from src/features/chat/application/composer/obsidian-context.ts rename to src/features/chat/panel/composer-obsidian-context.ts index 11d2ebec..34054e87 100644 --- a/src/features/chat/application/composer/obsidian-context.ts +++ b/src/features/chat/panel/composer-obsidian-context.ts @@ -1,7 +1,7 @@ import type { App } from "obsidian"; import { stripHeadingForLink, TFile } from "obsidian"; -import type { NoteCandidate } from "./suggestions"; +import type { NoteCandidate } from "../application/composer/suggestions"; export interface WikiLinkMention { name: string; diff --git a/src/features/chat/ui/shell-state.tsx b/src/features/chat/panel/shell-state.tsx similarity index 100% rename from src/features/chat/ui/shell-state.tsx rename to src/features/chat/panel/shell-state.tsx diff --git a/src/features/chat/ui/shell.tsx b/src/features/chat/panel/shell.tsx similarity index 96% rename from src/features/chat/ui/shell.tsx rename to src/features/chat/panel/shell.tsx index 9f149e1d..7ebaf99b 100644 --- a/src/features/chat/ui/shell.tsx +++ b/src/features/chat/panel/shell.tsx @@ -1,11 +1,11 @@ import type { ComponentChild as UiNode } from "preact"; import { renderUiRoot, unmountUiRoot } from "../../../shared/ui/ui-root"; import type { ChatStateStore } from "../application/state/reducer"; -import type { ChatPanelGoalSurface, ChatPanelToolbarSurface } from "../panel/surface/model"; -import { ChatPanelToolbar } from "../panel/surface/toolbar"; -import { ChatPanelGoal } from "../panel/surface/goal"; -import { ChatPanelMessageStream, type ChatPanelMessageStreamPresenter } from "../panel/surface/message-stream-presenter"; -import { ChatPanelComposer, type ChatPanelComposerActions, type ChatPanelComposerController } from "../panel/surface/composer"; +import type { ChatPanelGoalSurface, ChatPanelToolbarSurface } from "./surface/model"; +import { ChatPanelToolbar } from "./surface/toolbar-projection"; +import { ChatPanelGoal } from "./surface/goal-projection"; +import { ChatPanelMessageStream, type ChatPanelMessageStreamPresenter } from "./surface/message-stream-presenter"; +import { ChatPanelComposer, type ChatPanelComposerActions, type ChatPanelComposerController } from "./surface/composer-projection"; import { ChatPanelShellStateContext, createChatPanelShellState, syncChatPanelShellState, type ChatPanelShellState } from "./shell-state"; export interface ChatPanelShellParts { diff --git a/src/features/chat/panel/surface/composer.ts b/src/features/chat/panel/surface/composer-projection.tsx similarity index 87% rename from src/features/chat/panel/surface/composer.ts rename to src/features/chat/panel/surface/composer-projection.tsx index 8845f887..512685ea 100644 --- a/src/features/chat/panel/surface/composer.ts +++ b/src/features/chat/panel/surface/composer-projection.tsx @@ -15,28 +15,22 @@ import { sortedModelMetadata } from "../../../../domain/catalog/metadata"; import type { ReasoningEffort } from "../../../../domain/catalog/metadata"; import type { RuntimeSnapshot } from "../../application/runtime/snapshot"; import type { ChatState } from "../../application/state/reducer"; -import type { - ComposerContextMeterCellViewModel, - ComposerContextMeterViewModel, - ComposerMetaViewModel, - RuntimeChoice, -} from "../../ui/composer"; import { ComposerShell, type ComposerShellProps } from "../../ui/composer"; -import { composerStateFromShellState, useChatPanelShellState, type ChatPanelComposerShellState } from "../../ui/shell-state"; +import { composerStateFromShellState, useChatPanelShellState, type ChatPanelComposerShellState } from "../shell-state"; import { explicitThreadName } from "../../../../domain/threads/model"; -import type { ChatPanelComposerSurface, RestoredThreadTitleSnapshot } from "./model"; +import type { + ChatPanelComposerContextMeter, + ChatPanelComposerContextMeterCell, + ChatPanelComposerMeta, + ChatPanelComposerProjection, + ChatPanelComposerRuntimeChoice, + ChatPanelComposerSurface, + RestoredThreadTitleSnapshot, +} from "./model"; import { runtimeSnapshotForShellState } from "./runtime-snapshot"; type ComposerMetaState = Pick; -export interface ChatPanelComposerProjection { - placeholder: string; - meta: ComposerMetaViewModel & { - modelChoices: RuntimeChoice[]; - effortChoices: RuntimeChoice[]; - }; -} - export interface ChatPanelComposerController { renderState(state: ChatPanelComposerShellState, actions: ChatPanelComposerActions): ComposerShellProps; } @@ -88,7 +82,10 @@ export function chatPanelComposerProjection( }; } -export function composerMetaViewModel(state: ComposerMetaState, snapshot: RuntimeSnapshot): ComposerMetaViewModel { +export function composerMetaViewModel( + state: ComposerMetaState, + snapshot: RuntimeSnapshot, +): Omit { if (state.connection.phase.kind === "failed" || state.connection.phase.kind === "disconnected") { return { fatal: "Codex app-server disconnected", @@ -131,13 +128,13 @@ export function composerMetaViewModel(state: ComposerMetaState, snapshot: Runtim } export function runtimeComposerChoices(input: RuntimeComposerChoicesInput): { - modelChoices: RuntimeChoice[]; - effortChoices: RuntimeChoice[]; + modelChoices: ChatPanelComposerRuntimeChoice[]; + effortChoices: ChatPanelComposerRuntimeChoice[]; } { const config = runtimeConfigOrDefault(input.state.connection.runtimeConfig); const activeModel = currentModel(input.snapshot, config); const models = sortedModelMetadata(input.state.connection.availableModels); - const modelChoices: RuntimeChoice[] = models.slice(0, 12).map((model) => ({ + const modelChoices: ChatPanelComposerRuntimeChoice[] = models.slice(0, 12).map((model) => ({ label: model.model, selected: activeModel === model.model, onClick: () => { @@ -153,7 +150,7 @@ export function runtimeComposerChoices(input: RuntimeComposerChoicesInput): { } const activeEffort = currentReasoningEffort(input.snapshot, config); - const effortChoices: RuntimeChoice[] = [ + const effortChoices: ChatPanelComposerRuntimeChoice[] = [ { label: "Codex default", selected: activeEffort === null, @@ -174,7 +171,7 @@ export function runtimeComposerChoices(input: RuntimeComposerChoicesInput): { } function composerStatusSummary(input: { - context: ComposerContextMeterViewModel; + context: ChatPanelComposerContextMeter; model: string; effort: string | null; planActive: boolean; @@ -214,7 +211,7 @@ const CONTEXT_PARTIAL_DOTS = ["", "⣀", "⣤", "⣶", "⣿"] as const; const CONTEXT_FULL_DOT = "⣿"; const CONTEXT_EMPTY_DOT = "⣀"; -function contextComposerMeter(percent: number | null): ComposerContextMeterViewModel { +function contextComposerMeter(percent: number | null): ChatPanelComposerContextMeter { const percentLabel = percent === null ? "--%" : `${String(Math.round(Math.max(0, Math.min(100, percent)))).padStart(2, " ")}%`; return { cells: contextBrailleCells(percent), @@ -222,10 +219,10 @@ function contextComposerMeter(percent: number | null): ComposerContextMeterViewM }; } -function contextBrailleCells(percent: number | null): ComposerContextMeterCellViewModel[] { +function contextBrailleCells(percent: number | null): ChatPanelComposerContextMeterCell[] { if (percent === null) return Array.from({ length: CONTEXT_DOT_WIDTH }, () => ({ text: CONTEXT_EMPTY_DOT, placeholder: true })); const clamped = Math.max(0, Math.min(100, percent)); - const cells: ComposerContextMeterCellViewModel[] = []; + const cells: ChatPanelComposerContextMeterCell[] = []; for (let index = 0; index < CONTEXT_DOT_WIDTH; index += 1) { const remaining = clamped - index * CONTEXT_CELL_PERCENT; if (remaining <= 0) { diff --git a/src/features/chat/panel/surface/goal.ts b/src/features/chat/panel/surface/goal-projection.tsx similarity index 98% rename from src/features/chat/panel/surface/goal.ts rename to src/features/chat/panel/surface/goal-projection.tsx index d8154d9e..1604e063 100644 --- a/src/features/chat/panel/surface/goal.ts +++ b/src/features/chat/panel/surface/goal-projection.tsx @@ -3,7 +3,7 @@ import { h } from "preact"; import type { GoalPanelActions, GoalPanelDisplayState, GoalPanelEditorState, GoalPanelOptions } from "../../ui/goal"; import { GoalPanel } from "../../ui/goal"; -import { goalStateFromShellState, useChatPanelShellState, type ChatPanelGoalShellState } from "../../ui/shell-state"; +import { goalStateFromShellState, useChatPanelShellState, type ChatPanelGoalShellState } from "../shell-state"; import type { ChatPanelGoalSurface } from "./model"; export interface ChatPanelGoalProjection { diff --git a/src/features/chat/panel/surface/message-stream-presenter.ts b/src/features/chat/panel/surface/message-stream-presenter.ts index 13e32c23..c8dcc43f 100644 --- a/src/features/chat/panel/surface/message-stream-presenter.ts +++ b/src/features/chat/panel/surface/message-stream-presenter.ts @@ -8,7 +8,7 @@ import { MarkdownMessageRenderer } from "../../ui/message-stream/markdown-render import { MessageStreamViewport, type MessageStreamViewportState } from "../../ui/message-stream/viewport"; import type { MessageStreamItem } from "../../domain/message-stream/items"; import { messageStreamBlocks } from "../../ui/message-stream/stream-blocks"; -import { messageStreamStateFromShellState, useChatPanelShellState, type ChatPanelMessageStreamShellState } from "../../ui/shell-state"; +import { messageStreamStateFromShellState, useChatPanelShellState, type ChatPanelMessageStreamShellState } from "../shell-state"; import type { PendingRequestBlockActions, PendingRequestBlockState } from "../../application/pending-requests/block"; import type { ChatTurnDiffViewState } from "../../domain/turn-diff"; import { diff --git a/src/features/chat/panel/surface/message-stream-projection.ts b/src/features/chat/panel/surface/message-stream-projection.ts index b9eb1979..35c64c20 100644 --- a/src/features/chat/panel/surface/message-stream-projection.ts +++ b/src/features/chat/panel/surface/message-stream-projection.ts @@ -22,7 +22,7 @@ import { type MessageStreamRollbackCandidate, } from "../../application/state/message-stream"; import type { MessageStreamContext } from "../../ui/message-stream/context"; -import type { ChatPanelMessageStreamShellState } from "../../ui/shell-state"; +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"; diff --git a/src/features/chat/ui/message-stream/scroll-intent-state.ts b/src/features/chat/panel/surface/message-stream-scroll-intent.ts similarity index 88% rename from src/features/chat/ui/message-stream/scroll-intent-state.ts rename to src/features/chat/panel/surface/message-stream-scroll-intent.ts index e18666d1..4549109f 100644 --- a/src/features/chat/ui/message-stream/scroll-intent-state.ts +++ b/src/features/chat/panel/surface/message-stream-scroll-intent.ts @@ -1,4 +1,4 @@ -import type { MessageStreamScrollIntent } from "./virtualizer"; +import type { MessageStreamScrollIntent } from "../../ui/message-stream/virtualizer"; export interface ChatMessageScrollIntentState { consumeIntent(): MessageStreamScrollIntent; diff --git a/src/features/chat/panel/surface/model.ts b/src/features/chat/panel/surface/model.ts index ddddf731..4b746655 100644 --- a/src/features/chat/panel/surface/model.ts +++ b/src/features/chat/panel/surface/model.ts @@ -1,6 +1,5 @@ import type { ReasoningEffort } from "../../../../domain/catalog/metadata"; import type { SendShortcut } from "../../../../shared/ui/keyboard"; -import type { ToolbarActions } from "../../ui/toolbar"; export interface RestoredThreadTitleSnapshot { threadId: string; @@ -8,7 +7,60 @@ export interface RestoredThreadTitleSnapshot { explicitName: string | null; } -type ChatPanelToolbarActions = ToolbarActions; +export interface ChatPanelComposerContextMeterCell { + text: string; + placeholder: boolean; +} + +export interface ChatPanelComposerContextMeter { + cells: ChatPanelComposerContextMeterCell[]; + percent: string; +} + +export interface ChatPanelComposerRuntimeChoice { + label: string; + selected?: boolean; + disabled?: boolean; + meta?: string; + onClick: () => void; +} + +export interface ChatPanelComposerMeta { + fatal: string | null; + context: ChatPanelComposerContextMeter; + statusSummary: string; + model: string; + effort: string | null; + planActive: boolean; + autoReviewActive: boolean; + fastActive: boolean; + modelChoices?: ChatPanelComposerRuntimeChoice[]; + effortChoices?: ChatPanelComposerRuntimeChoice[]; +} + +export interface ChatPanelComposerProjection { + placeholder: string; + meta: ChatPanelComposerMeta; +} + +interface ChatPanelToolbarActions { + startNewThread: () => void; + toggleChatActions: () => void; + compactConversation: () => void; + setGoal: () => void; + toggleHistory: () => void; + toggleStatusPanel: () => void; + connect: () => void; + refreshStatus: () => void; + resumeThread: (threadId: string) => void; + startArchiveThread: (threadId: string) => void; + archiveThread: (threadId: string, saveMarkdown: boolean) => void; + startRenameThread: (threadId: string) => void; + updateRenameDraft: (threadId: string, value: string) => void; + saveRenameThread: (threadId: string, value: string) => void; + cancelRenameThread: (threadId: string) => void; + autoNameThread: (threadId: string) => void; +} interface ChatPanelGoalActions { saveObjective: (objective: string, tokenBudget: number | null) => Promise; diff --git a/src/features/chat/panel/surface/toolbar.ts b/src/features/chat/panel/surface/toolbar-projection.tsx similarity index 98% rename from src/features/chat/panel/surface/toolbar.ts rename to src/features/chat/panel/surface/toolbar-projection.tsx index 49cc539b..b4394b22 100644 --- a/src/features/chat/panel/surface/toolbar.ts +++ b/src/features/chat/panel/surface/toolbar-projection.tsx @@ -7,7 +7,7 @@ import { runtimeConfigSections, rateLimitSummary } from "../../presentation/runt import { connectionDiagnosticSections } from "../../application/connection/diagnostics-display"; import type { RuntimeSnapshot } from "../../application/runtime/snapshot"; import { chatTurnBusy, type ChatState } from "../../application/state/reducer"; -import { toolbarStateFromShellState, useChatPanelShellState, type ChatPanelToolbarShellState } from "../../ui/shell-state"; +import { toolbarStateFromShellState, useChatPanelShellState, type ChatPanelToolbarShellState } from "../shell-state"; import { Toolbar, type ToolbarThreadRow, type ToolbarViewModel } from "../../ui/toolbar"; import type { ChatPanelToolbarSurface } from "./model"; import { runtimeSnapshotForToolbarShellState } from "./runtime-snapshot"; diff --git a/src/features/chat/panel/view-lifecycle.ts b/src/features/chat/panel/view-lifecycle.ts index 703ff9e2..b328040d 100644 --- a/src/features/chat/panel/view-lifecycle.ts +++ b/src/features/chat/panel/view-lifecycle.ts @@ -1,6 +1,6 @@ import type { EventRef } from "obsidian"; -import { unmountChatPanelShell } from "../ui/shell"; +import { unmountChatPanelShell } from "./shell"; export interface ChatViewLifecycleHost { lifecycle: { diff --git a/src/features/chat/ui/composer.tsx b/src/features/chat/ui/composer.tsx index 4f7dcebb..075c8cd6 100644 --- a/src/features/chat/ui/composer.tsx +++ b/src/features/chat/ui/composer.tsx @@ -1,12 +1,21 @@ import type { ButtonHTMLAttributes, ComponentChild as UiNode, Ref } from "preact"; import { useLayoutEffect, useRef, useState } from "preact/hooks"; -import type { ComposerSuggestion } from "../application/composer/suggestions"; import { IconButton } from "../../../shared/ui/components"; import { disposeDomListeners, listenDomEvent } from "../../../shared/ui/dom-events"; import { syncTextareaHeight } from "../../../shared/ui/textarea-autogrow"; import { renderComposerMetaIcon, scrollComposerSuggestionIntoView, updateComposerMetaStatusOverflow } from "./composer-dom"; +export interface ComposerSuggestion { + display: string; + detail: string; + replacement: string; + start: number; + appendSpaceOnInsert?: boolean; + tabCursorOffset?: number; + suffixOnInsert?: string; +} + export interface ComposerMetaViewModel { fatal: string | null; context: ComposerContextMeterViewModel; @@ -20,7 +29,7 @@ export interface ComposerMetaViewModel { effortChoices?: RuntimeChoice[]; } -export interface RuntimeChoice { +interface RuntimeChoice { label: string; selected?: boolean; disabled?: boolean; @@ -28,12 +37,12 @@ export interface RuntimeChoice { onClick: () => void; } -export interface ComposerContextMeterCellViewModel { +interface ComposerContextMeterCellViewModel { text: string; placeholder: boolean; } -export interface ComposerContextMeterViewModel { +interface ComposerContextMeterViewModel { cells: ComposerContextMeterCellViewModel[]; percent: string; } diff --git a/src/features/chat/ui/message-stream/context.ts b/src/features/chat/ui/message-stream/context.ts index 0d780b76..41c01420 100644 --- a/src/features/chat/ui/message-stream/context.ts +++ b/src/features/chat/ui/message-stream/context.ts @@ -1,7 +1,6 @@ import type { ComponentChild as UiNode } from "preact"; -import type { ChatDisclosureBucket, ChatDisclosureUiState, ChatTurnLifecycleState } from "../../application/state/reducer"; -import type { PendingRequestBlockActions } from "../../application/pending-requests/block"; +import type { ApprovalAction, PendingRequestId } from "../../domain/pending-requests/model"; import type { PendingRequestBlockSnapshot } from "../../presentation/pending-requests/snapshot"; import type { MessageStreamItem } from "../../domain/message-stream/items"; import type { TextMessageStreamItem } from "../../presentation/message-stream/text-view"; @@ -14,9 +13,41 @@ export interface MessageStreamBlock { export type { TextMessageStreamItem }; +type MessageStreamDisclosureBucket = + | "toolResults" + | "activityGroups" + | "agentDetails" + | "textDetails" + | "userMessageExpanded" + | "goalObjectiveExpanded" + | "approvalDetails"; + +export interface MessageStreamDisclosureState { + toolResults: ReadonlySet; + activityGroups: ReadonlySet; + agentDetails: ReadonlySet; + textDetails: ReadonlySet; + userMessageExpanded: ReadonlySet; + goalObjectiveExpanded: ReadonlySet; + approvalDetails: ReadonlySet; +} + +export type MessageStreamTurnLifecycleState = + | { kind: "idle" } + | { kind: "starting"; pendingTurnStart: unknown } + | { kind: "running"; turnId: string }; + +export interface PendingRequestBlockActions { + resolveApproval: (requestId: PendingRequestId, action: ApprovalAction) => void; + resolveUserInput: (requestId: PendingRequestId) => void; + cancelUserInput: (requestId: PendingRequestId) => void; + setApprovalDetailsExpanded?: (requestId: PendingRequestId, expanded: boolean) => void; + setUserInputDraft: (key: string, value: string) => void; +} + export interface TextItemDetailStateContext { - disclosures: ChatDisclosureUiState; - onDisclosureToggle?: (bucket: ChatDisclosureBucket, id: string, open: boolean) => void; + disclosures: MessageStreamDisclosureState; + onDisclosureToggle?: (bucket: MessageStreamDisclosureBucket, id: string, open: boolean) => void; } export interface TextItemContentContext extends TextItemDetailStateContext { @@ -24,7 +55,7 @@ export interface TextItemContentContext extends TextItemDetailStateContext { } export interface TextItemActionContext extends TextItemDetailStateContext { - turnLifecycle: ChatTurnLifecycleState; + turnLifecycle: MessageStreamTurnLifecycleState; forkActionsItemId: string | null; onForkActionsToggle?: (itemId: string | null) => void; copyText?: (text: string) => void; @@ -44,7 +75,7 @@ export interface TextItemMetadataContext extends TextItemDetailStateContext { interface MessageStreamRenderContext { activeThreadId: string | null; - turnLifecycle: ChatTurnLifecycleState; + turnLifecycle: MessageStreamTurnLifecycleState; workspaceRoot?: string | null; loadOlderTurns: () => void; pendingRequests?: PendingRequestBlockContext; diff --git a/src/features/chat/ui/message-stream/pending-request-block.tsx b/src/features/chat/ui/message-stream/pending-request-block.tsx index b7667c68..d0d69d8c 100644 --- a/src/features/chat/ui/message-stream/pending-request-block.tsx +++ b/src/features/chat/ui/message-stream/pending-request-block.tsx @@ -6,7 +6,7 @@ import { type PendingUserInputQuestionViewModel, type PendingUserInputViewModel, } from "../../presentation/pending-requests/view-model"; -import type { PendingRequestBlockActions } from "../../application/pending-requests/block"; +import type { PendingRequestBlockActions } from "./context"; import { createWorkMessageClassName } from "./work-message"; export function pendingRequestBlockNode( diff --git a/src/features/chat/ui/message-stream/text-item-actions.tsx b/src/features/chat/ui/message-stream/text-item-actions.tsx index 9b7a5e02..4322249b 100644 --- a/src/features/chat/ui/message-stream/text-item-actions.tsx +++ b/src/features/chat/ui/message-stream/text-item-actions.tsx @@ -1,7 +1,6 @@ import { type ComponentChild as UiNode } from "preact"; import { useEffect, useRef } from "preact/hooks"; -import { activeTurnId } from "../../application/state/reducer"; import type { MessageStreamItem } from "../../domain/message-stream/items"; import { IconButton } from "../../../../shared/ui/components"; import { listenDomEvent } from "../../../../shared/ui/dom-events"; @@ -117,6 +116,6 @@ function displayRoleLabel(item: MessageStreamItem): string { function isMessageCopyActionVisible(item: MessageStreamItem, context: Pick): boolean { if (item.kind !== "message" || item.copyText === undefined) return false; - const activeTurn = activeTurnId({ lifecycle: context.turnLifecycle }); + const activeTurn = context.turnLifecycle.kind === "running" ? context.turnLifecycle.turnId : null; return !(activeTurn && item.role === "assistant" && item.turnId === activeTurn); } diff --git a/src/features/chat/ui/message-stream/tool-result.tsx b/src/features/chat/ui/message-stream/tool-result.tsx index 509e20f7..e38aeac0 100644 --- a/src/features/chat/ui/message-stream/tool-result.tsx +++ b/src/features/chat/ui/message-stream/tool-result.tsx @@ -3,10 +3,10 @@ import { useLayoutEffect, useRef } from "preact/hooks"; import { type ToolResultDetailSection, type ToolResultView } from "../../presentation/message-stream/tool-result-view"; import { renderRawDiffLines } from "../../../../shared/diff/render"; -import type { ChatDisclosureUiState } from "../../application/state/reducer"; +import type { MessageStreamDisclosureState } from "./context"; export interface ToolResultRenderContext { - disclosures: ChatDisclosureUiState; + disclosures: MessageStreamDisclosureState; onDisclosureToggle?: (bucket: "toolResults", id: string, open: boolean) => void; } diff --git a/src/features/chat/ui/message-stream/work-items.tsx b/src/features/chat/ui/message-stream/work-items.tsx index b111e929..0ab72bb1 100644 --- a/src/features/chat/ui/message-stream/work-items.tsx +++ b/src/features/chat/ui/message-stream/work-items.tsx @@ -2,11 +2,11 @@ import type { ComponentChild as UiNode } from "preact"; import type { ExecutionState, TaskProgressMessageStreamItem } from "../../domain/message-stream/items"; import type { AgentRunSummaryView, MessageStreamWorkView } from "../../presentation/message-stream/work-view"; -import type { ChatDisclosureUiState } from "../../application/state/reducer"; +import type { MessageStreamDisclosureState } from "./context"; import { createWorkMessageClassName } from "./work-message"; export interface WorkItemContext { - disclosures: ChatDisclosureUiState; + disclosures: MessageStreamDisclosureState; onDisclosureToggle?: (bucket: "agentDetails", id: string, open: boolean) => void; } diff --git a/tests/features/chat/conversation/composer/controller.test.ts b/tests/features/chat/conversation/composer/controller.test.ts index b9d4a323..0bc3b159 100644 --- a/tests/features/chat/conversation/composer/controller.test.ts +++ b/tests/features/chat/conversation/composer/controller.test.ts @@ -4,13 +4,13 @@ import type { App } from "obsidian"; import { describe, expect, it, vi } from "vitest"; import { h } from "preact"; -import { ChatComposerController, type ChatComposerRenderActions } from "../../../../../src/features/chat/ui/composer-controller"; +import { ChatComposerController, type ChatComposerRenderActions } from "../../../../../src/features/chat/panel/composer-controller"; import { ComposerShell } from "../../../../../src/features/chat/ui/composer"; import { createChatStateStore, type ChatStateStore } from "../../../../../src/features/chat/application/state/reducer"; import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/ui/ui-root"; import type { SkillMetadata } from "../../../../../src/domain/catalog/metadata"; import { installObsidianDomShims } from "../../../../support/dom"; -import type { ChatPanelComposerShellState } from "../../../../../src/features/chat/ui/shell-state"; +import type { ChatPanelComposerShellState } from "../../../../../src/features/chat/panel/shell-state"; installObsidianDomShims(); diff --git a/tests/features/chat/conversation/composer/obsidian-context.test.ts b/tests/features/chat/panel/composer-obsidian-context.test.ts similarity index 99% rename from tests/features/chat/conversation/composer/obsidian-context.test.ts rename to tests/features/chat/panel/composer-obsidian-context.test.ts index 0668afef..420fa7b1 100644 --- a/tests/features/chat/conversation/composer/obsidian-context.test.ts +++ b/tests/features/chat/panel/composer-obsidian-context.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { TFile, type App } from "obsidian"; -import { noteCandidates, resolveWikiLinkMention } from "../../../../../src/features/chat/application/composer/obsidian-context"; +import { noteCandidates, resolveWikiLinkMention } from "../../../../src/features/chat/panel/composer-obsidian-context"; describe("Obsidian composer context", () => { it("builds note candidates from markdown files", () => { diff --git a/tests/features/chat/ui/shell.test.tsx b/tests/features/chat/panel/shell.test.tsx similarity index 99% rename from tests/features/chat/ui/shell.test.tsx rename to tests/features/chat/panel/shell.test.tsx index 35d86faa..2f2f06f0 100644 --- a/tests/features/chat/ui/shell.test.tsx +++ b/tests/features/chat/panel/shell.test.tsx @@ -5,8 +5,8 @@ import { act } from "preact/test-utils"; import { createChatStateStore } from "../../../../src/features/chat/application/state/reducer"; import { messageStreamItems } from "../../../../src/features/chat/application/state/message-stream"; -import { renderChatPanelShell, unmountChatPanelShell, type ChatPanelShellParts } from "../../../../src/features/chat/ui/shell"; -import type { ChatPanelComposerShellState, ChatPanelMessageStreamShellState } from "../../../../src/features/chat/ui/shell-state"; +import { renderChatPanelShell, unmountChatPanelShell, type ChatPanelShellParts } from "../../../../src/features/chat/panel/shell"; +import type { ChatPanelComposerShellState, ChatPanelMessageStreamShellState } from "../../../../src/features/chat/panel/shell-state"; import type { ChatPanelSurface } from "../../../../src/features/chat/panel/surface/model"; import { installObsidianDomShims } from "../../../support/dom"; diff --git a/tests/features/chat/ui/message-stream/scroll-intent-state.test.ts b/tests/features/chat/panel/surface/message-stream-scroll-intent.test.ts similarity index 66% rename from tests/features/chat/ui/message-stream/scroll-intent-state.test.ts rename to tests/features/chat/panel/surface/message-stream-scroll-intent.test.ts index 021a6c43..198064f2 100644 --- a/tests/features/chat/ui/message-stream/scroll-intent-state.test.ts +++ b/tests/features/chat/panel/surface/message-stream-scroll-intent.test.ts @@ -1,33 +1,30 @@ import { describe, expect, it } from "vitest"; -import { createChatMessageScrollIntentState } from "../../../../../src/features/chat/ui/message-stream/scroll-intent-state"; +import { createChatMessageScrollIntentState } from "../../../../../src/features/chat/panel/surface/message-stream-scroll-intent"; describe("createChatMessageScrollIntentState", () => { - it("consumes one-shot scroll intents", () => { + it("starts with automatic scrolling", () => { const scrollIntent = createChatMessageScrollIntentState(); + expect(scrollIntent.consumeIntent()).toBe("auto"); + expect(scrollIntent.consumeIntent()).toBe("auto"); + }); + + it("consumes force bottom intent once", () => { + const scrollIntent = createChatMessageScrollIntentState(); + + scrollIntent.forceBottom(); + + expect(scrollIntent.consumeIntent()).toBe("force-bottom"); + expect(scrollIntent.consumeIntent()).toBe("auto"); + }); + + it("keeps the latest requested intent", () => { + const scrollIntent = createChatMessageScrollIntentState(); + + scrollIntent.followBottom(); scrollIntent.preservePosition(); expect(scrollIntent.consumeIntent()).toBe("preserve"); - expect(scrollIntent.consumeIntent()).toBe("auto"); - }); - - it("emits a one-shot force-bottom intent", () => { - const scrollIntent = createChatMessageScrollIntentState(); - - scrollIntent.forceBottom(); - - expect(scrollIntent.consumeIntent()).toBe("force-bottom"); - }); - - it("uses the latest scroll intent before consumption", () => { - const scrollIntent = createChatMessageScrollIntentState(); - - scrollIntent.preservePosition(); - scrollIntent.followBottom(); - scrollIntent.forceBottom(); - - expect(scrollIntent.consumeIntent()).toBe("force-bottom"); - expect(scrollIntent.consumeIntent()).toBe("auto"); }); }); diff --git a/tests/features/chat/view-model.test.ts b/tests/features/chat/panel/surface/projection.test.ts similarity index 91% rename from tests/features/chat/view-model.test.ts rename to tests/features/chat/panel/surface/projection.test.ts index 666af814..c51c8ee3 100644 --- a/tests/features/chat/view-model.test.ts +++ b/tests/features/chat/panel/surface/projection.test.ts @@ -1,26 +1,30 @@ import { describe, expect, it } from "vitest"; -import { createServerDiagnostics } from "../../../src/domain/server/diagnostics"; +import { createServerDiagnostics } from "../../../../../src/domain/server/diagnostics"; import { runtimeConfigSnapshotFromAppServerConfig, type ConfigReadResult, type RuntimeConfigSnapshot, -} from "../../../src/app-server/protocol/runtime-config"; -import { createChatState } from "../../../src/features/chat/application/state/reducer"; -import { chatPanelComposerProjection, composerMetaViewModel, composerPlaceholder } from "../../../src/features/chat/panel/surface/composer"; -import { effortStatusLines, modelStatusLines, statusSummaryLines } from "../../../src/features/chat/presentation/runtime/status"; -import { runtimeComposerChoices } from "../../../src/features/chat/panel/surface/composer"; -import { runtimeSnapshotForChatState } from "../../../src/features/chat/application/runtime/snapshot"; -import { chatPanelToolbarProjection, toolbarStateProjection } from "../../../src/features/chat/panel/surface/toolbar"; -import type { ChatState } from "../../../src/features/chat/application/state/reducer"; -import type { ModelMetadata } from "../../../src/domain/catalog/metadata"; -import type { Thread } from "../../../src/domain/threads/model"; -import { chatPanelGoalProjection, chatPanelGoalViewModel } from "../../../src/features/chat/panel/surface/goal"; -import type { ChatPanelComposerSurface, ChatPanelGoalSurface } from "../../../src/features/chat/panel/surface/model"; -import type { ThreadGoal } from "../../../src/domain/threads/goal"; -import { setChatStateMessageStreamItems } from "./support/message-stream"; +} from "../../../../../src/app-server/protocol/runtime-config"; +import { createChatState } from "../../../../../src/features/chat/application/state/reducer"; +import { + chatPanelComposerProjection, + composerMetaViewModel, + composerPlaceholder, +} from "../../../../../src/features/chat/panel/surface/composer-projection"; +import { effortStatusLines, modelStatusLines, statusSummaryLines } from "../../../../../src/features/chat/presentation/runtime/status"; +import { runtimeComposerChoices } from "../../../../../src/features/chat/panel/surface/composer-projection"; +import { runtimeSnapshotForChatState } from "../../../../../src/features/chat/application/runtime/snapshot"; +import { chatPanelToolbarProjection, toolbarStateProjection } from "../../../../../src/features/chat/panel/surface/toolbar-projection"; +import type { ChatState } from "../../../../../src/features/chat/application/state/reducer"; +import type { ModelMetadata } from "../../../../../src/domain/catalog/metadata"; +import type { Thread } from "../../../../../src/domain/threads/model"; +import { chatPanelGoalProjection, chatPanelGoalViewModel } from "../../../../../src/features/chat/panel/surface/goal-projection"; +import type { ChatPanelComposerSurface, ChatPanelGoalSurface } from "../../../../../src/features/chat/panel/surface/model"; +import type { ThreadGoal } from "../../../../../src/domain/threads/goal"; +import { setChatStateMessageStreamItems } from "../../support/message-stream"; -describe("chat view model", () => { +describe("chat panel surface projections", () => { it("builds toolbar rows from immutable chat state snapshots", () => { const state = createChatState(); state.activeThread.id = "thread-1"; diff --git a/tests/features/chat/panel/surface/toolbar.test.ts b/tests/features/chat/panel/toolbar-actions.test.ts similarity index 84% rename from tests/features/chat/panel/surface/toolbar.test.ts rename to tests/features/chat/panel/toolbar-actions.test.ts index 95e706b9..0d77dc2b 100644 --- a/tests/features/chat/panel/surface/toolbar.test.ts +++ b/tests/features/chat/panel/toolbar-actions.test.ts @@ -2,9 +2,9 @@ import { describe, expect, it, vi } from "vitest"; -import { createChatState, createChatStateStore } from "../../../../../src/features/chat/application/state/reducer"; -import { createToolbarPanelActions } from "../../../../../src/features/chat/panel/toolbar-actions"; -import type { ThreadManagementActions } from "../../../../../src/features/chat/application/threads/thread-management-actions"; +import { createChatState, createChatStateStore } from "../../../../src/features/chat/application/state/reducer"; +import { createToolbarPanelActions } from "../../../../src/features/chat/panel/toolbar-actions"; +import type { ThreadManagementActions } from "../../../../src/features/chat/application/threads/thread-management-actions"; describe("createToolbarPanelActions", () => { it("tracks archive confirmation and delegates archive actions", async () => { diff --git a/tests/features/chat/ui/toolbar-archive-state.test.tsx b/tests/features/chat/panel/toolbar-archive-state.test.tsx similarity index 98% rename from tests/features/chat/ui/toolbar-archive-state.test.tsx rename to tests/features/chat/panel/toolbar-archive-state.test.tsx index 0595efc2..deaaf375 100644 --- a/tests/features/chat/ui/toolbar-archive-state.test.tsx +++ b/tests/features/chat/panel/toolbar-archive-state.test.tsx @@ -8,7 +8,7 @@ import { createChatStateStore } from "../../../../src/features/chat/application/ import { createToolbarPanelActions, type ToolbarPanelActions } from "../../../../src/features/chat/panel/toolbar-actions"; import type { ChatPanelSurface, ChatPanelToolbarSurface } from "../../../../src/features/chat/panel/surface/model"; import type { ThreadManagementActions } from "../../../../src/features/chat/application/threads/thread-management-actions"; -import { renderChatPanelShell, unmountChatPanelShell, type ChatPanelShellParts } from "../../../../src/features/chat/ui/shell"; +import { renderChatPanelShell, unmountChatPanelShell, type ChatPanelShellParts } from "../../../../src/features/chat/panel/shell"; import { installObsidianDomShims } from "../../../support/dom"; installObsidianDomShims(); diff --git a/tests/features/chat/panel/view-lifecycle.test.ts b/tests/features/chat/panel/view-lifecycle.test.ts index 6ef8a5ca..e2f934ab 100644 --- a/tests/features/chat/panel/view-lifecycle.test.ts +++ b/tests/features/chat/panel/view-lifecycle.test.ts @@ -4,9 +4,9 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { EventRef } from "obsidian"; import { closeChatView, openChatView, type ChatViewLifecycleHost } from "../../../../src/features/chat/panel/view-lifecycle"; -import { unmountChatPanelShell } from "../../../../src/features/chat/ui/shell"; +import { unmountChatPanelShell } from "../../../../src/features/chat/panel/shell"; -vi.mock("../../../../src/features/chat/ui/shell", () => ({ +vi.mock("../../../../src/features/chat/panel/shell", () => ({ unmountChatPanelShell: vi.fn(), })); diff --git a/tests/features/chat/ui/message-stream/test-helpers.tsx b/tests/features/chat/ui/message-stream/test-helpers.tsx index 33ef9ed3..ee777043 100644 --- a/tests/features/chat/ui/message-stream/test-helpers.tsx +++ b/tests/features/chat/ui/message-stream/test-helpers.tsx @@ -4,15 +4,15 @@ import { act } from "preact/test-utils"; import type { PendingApproval, PendingUserInput } from "../../../../../src/features/chat/domain/pending-requests/model"; import { pendingRequestBlockSnapshotFromState } from "../../../../../src/features/chat/presentation/pending-requests/snapshot"; -import type { PendingRequestBlockActions } from "../../../../../src/features/chat/application/pending-requests/block"; import { pendingRequestBlockNode } from "../../../../../src/features/chat/ui/message-stream/pending-request-block"; -import { - activeTurnId, - type ChatDisclosureUiState, - type ChatTurnLifecycleState, -} from "../../../../../src/features/chat/application/state/reducer"; import { messageStreamBlocks as rawMessageStreamBlocks } from "../../../../../src/features/chat/ui/message-stream/stream-blocks"; -import type { MessageStreamBlock, MessageStreamContext } from "../../../../../src/features/chat/ui/message-stream/context"; +import type { + MessageStreamBlock, + MessageStreamContext, + MessageStreamDisclosureState, + MessageStreamTurnLifecycleState, + PendingRequestBlockActions, +} 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 { MessageStreamViewport } from "../../../../../src/features/chat/ui/message-stream/viewport"; @@ -24,7 +24,7 @@ export function messageStreamBlocks( const normalized = normalizeMessageStreamContext(context); const viewBlocks = messageStreamViewBlocks({ activeThreadId: normalized.activeThreadId, - activeTurnId: activeTurnId({ lifecycle: normalized.turnLifecycle }), + activeTurnId: activeTurnIdForMessageStream(normalized.turnLifecycle), historyCursor: context.historyCursor, loadingHistory: context.loadingHistory, items: context.items, @@ -48,11 +48,13 @@ type TestMessageStreamContext = Omit; }; -export function emptyDisclosures(): ChatDisclosureUiState { +export function emptyDisclosures(): MessageStreamDisclosureState { return testDisclosures(); } -export function testDisclosures(overrides: Partial> = {}): ChatDisclosureUiState { +export function testDisclosures( + overrides: Partial> = {}, +): MessageStreamDisclosureState { return { toolResults: new Set(overrides.toolResults), activityGroups: new Set(overrides.activityGroups), @@ -194,18 +196,22 @@ export function pendingRequestActions(overrides: Partial(scrollHeight: number, fn: () => T): T { const descriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "scrollHeight"); Object.defineProperty(HTMLElement.prototype, "scrollHeight", { diff --git a/tests/features/chat/ui/renderers/composer.test.ts b/tests/features/chat/ui/renderers/composer.test.ts index b5bed937..df574245 100644 --- a/tests/features/chat/ui/renderers/composer.test.ts +++ b/tests/features/chat/ui/renderers/composer.test.ts @@ -8,8 +8,8 @@ import { scrollComposerSuggestionIntoView, syncComposerHeight, type ComposerCallbacks, + type ComposerSuggestion, } from "../../../../../src/features/chat/ui/composer"; -import type { ComposerSuggestion } from "../../../../../src/features/chat/application/composer/suggestions"; import type { ComposerMetaViewModel } from "../../../../../src/features/chat/ui/composer"; import { renderUiRoot } from "../../../../../src/shared/ui/ui-root"; import { waitForAsyncWork } from "../../../../support/async"; diff --git a/tests/scripts/eslint-config.test.ts b/tests/scripts/eslint-config.test.ts index 9de88bf3..451f4fca 100644 --- a/tests/scripts/eslint-config.test.ts +++ b/tests/scripts/eslint-config.test.ts @@ -68,7 +68,7 @@ export function setBoxValue(box: Box): void { it("does not confuse signal value writes with DOM writes", async () => { const messages = await lintSource( - "src/features/chat/ui/shell-state.tsx", + "src/features/chat/panel/shell-state.tsx", ` import { signal } from "@preact/signals"; @@ -247,7 +247,7 @@ export function mutateState(store: ChatStateStore): void { it("allows signals in the chat shell-state adapter", async () => { const messages = await lintSource( - "src/features/chat/ui/shell-state.tsx", + "src/features/chat/panel/shell-state.tsx", ` import { signal } from "@preact/signals"; @@ -339,7 +339,7 @@ export const render = renderUiRoot; it("allows Preact root adapter imports in explicit root bridge files", async () => { const messages = await lintSource( - "src/features/chat/ui/shell.tsx", + "src/features/chat/panel/shell.tsx", ` import { renderUiRoot } from "../../../shared/ui/ui-root";