diff --git a/src/features/chat/composer-controller.ts b/src/features/chat/chat-composer-controller.ts similarity index 97% rename from src/features/chat/composer-controller.ts rename to src/features/chat/chat-composer-controller.ts index 7164f1db..63f8c504 100644 --- a/src/features/chat/composer-controller.ts +++ b/src/features/chat/chat-composer-controller.ts @@ -15,11 +15,11 @@ import { userInputWithWikiLinkMentionsAndSkills } from "./composer/wikilink-cont import type { UserInput } from "../../generated/app-server/v2/UserInput"; import type { SendShortcut } from "../../settings/model"; import { renderComposerShell, renderComposerSuggestions, syncComposerControls, syncComposerHeight } from "./ui/composer"; -import type { PanelState } from "./state"; +import type { ChatState } from "./chat-state"; -export interface PanelComposerControllerOptions { +export interface ChatComposerControllerOptions { app: App; - state: PanelState; + state: ChatState; viewId: string; sendShortcut: () => SendShortcut; canInterrupt: () => boolean; @@ -29,13 +29,13 @@ export interface PanelComposerControllerOptions { onNewThread: () => void; } -export class PanelComposerController { +export class ChatComposerController { private composer: HTMLTextAreaElement | null = null; private suggestionsEl: HTMLElement | null = null; private noteCandidatesCache: NoteCandidate[] | null = null; private noteEventsRegistered = false; - constructor(private readonly options: PanelComposerControllerOptions) {} + constructor(private readonly options: ChatComposerControllerOptions) {} get trimmedDraft(): string { return this.composer?.value.trim() ?? this.options.state.composerDraft.trim(); diff --git a/src/features/chat/controller.ts b/src/features/chat/chat-controller.ts similarity index 98% rename from src/features/chat/controller.ts rename to src/features/chat/chat-controller.ts index 75e70bf7..f539d2e5 100644 --- a/src/features/chat/controller.ts +++ b/src/features/chat/chat-controller.ts @@ -25,7 +25,7 @@ import type { ServerRequest } from "../../generated/app-server/ServerRequest"; import type { FileUpdateChange } from "../../generated/app-server/v2/FileUpdateChange"; import type { ThreadItem } from "../../generated/app-server/v2/ThreadItem"; import type { Turn } from "../../generated/app-server/v2/Turn"; -import { clearActiveThreadState, type PanelState } from "./state"; +import { clearActiveThreadState, type ChatState } from "./chat-state"; import { userInputResponse, type PendingUserInput } from "./user-input/model"; import { jsonPreview } from "../../utils"; import { classifyAppServerLog } from "./app-server-logs"; @@ -33,7 +33,7 @@ import { attachHookRunsToTurn, hookRunDisplayItem } from "./hook-display"; import { routeServerNotification, routeServerRequest } from "./inbound-routing"; import { clearUserInputDrafts, createApprovalResultItem, createUserInputResultItem } from "./request-state"; -export interface PanelControllerActions { +export interface ChatControllerActions { refreshThreads: () => void; refreshSkills: (forceReload?: boolean) => void; maybeNameThread: (threadId: string, turn: Turn) => void; @@ -42,10 +42,10 @@ export interface PanelControllerActions { rejectServerRequest: (requestId: RequestId, code: number, message: string) => boolean; } -export class PanelController { +export class ChatController { constructor( - private readonly state: PanelState, - private readonly actions: PanelControllerActions, + private readonly state: ChatState, + private readonly actions: ChatControllerActions, ) {} handleNotification(notification: ServerNotification): void { diff --git a/src/features/chat/message-renderer.ts b/src/features/chat/chat-message-renderer.ts similarity index 93% rename from src/features/chat/message-renderer.ts rename to src/features/chat/chat-message-renderer.ts index b069c8b5..06ffff89 100644 --- a/src/features/chat/message-renderer.ts +++ b/src/features/chat/chat-message-renderer.ts @@ -5,28 +5,28 @@ import { copyTextWithNotice } from "../../shared/ui/clipboard"; import { renderTextWithWikiLinks as renderInlineWikiLinks } from "../../shared/ui/dom"; import { messageRenderBlocks, notifyMessageContentRendered, syncMessageRenderBlocks } from "./ui/message-stream"; import { bottomScrollTop, captureScrollAnchor, isNearScrollBottom, restoreScrollAnchor } from "./ui/scroll"; -import type { TurnDiffViewState } from "./ui/turn-diff"; +import type { ChatTurnDiffViewState } from "./ui/turn-diff"; import { markdownFileLinkTarget } from "./markdown-file-links"; import { isRollbackCandidateItem, rollbackCandidateFromItems } from "./rollback"; -import type { PanelState } from "./state"; +import type { ChatState } from "./chat-state"; -export interface PanelMessageRendererOptions { +export interface ChatMessageRendererOptions { app: App; owner: Component; - state: PanelState; + state: ChatState; vaultPath: string; blockSignatures: Map; consumeForceScrollToBottom: () => boolean; loadOlderTurns: () => void; rollbackThread: (threadId: string) => void; implementPlan: (item: DisplayItem) => void; - openTurnDiff: (state: TurnDiffViewState) => void; + openTurnDiff: (state: ChatTurnDiffViewState) => void; pendingRequestsSignature: () => string; renderPendingRequests: () => HTMLElement | null; } -export class PanelMessageRenderer { - constructor(private readonly options: PanelMessageRendererOptions) {} +export class ChatMessageRenderer { + constructor(private readonly options: ChatMessageRendererOptions) {} render(parent: HTMLElement): void { const { state } = this.options; @@ -154,7 +154,7 @@ export class PanelMessageRenderer { } export function implementPlanCandidateFromState( - state: Pick, + state: Pick, ): DisplayItem | null { if (!state.activeThreadId || state.busy || state.composerDraft.trim().length > 0 || state.requestedCollaborationMode !== "plan") { return null; diff --git a/src/features/chat/session-controller.ts b/src/features/chat/chat-session-controller.ts similarity index 96% rename from src/features/chat/session-controller.ts rename to src/features/chat/chat-session-controller.ts index b315d56a..5b709dba 100644 --- a/src/features/chat/session-controller.ts +++ b/src/features/chat/chat-session-controller.ts @@ -8,18 +8,18 @@ import { import { reportedServiceTier } from "../../app-server/service-tier"; import type { McpServerStatus } from "../../generated/app-server/v2/McpServerStatus"; import { requestedOrConfiguredServiceTier, type RuntimeSnapshot } from "../../runtime/state"; -import type { PanelState } from "./state"; +import type { ChatState } from "./chat-state"; -export interface PanelSessionControllerHost { - state: PanelState; +export interface ChatSessionControllerHost { + state: ChatState; vaultPath: string; currentClient: () => AppServerClient | null; runtimeSnapshot: () => RuntimeSnapshot; forceMessagesToBottom: () => void; } -export class PanelSessionController { - constructor(private readonly host: PanelSessionControllerHost) {} +export class ChatSessionController { + constructor(private readonly host: ChatSessionControllerHost) {} async refreshThreadList(): Promise { const client = this.host.currentClient(); diff --git a/src/features/chat/state.ts b/src/features/chat/chat-state.ts similarity index 95% rename from src/features/chat/state.ts rename to src/features/chat/chat-state.ts index 20aa82f9..b6ea92f5 100644 --- a/src/features/chat/state.ts +++ b/src/features/chat/chat-state.ts @@ -22,7 +22,7 @@ export interface PendingTurnStart { promptSubmitHookItemIds: string[]; } -export interface PanelState { +export interface ChatState { status: string; effectiveConfig: ConfigReadResponse | null; initializeResponse: InitializeResponse | null; @@ -66,7 +66,7 @@ export interface PanelState { openDetails: Set; } -export function createPanelState(): PanelState { +export function createChatState(): ChatState { return { status: "Idle", effectiveConfig: null, @@ -112,7 +112,7 @@ export function createPanelState(): PanelState { }; } -export function clearActiveTurnState(state: PanelState): void { +export function clearActiveTurnState(state: ChatState): void { state.activeTurnId = null; state.busy = false; state.pendingTurnStart = null; @@ -121,7 +121,7 @@ export function clearActiveTurnState(state: PanelState): void { state.userInputDrafts.clear(); } -export function clearActiveThreadState(state: PanelState): void { +export function clearActiveThreadState(state: ChatState): void { state.activeThreadId = null; state.activeThreadCwd = null; state.activeModel = null; @@ -139,7 +139,7 @@ export function clearActiveThreadState(state: PanelState): void { clearActiveTurnState(state); } -export function clearConnectionScopedState(state: PanelState): void { +export function clearConnectionScopedState(state: ChatState): void { clearActiveTurnState(state); state.activeModel = null; state.activeReasoningEffort = null; diff --git a/src/features/chat/turn-diff-view.ts b/src/features/chat/chat-turn-diff-view.ts similarity index 69% rename from src/features/chat/turn-diff-view.ts rename to src/features/chat/chat-turn-diff-view.ts index 504ddc08..8bfd2b9f 100644 --- a/src/features/chat/turn-diff-view.ts +++ b/src/features/chat/chat-turn-diff-view.ts @@ -3,23 +3,23 @@ import { ItemView, type ViewStateResult } from "obsidian"; import { VIEW_TYPE_CODEX_TURN_DIFF } from "../../constants"; import { copyTextWithNotice } from "../../shared/ui/clipboard"; import { - isPersistedTurnDiffViewState, - persistedTurnDiffViewState, - renderTurnDiffView, - type PersistedTurnDiffViewState, - type TurnDiffViewState, + isPersistedChatTurnDiffViewState, + persistedChatTurnDiffViewState, + renderChatTurnDiffView, + type PersistedChatTurnDiffViewState, + type ChatTurnDiffViewState, } from "./ui/turn-diff"; -export class CodexTurnDiffView extends ItemView { - private metadata: PersistedTurnDiffViewState | null = null; - private payload: TurnDiffViewState | null = null; +export class CodexChatTurnDiffView extends ItemView { + private metadata: PersistedChatTurnDiffViewState | null = null; + private payload: ChatTurnDiffViewState | null = null; override getViewType(): string { return VIEW_TYPE_CODEX_TURN_DIFF; } override getDisplayText(): string { - return "Codex turn diff"; + return "Codex chat turn diff"; } override getIcon(): string { @@ -32,7 +32,7 @@ export class CodexTurnDiffView extends ItemView { override async setState(state: unknown, result: ViewStateResult): Promise { await super.setState(state, result); - this.metadata = isPersistedTurnDiffViewState(state) + this.metadata = isPersistedChatTurnDiffViewState(state) ? { threadId: state.threadId, turnId: state.turnId, @@ -48,15 +48,15 @@ export class CodexTurnDiffView extends ItemView { this.render(); } - setDiffPayload(payload: TurnDiffViewState): void { - this.metadata = persistedTurnDiffViewState(payload); + setDiffPayload(payload: ChatTurnDiffViewState): void { + this.metadata = persistedChatTurnDiffViewState(payload); this.payload = payload; this.render(); } private render(): void { const root = this.contentEl; - renderTurnDiffView( + renderChatTurnDiffView( root, this.payload, this.payload ? { copyDiff: () => void this.copyDiff(this.payload?.diff ?? "") } : {}, diff --git a/src/features/chat/thread-history.ts b/src/features/chat/thread-history.ts index 5717a222..bbee57bb 100644 --- a/src/features/chat/thread-history.ts +++ b/src/features/chat/thread-history.ts @@ -1,9 +1,9 @@ import type { AppServerClient } from "../../app-server/client"; import { displayItemsFromTurns } from "./display/thread-items"; -import type { PanelState } from "./state"; +import type { ChatState } from "./chat-state"; export interface ThreadHistoryLoaderHost { - state: PanelState; + state: ChatState; currentClient: () => AppServerClient | null; render: () => void; addSystemMessage: (text: string) => void; diff --git a/src/features/chat/thread-rename.ts b/src/features/chat/thread-rename.ts index 888161c0..b12d29c8 100644 --- a/src/features/chat/thread-rename.ts +++ b/src/features/chat/thread-rename.ts @@ -2,7 +2,7 @@ import type { AppServerClient } from "../../app-server/client"; import type { Thread } from "../../generated/app-server/v2/Thread"; import type { Turn } from "../../generated/app-server/v2/Turn"; import type { CodexPanelSettings } from "../../settings/model"; -import type { PanelState } from "./state"; +import type { ChatState } from "./chat-state"; import { getThreadTitle } from "../../domain/threads/model"; import { findThreadNamingContext, @@ -19,7 +19,7 @@ export interface ThreadRenameEditState { } export interface ThreadRenameControllerHost { - state: PanelState; + state: ChatState; vaultPath: string; settings: () => CodexPanelSettings; ensureConnected: () => Promise; diff --git a/src/features/chat/ui/message-stream.ts b/src/features/chat/ui/message-stream.ts index e7da31e7..bdb2c465 100644 --- a/src/features/chat/ui/message-stream.ts +++ b/src/features/chat/ui/message-stream.ts @@ -13,7 +13,7 @@ import { renderReasoningItem, renderTaskProgressItem, } from "./work-items"; -import type { TurnDiffViewState } from "./turn-diff"; +import type { ChatTurnDiffViewState } from "./turn-diff"; const USER_MESSAGE_COLLAPSE_HEIGHT_PX = 360; const MESSAGE_CONTENT_RENDERED_EVENT = "codex-panel:message-content-rendered"; @@ -43,7 +43,7 @@ export interface MessageStreamContext { onImplementPlanItem?: (item: DisplayItem) => void; canRollbackItem?: (item: DisplayItem) => boolean; onRollbackItem?: (item: DisplayItem) => void; - openTurnDiff?: (state: TurnDiffViewState) => void; + openTurnDiff?: (state: ChatTurnDiffViewState) => void; pendingRequestsSignature?: string; renderPendingRequests?: () => HTMLElement | null; } diff --git a/src/features/chat/ui/tool-result.ts b/src/features/chat/ui/tool-result.ts index fcaabac6..07a474c6 100644 --- a/src/features/chat/ui/tool-result.ts +++ b/src/features/chat/ui/tool-result.ts @@ -68,7 +68,7 @@ function renderDetailSection(parent: HTMLElement, section: ToolResultDetailSecti return; } if (section.kind === "diff") { - const diffBlock = renderOutputSection(parent, section.title, "codex-panel__diff-file"); + const diffBlock = renderOutputSection(parent, section.title, "codex-panel-diff-file"); renderDiff(diffBlock, section.diff); return; } @@ -95,9 +95,9 @@ function renderOutputSection(parent: HTMLElement, title: string, className: stri } function renderDiff(parent: HTMLElement, diff: string): void { - const pre = parent.createEl("pre", { cls: "codex-panel__diff" }); + const pre = parent.createEl("pre", { cls: "codex-panel-diff" }); for (const line of diff.split("\n")) { const cls = diffLineClassFromText(line); - pre.createEl("span", { cls: `codex-panel__diff-line codex-panel__diff-line--${cls}`, text: displayDiffLineText(line, cls) }); + pre.createEl("span", { cls: `codex-panel-diff__line codex-panel-diff__line--${cls}`, text: displayDiffLineText(line, cls) }); } } diff --git a/src/features/chat/ui/turn-diff.ts b/src/features/chat/ui/turn-diff.ts index 55acbefc..72e52359 100644 --- a/src/features/chat/ui/turn-diff.ts +++ b/src/features/chat/ui/turn-diff.ts @@ -4,7 +4,7 @@ import { shortThreadId } from "../../../utils"; export { displayDiffLines } from "../../../shared/diff/unified"; -export interface TurnDiffViewState { +export interface ChatTurnDiffViewState { threadId: string; turnId: string; cwd: string | null; @@ -12,13 +12,13 @@ export interface TurnDiffViewState { diff: string; } -export type PersistedTurnDiffViewState = Omit; +export type PersistedChatTurnDiffViewState = Omit; -export interface TurnDiffViewActions { +export interface ChatTurnDiffViewActions { copyDiff?: () => void; } -export function persistedTurnDiffViewState(state: TurnDiffViewState): PersistedTurnDiffViewState { +export function persistedChatTurnDiffViewState(state: ChatTurnDiffViewState): PersistedChatTurnDiffViewState { return { threadId: state.threadId, turnId: state.turnId, @@ -27,9 +27,9 @@ export function persistedTurnDiffViewState(state: TurnDiffViewState): PersistedT }; } -export function isPersistedTurnDiffViewState(value: unknown): value is PersistedTurnDiffViewState { +export function isPersistedChatTurnDiffViewState(value: unknown): value is PersistedChatTurnDiffViewState { if (!value || typeof value !== "object") return false; - const record = value as Partial; + const record = value as Partial; return ( typeof record.threadId === "string" && typeof record.turnId === "string" && @@ -39,20 +39,20 @@ export function isPersistedTurnDiffViewState(value: unknown): value is Persisted ); } -export function renderTurnDiffView( +export function renderChatTurnDiffView( parent: HTMLElement, - state: TurnDiffViewState | null, - actions: TurnDiffViewActions = {}, - metadata: PersistedTurnDiffViewState | null = null, + state: ChatTurnDiffViewState | null, + actions: ChatTurnDiffViewActions = {}, + metadata: PersistedChatTurnDiffViewState | null = null, ): void { parent.empty(); - parent.addClass("codex-panel-turn-diff"); + parent.addClass("codex-panel-chat-turn-diff"); if (!state) { if (metadata) { renderTurnDiffHeader(parent, metadata, null); - parent.createDiv({ cls: "codex-panel-turn-diff__empty", text: "Turn diff is no longer available." }); + parent.createDiv({ cls: "codex-panel-chat-turn-diff__empty", text: "Turn diff is no longer available." }); } else { - parent.createDiv({ cls: "codex-panel-turn-diff__empty", text: "No turn diff selected." }); + parent.createDiv({ cls: "codex-panel-chat-turn-diff__empty", text: "No turn diff selected." }); } return; } @@ -60,7 +60,7 @@ export function renderTurnDiffView( renderTurnDiffHeader(parent, state, actions.copyDiff ?? null); if (state.files.length > 0) { - const files = parent.createEl("details", { cls: "codex-panel-turn-diff__files" }); + const files = parent.createEl("details", { cls: "codex-panel-chat-turn-diff__files" }); files.createEl("summary", { text: "Changed files" }); const list = files.createEl("ul"); for (const file of state.files) { @@ -73,29 +73,29 @@ export function renderTurnDiffView( function renderTurnDiffHeader( parent: HTMLElement, - state: PersistedTurnDiffViewState, - copyDiff: TurnDiffViewActions["copyDiff"] | null, + state: PersistedChatTurnDiffViewState, + copyDiff: ChatTurnDiffViewActions["copyDiff"] | null, ): void { - const header = parent.createDiv({ cls: "codex-panel-turn-diff__header" }); - const titleBlock = header.createDiv({ cls: "codex-panel-turn-diff__title-block" }); - titleBlock.createDiv({ cls: "codex-panel-turn-diff__title", text: "Turn diff" }); + const header = parent.createDiv({ cls: "codex-panel-chat-turn-diff__header" }); + const titleBlock = header.createDiv({ cls: "codex-panel-chat-turn-diff__title-block" }); + titleBlock.createDiv({ cls: "codex-panel-chat-turn-diff__title", text: "Turn diff" }); titleBlock.createDiv({ - cls: "codex-panel-turn-diff__meta", + cls: "codex-panel-chat-turn-diff__meta", text: `${shortThreadId(state.threadId)} / ${shortThreadId(state.turnId)} ยท ${fileCountLabel(state.files)}`, attr: { title: `Thread ${state.threadId}\nTurn ${state.turnId}${state.cwd ? `\n${state.cwd}` : ""}` }, }); if (copyDiff) { - const copyButton = createIconButton(header, "copy", "Copy diff", "codex-panel-turn-diff__copy"); + const copyButton = createIconButton(header, "copy", "Copy diff", "codex-panel-chat-turn-diff__copy"); copyButton.onclick = copyDiff; } } export function renderUnifiedDiff(parent: HTMLElement, diff: string): HTMLElement { - const pre = parent.createEl("pre", { cls: "codex-panel__diff codex-panel-turn-diff__diff" }); + const pre = parent.createEl("pre", { cls: "codex-panel-diff codex-panel-chat-turn-diff__diff" }); for (const line of displayDiffLines(diff)) { const lineClass = diffLineClass(line); pre.createEl("span", { - cls: `codex-panel__diff-line codex-panel__diff-line--${lineClass}`, + cls: `codex-panel-diff__line codex-panel-diff__line--${lineClass}`, text: displayDiffLineText(line.text, lineClass), }); } diff --git a/src/features/chat/view.ts b/src/features/chat/view.ts index 2d67a983..f33e7d77 100644 --- a/src/features/chat/view.ts +++ b/src/features/chat/view.ts @@ -20,7 +20,7 @@ import { collaborationModeToggleMessage, nextCollaborationMode, } from "../../runtime/collaboration-mode"; -import { PanelController } from "./controller"; +import { ChatController } from "./chat-controller"; import { connectionDiagnosticSections, diagnosticAlertLevel } from "./diagnostics"; import { rollbackCandidateFromItems } from "./rollback"; import { contextSummary, effectiveConfigSections, rateLimitSummary } from "../../runtime/view"; @@ -46,16 +46,16 @@ import { compactContextLabel, modelOverrideMessage, reasoningEffortOverrideMessa import { executeSlashCommand as runSlashCommand, type SlashCommandExecutionResult } from "./slash-commands"; import type { ThreadReferenceInput } from "./slash-commands"; import { mcpStatusLines } from "./mcp-status"; -import { PanelSessionController } from "./session-controller"; +import { ChatSessionController } from "./chat-session-controller"; import { statusValue, usageLimitStatusLines } from "./status-lines"; import { ThreadHistoryLoader } from "./thread-history"; import { ThreadRenameController } from "./thread-rename"; import { pendingRequestsSignature as requestStateSignature, userInputDraftKey, userInputOtherDraftKey } from "./request-state"; import type { CodexPanelSettings } from "../../settings/model"; import { questionDefaultAnswer, type PendingUserInput } from "./user-input/model"; -import { PanelComposerController } from "./composer-controller"; +import { ChatComposerController } from "./chat-composer-controller"; import { attachHookRunsToTurn } from "./hook-display"; -import { clearActiveThreadState, clearConnectionScopedState, createPanelState, type PanelState } from "./state"; +import { clearActiveThreadState, clearConnectionScopedState, createChatState, type ChatState } from "./chat-state"; import { codexPanelDisplayTitle, getThreadTitle, inheritedForkThreadName, upsertThread } from "../../domain/threads/model"; import { exportArchivedThreadMarkdown } from "../../domain/threads/export"; import { @@ -68,28 +68,28 @@ import { } from "../../domain/threads/reference"; import { renderPendingRequestMessage } from "./ui/pending-request-message"; import { renderToolbar, toolbarSignature, type ToolbarChoice, type ToolbarViewModel } from "./ui/toolbar"; -import type { TurnDiffViewState } from "./ui/turn-diff"; -import { PanelMessageRenderer } from "./message-renderer"; +import type { ChatTurnDiffViewState } from "./ui/turn-diff"; +import { ChatMessageRenderer } from "./chat-message-renderer"; -export interface CodexPanelHost { +export interface CodexChatHost { readonly settings: CodexPanelSettings; readonly vaultPath: string; openThreadInNewView(threadId: string): Promise; - openTurnDiff(state: TurnDiffViewState): Promise; + openTurnDiff(state: ChatTurnDiffViewState): Promise; refreshOpenThreadLists(): void; } -export class CodexPanelView extends ItemView { +export class CodexChatView extends ItemView { private client: AppServerClient | null = null; private readonly connection: ConnectionManager; - private readonly controller: PanelController; - private readonly session: PanelSessionController; + private readonly controller: ChatController; + private readonly session: ChatSessionController; private readonly history: ThreadHistoryLoader; private readonly threadRename: ThreadRenameController; - private readonly state: PanelState = createPanelState(); + private readonly state: ChatState = createChatState(); private readonly viewId = `codex-panel-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; - private readonly composerController: PanelComposerController; - private readonly messageRenderer: PanelMessageRenderer; + private readonly composerController: ChatComposerController; + private readonly messageRenderer: ChatMessageRenderer; private readonly blockSignatures = new Map(); private toolbarEl: HTMLElement | null = null; private configSlotEl: HTMLElement | null = null; @@ -101,10 +101,10 @@ export class CodexPanelView extends ItemView { constructor( leaf: WorkspaceLeaf, - private readonly plugin: CodexPanelHost, + private readonly plugin: CodexChatHost, ) { super(leaf); - this.messageRenderer = new PanelMessageRenderer({ + this.messageRenderer = new ChatMessageRenderer({ app: this.app, owner: this, state: this.state, @@ -122,7 +122,7 @@ export class CodexPanelView extends ItemView { pendingRequestsSignature: () => this.pendingRequestsSignature(), renderPendingRequests: () => this.createPendingRequestsElement(), }); - this.composerController = new PanelComposerController({ + this.composerController = new ChatComposerController({ app: this.app, state: this.state, viewId: this.viewId, @@ -156,7 +156,7 @@ export class CodexPanelView extends ItemView { this.render(); }, }); - this.controller = new PanelController(this.state, { + this.controller = new ChatController(this.state, { refreshThreads: () => void this.refreshThreads(), refreshSkills: (forceReload) => void this.refreshSkills(forceReload), maybeNameThread: (threadId, turn) => { @@ -169,7 +169,7 @@ export class CodexPanelView extends ItemView { respondToServerRequest: (requestId, result) => this.respondToServerRequest(requestId, result), rejectServerRequest: (requestId, code, message) => this.rejectServerRequest(requestId, code, message), }); - this.session = new PanelSessionController({ + this.session = new ChatSessionController({ state: this.state, vaultPath: this.plugin.vaultPath, currentClient: () => this.connection.currentClient(), @@ -710,7 +710,7 @@ export class CodexPanelView extends ItemView { return latestProposedPlanItem(this.state.displayItems)?.id === item.id; } - private toggleRuntimePicker(picker: NonNullable): void { + private toggleRuntimePicker(picker: NonNullable): void { this.state.runtimePicker = this.state.runtimePicker === picker ? null : picker; if (this.state.runtimePicker !== null) { this.state.openDetails.delete("history"); diff --git a/src/features/selection-rewrite/command.ts b/src/features/selection-rewrite/command.ts index d8b0b81b..f3284cc4 100644 --- a/src/features/selection-rewrite/command.ts +++ b/src/features/selection-rewrite/command.ts @@ -1,19 +1,19 @@ import { MarkdownView, Notice, type Editor, type Plugin } from "obsidian"; -import type { RewriteRuntimeSettings, RewriteSession } from "./model"; -import { RewriteSelectionPopover } from "./popover"; +import type { SelectionRewriteRuntimeSettings, SelectionRewriteSession } from "./model"; +import { SelectionRewritePopover } from "./popover"; import type { SendShortcut } from "../../settings/model"; -export interface RewriteSelectionCommandHost extends Plugin { +export interface SelectionRewriteCommandHost extends Plugin { settings: { codexPath: string; sendShortcut: SendShortcut; - } & RewriteRuntimeSettings; + } & SelectionRewriteRuntimeSettings; vaultPath: string; } -export function registerRewriteSelectionCommand(plugin: RewriteSelectionCommandHost): void { - const activePopovers = new Set(); +export function registerSelectionRewriteCommand(plugin: SelectionRewriteCommandHost): void { + const activePopovers = new Set(); plugin.register(() => { for (const popover of activePopovers) popover.close(); @@ -35,7 +35,7 @@ export function registerRewriteSelectionCommand(plugin: RewriteSelectionCommandH return; } - const session: RewriteSession = { + const session: SelectionRewriteSession = { filePath: view.file.path, targetRange: { from: clonePosition(editor.getCursor("from")), @@ -50,7 +50,7 @@ export function registerRewriteSelectionCommand(plugin: RewriteSelectionCommandH debugText: null, }; - const popover = new RewriteSelectionPopover({ + const popover = new SelectionRewritePopover({ codexPath: plugin.settings.codexPath, cwd: plugin.vaultPath, editor, diff --git a/src/features/selection-rewrite/keys.ts b/src/features/selection-rewrite/keys.ts index 2daf413d..f7cb7a78 100644 --- a/src/features/selection-rewrite/keys.ts +++ b/src/features/selection-rewrite/keys.ts @@ -1,13 +1,13 @@ import { isComposerSendKey, type ComposerSendKeyEvent } from "../chat/composer/keys"; import type { SendShortcut } from "../../settings/model"; -export type RewriteGenerateKeyEvent = ComposerSendKeyEvent; +export type SelectionRewriteGenerateKeyEvent = ComposerSendKeyEvent; -export function isRewriteGenerateKey(event: RewriteGenerateKeyEvent, shortcut: SendShortcut): boolean { +export function isSelectionRewriteGenerateKey(event: SelectionRewriteGenerateKeyEvent, shortcut: SendShortcut): boolean { return isComposerSendKey(event, shortcut); } -export function isRewriteActionKey(event: RewriteGenerateKeyEvent): boolean { +export function isSelectionRewriteActionKey(event: SelectionRewriteGenerateKeyEvent): boolean { if (event.isComposing || event.key !== "Enter" || event.shiftKey || event.altKey) return false; return true; } diff --git a/src/features/selection-rewrite/model.ts b/src/features/selection-rewrite/model.ts index 0f6da635..1ef1aad9 100644 --- a/src/features/selection-rewrite/model.ts +++ b/src/features/selection-rewrite/model.ts @@ -1,9 +1,9 @@ import type { EditorPosition } from "obsidian"; import type { ReasoningEffort } from "../../generated/app-server/ReasoningEffort"; -export type RewriteStatus = "editing-prompt" | "generating" | "preview" | "applied" | "cancelled" | "failed"; +export type SelectionRewriteStatus = "editing-prompt" | "generating" | "preview" | "applied" | "cancelled" | "failed"; -export interface RewriteSession { +export interface SelectionRewriteSession { filePath: string; targetRange: { from: EditorPosition; @@ -12,17 +12,17 @@ export interface RewriteSession { originalText: string; noteText: string; instruction: string; - status: RewriteStatus; + status: SelectionRewriteStatus; streamText: string; replacementText: string | null; debugText: string | null; } -export interface RewriteRuntimeSettings { +export interface SelectionRewriteRuntimeSettings { rewriteSelectionModel: string | null; rewriteSelectionEffort: ReasoningEffort | null; } -export function canApplyRewrite(currentText: string, originalText: string): boolean { +export function canApplySelectionRewrite(currentText: string, originalText: string): boolean { return currentText === originalText; } diff --git a/src/features/selection-rewrite/output.ts b/src/features/selection-rewrite/output.ts index 72633bda..ef21bf15 100644 --- a/src/features/selection-rewrite/output.ts +++ b/src/features/selection-rewrite/output.ts @@ -1,25 +1,25 @@ import type { Turn } from "../../generated/app-server/v2/Turn"; -export interface RewriteOutput { +export interface SelectionRewriteOutput { replacementText: string; } -export interface RewriteOutputParseResult { - output: RewriteOutput | null; +export interface SelectionRewriteOutputParseResult { + output: SelectionRewriteOutput | null; rawText: string | null; } -export class RewriteOutputError extends Error { +export class SelectionRewriteOutputError extends Error { constructor( message: string, readonly rawText: string | null, ) { super(message); - this.name = "RewriteOutputError"; + this.name = "SelectionRewriteOutputError"; } } -export function parseRewriteOutput(text: string): RewriteOutput | null { +export function parseSelectionRewriteOutput(text: string): SelectionRewriteOutput | null { try { const parsed = JSON.parse(text.trim()) as unknown; if (!parsed || typeof parsed !== "object") return null; @@ -31,14 +31,14 @@ export function parseRewriteOutput(text: string): RewriteOutput | null { } } -export function rewriteOutputFromTurn(turn: Turn): RewriteOutput | null { - return rewriteOutputParseResultFromTurn(turn).output; +export function selectionRewriteOutputFromTurn(turn: Turn): SelectionRewriteOutput | null { + return selectionRewriteOutputParseResultFromTurn(turn).output; } -export function rewriteOutputParseResultFromTurn(turn: Turn): RewriteOutputParseResult { +export function selectionRewriteOutputParseResultFromTurn(turn: Turn): SelectionRewriteOutputParseResult { const text = lastAgentMessageText(turn); if (!text) return { output: null, rawText: null }; - return { output: parseRewriteOutput(text), rawText: text }; + return { output: parseSelectionRewriteOutput(text), rawText: text }; } function lastAgentMessageText(turn: Turn): string | null { diff --git a/src/features/selection-rewrite/popover.ts b/src/features/selection-rewrite/popover.ts index fbc145c9..a56b7e80 100644 --- a/src/features/selection-rewrite/popover.ts +++ b/src/features/selection-rewrite/popover.ts @@ -4,29 +4,29 @@ import { diffLineClass, displayDiffLineText, displayDiffLines } from "../../shar import { createIconButton } from "../../shared/ui/components"; import { syncTextareaHeight } from "../../shared/ui/textarea-autogrow"; import { buildSelectionUnifiedDiff } from "./diff"; -import { isRewriteActionKey, isRewriteGenerateKey } from "./keys"; -import { canApplyRewrite, type RewriteRuntimeSettings, type RewriteSession } from "./model"; -import { RewriteOutputError } from "./output"; -import { positionRewritePopover } from "./position"; -import { buildRewritePrompt } from "./prompt"; -import { runRewriteSelection, type RewriteActivity } from "./runner"; +import { isSelectionRewriteActionKey, isSelectionRewriteGenerateKey } from "./keys"; +import { canApplySelectionRewrite, type SelectionRewriteRuntimeSettings, type SelectionRewriteSession } from "./model"; +import { SelectionRewriteOutputError } from "./output"; +import { positionSelectionRewritePopover } from "./position"; +import { buildSelectionRewritePrompt } from "./prompt"; +import { runSelectionRewrite, type SelectionRewriteActivity } from "./runner"; import type { SendShortcut } from "../../settings/model"; const POPOVER_MARGIN = 8; -export interface RewriteSelectionPopoverOptions { +export interface SelectionRewritePopoverOptions { codexPath: string; cwd: string; editor: Editor; onClose?: () => void; - runtimeSettings: RewriteRuntimeSettings; + runtimeSettings: SelectionRewriteRuntimeSettings; sendShortcut: SendShortcut; - session: RewriteSession; + session: SelectionRewriteSession; } type Cleanup = () => void; -interface RewritePopoverElements { +interface SelectionRewriteElements { root: HTMLElement; instruction: HTMLTextAreaElement; generateButton: HTMLButtonElement; @@ -38,12 +38,12 @@ interface RewritePopoverElements { debug: HTMLDetailsElement | null; } -export class RewriteSelectionPopover { - private elements: RewritePopoverElements | null = null; +export class SelectionRewritePopover { + private elements: SelectionRewriteElements | null = null; private abortController: AbortController | null = null; private readonly cleanups: Cleanup[] = []; - constructor(private readonly options: RewriteSelectionPopoverOptions) {} + constructor(private readonly options: SelectionRewritePopoverOptions) {} open(): void { this.close(); @@ -104,10 +104,10 @@ export class RewriteSelectionPopover { this.abortController = abortController; try { - const output = await runRewriteSelection({ + const output = await runSelectionRewrite({ codexPath: this.options.codexPath, cwd: this.options.cwd, - prompt: buildRewritePrompt(this.options.session), + prompt: buildSelectionRewritePrompt(this.options.session), runtimeSettings: this.options.runtimeSettings, onActivity: (activity) => { this.updateActivity(activity); @@ -118,7 +118,7 @@ export class RewriteSelectionPopover { signal: abortController.signal, }); if (abortController.signal.aborted) return; - this.showRewritePreview(output.replacementText); + this.showSelectionRewritePreview(output.replacementText); } catch (error) { if (abortController.signal.aborted) { this.options.session.status = "cancelled"; @@ -146,17 +146,17 @@ export class RewriteSelectionPopover { this.position(); } - private updateActivity(activity: RewriteActivity): void { + private updateActivity(activity: SelectionRewriteActivity): void { this.setStatus(activity === "reasoning" ? "Reasoning" : "Writing replacement", { active: true }); } - private createElements(): RewritePopoverElements { - const root = activeDocument.body.createDiv({ cls: "codex-panel-rewrite-popover" }); + private createElements(): SelectionRewriteElements { + const root = activeDocument.body.createDiv({ cls: "codex-panel-selection-rewrite" }); root.setAttr("role", "dialog"); root.setAttr("aria-label", "Rewrite selection"); const instruction = root.createEl("textarea", { - cls: "codex-panel__input codex-panel-rewrite-popover__instruction", + cls: "codex-panel__input codex-panel-selection-rewrite__instruction", attr: { placeholder: "How should Codex rewrite this selection?" }, }); instruction.value = this.options.session.instruction; @@ -167,42 +167,47 @@ export class RewriteSelectionPopover { }; instruction.onkeydown = (event) => { const hasReplacement = this.options.session.replacementText !== null; - if (!(hasReplacement ? isRewriteActionKey(event) : isRewriteGenerateKey(event, this.options.sendShortcut))) return; + if (!(hasReplacement ? isSelectionRewriteActionKey(event) : isSelectionRewriteGenerateKey(event, this.options.sendShortcut))) return; event.preventDefault(); event.stopPropagation(); void this.generate(); }; - const promptRow = root.createDiv({ cls: "codex-panel-rewrite-popover__prompt-row" }); + const promptRow = root.createDiv({ cls: "codex-panel-selection-rewrite__prompt-row" }); promptRow.append(instruction); - const controls = promptRow.createDiv({ cls: "codex-panel__composer-actions codex-panel-rewrite-popover__controls" }); + const controls = promptRow.createDiv({ cls: "codex-panel__composer-actions codex-panel-selection-rewrite__controls" }); const generateButton = createIconButton( controls, "sparkles", "Generate", - "codex-panel__composer-action codex-panel-rewrite-popover__icon-button", + "codex-panel__composer-action codex-panel-selection-rewrite__icon-button", ); generateButton.onclick = () => void this.generate(); - const cancelButton = createIconButton(controls, "x", "Cancel", "codex-panel__composer-action codex-panel-rewrite-popover__icon-button"); + const cancelButton = createIconButton( + controls, + "x", + "Cancel", + "codex-panel__composer-action codex-panel-selection-rewrite__icon-button", + ); cancelButton.onclick = () => { this.cancel(); }; - const status = root.createDiv({ cls: "codex-panel-rewrite-popover__status" }); - const streamPreview = root.createEl("pre", { cls: "codex-panel-rewrite-popover__stream-preview is-hidden" }); - const resultRow = root.createDiv({ cls: "codex-panel-rewrite-popover__result-row" }); - const diff = resultRow.createDiv({ cls: "codex-panel-rewrite-popover__diff" }); + const status = root.createDiv({ cls: "codex-panel-selection-rewrite__status" }); + const streamPreview = root.createEl("pre", { cls: "codex-panel-selection-rewrite__stream-preview is-hidden" }); + const resultRow = root.createDiv({ cls: "codex-panel-selection-rewrite__result-row" }); + const diff = resultRow.createDiv({ cls: "codex-panel-selection-rewrite__diff" }); const applyButton = createIconButton( resultRow, "check", "Apply", - "codex-panel__composer-action codex-panel-rewrite-popover__icon-button mod-cta", + "codex-panel__composer-action codex-panel-selection-rewrite__icon-button mod-cta", ); applyButton.onclick = () => { this.apply(); }; applyButton.onkeydown = (event) => { - if (!isRewriteActionKey(event)) return; + if (!isSelectionRewriteActionKey(event)) return; event.preventDefault(); this.apply(); }; @@ -221,7 +226,7 @@ export class RewriteSelectionPopover { this.renderDebug(null); } - private showRewritePreview(replacementText: string): void { + private showSelectionRewritePreview(replacementText: string): void { this.options.session.replacementText = replacementText; this.options.session.status = "preview"; this.options.session.streamText = ""; @@ -232,7 +237,7 @@ export class RewriteSelectionPopover { private showGenerationFailure(error: unknown): void { this.options.session.status = "failed"; - this.options.session.debugText = error instanceof RewriteOutputError ? error.rawText : null; + this.options.session.debugText = error instanceof SelectionRewriteOutputError ? error.rawText : null; this.options.session.streamText = ""; this.renderStreamPreview(); this.renderDebug(this.options.session.debugText); @@ -251,7 +256,7 @@ export class RewriteSelectionPopover { const replacement = this.options.session.replacementText; if (replacement === null || !this.elements) return; this.elements.diff.empty(); - renderRewriteDiff( + renderSelectionRewriteDiff( this.elements.diff, buildSelectionUnifiedDiff(this.options.session.filePath, this.options.session.originalText, replacement), ); @@ -263,7 +268,7 @@ export class RewriteSelectionPopover { this.elements.debug = null; if (!debugText) return; - const debugEl = this.elements.root.createEl("details", { cls: "codex-panel-rewrite-popover__debug" }); + const debugEl = this.elements.root.createEl("details", { cls: "codex-panel-selection-rewrite__debug" }); debugEl.createEl("summary", { text: "Debug output" }); debugEl.createEl("pre", { text: debugText }); this.elements.debug = debugEl; @@ -275,7 +280,7 @@ export class RewriteSelectionPopover { const { editor, session } = this.options; const currentText = editor.getRange(session.targetRange.from, session.targetRange.to); - if (!canApplyRewrite(currentText, session.originalText)) { + if (!canApplySelectionRewrite(currentText, session.originalText)) { new Notice("Selection changed. Generate the rewrite again before applying."); this.setStatus("Selection changed. Generate the rewrite again before applying."); return; @@ -299,7 +304,7 @@ export class RewriteSelectionPopover { if (!text && !options.active) return; status.createSpan({ text }); if (!options.active) return; - const dots = status.createSpan({ cls: "codex-panel-rewrite-popover__status-dots" }); + const dots = status.createSpan({ cls: "codex-panel-selection-rewrite__status-dots" }); dots.createSpan({ text: "." }); dots.createSpan({ text: "." }); dots.createSpan({ text: "." }); @@ -328,7 +333,7 @@ export class RewriteSelectionPopover { private position(): void { if (!this.elements) return; - if (!positionRewritePopover(this.elements.root, this.options.editor, POPOVER_MARGIN)) this.close(); + if (!positionSelectionRewritePopover(this.elements.root, this.options.editor, POPOVER_MARGIN)) this.close(); } private addDomListener( @@ -356,13 +361,13 @@ export class RewriteSelectionPopover { } } -function renderRewriteDiff(parent: HTMLElement, diff: string): void { - const pre = parent.createEl("pre", { cls: "codex-panel__diff codex-panel-rewrite-popover__diff-body" }); +function renderSelectionRewriteDiff(parent: HTMLElement, diff: string): void { + const pre = parent.createEl("pre", { cls: "codex-panel-diff codex-panel-selection-rewrite__diff-body" }); for (const line of displayDiffLines(diff)) { if (line.kind === "file" || line.text.startsWith("@@")) continue; const lineClass = diffLineClass(line); pre.createEl("span", { - cls: `codex-panel__diff-line codex-panel__diff-line--${lineClass}`, + cls: `codex-panel-diff__line codex-panel-diff__line--${lineClass}`, text: displayDiffLineText(line.text, lineClass), }); } diff --git a/src/features/selection-rewrite/position.ts b/src/features/selection-rewrite/position.ts index fb3cfd15..4916e3ca 100644 --- a/src/features/selection-rewrite/position.ts +++ b/src/features/selection-rewrite/position.ts @@ -1,6 +1,6 @@ import type { Editor } from "obsidian"; -export function positionRewritePopover(root: HTMLElement, editor: Editor, margin: number): boolean { +export function positionSelectionRewritePopover(root: HTMLElement, editor: Editor, margin: number): boolean { if (!root.isConnected) return false; const view = editorViewFromEditor(editor); diff --git a/src/features/selection-rewrite/prompt.ts b/src/features/selection-rewrite/prompt.ts index 3f2338c3..d8a10456 100644 --- a/src/features/selection-rewrite/prompt.ts +++ b/src/features/selection-rewrite/prompt.ts @@ -1,10 +1,10 @@ -import type { RewriteSession } from "./model"; +import type { SelectionRewriteSession } from "./model"; const MAX_NOTE_CONTEXT_CHARS = 20_000; -export const REWRITE_SERVICE_NAME = "codex-panel-rewrite-selection"; +export const SELECTION_REWRITE_SERVICE_NAME = "codex-panel-rewrite-selection"; -export const REWRITE_DEVELOPER_INSTRUCTIONS = [ +export const SELECTION_REWRITE_DEVELOPER_INSTRUCTIONS = [ "You rewrite selected Obsidian note text.", "Return only JSON matching the requested schema.", "Do not include Markdown fences, explanations, alternatives, or comments outside JSON.", @@ -12,7 +12,7 @@ export const REWRITE_DEVELOPER_INSTRUCTIONS = [ "Preserve Obsidian-specific syntax such as wikilinks, block ids, callouts, frontmatter-like text, and Dataview blocks unless the user's instruction explicitly asks to change them.", ].join("\n"); -export function buildRewritePrompt(session: RewriteSession): string { +export function buildSelectionRewritePrompt(session: SelectionRewriteSession): string { return [ "Rewrite the selected text according to the user's instruction.", "", diff --git a/src/features/selection-rewrite/runner.ts b/src/features/selection-rewrite/runner.ts index 725f724a..4fe5811a 100644 --- a/src/features/selection-rewrite/runner.ts +++ b/src/features/selection-rewrite/runner.ts @@ -6,13 +6,13 @@ import type { Model } from "../../generated/app-server/v2/Model"; import type { ThreadItem } from "../../generated/app-server/v2/ThreadItem"; import type { Turn } from "../../generated/app-server/v2/Turn"; import { runtimeOverride, validatedRuntimeOverride } from "../../runtime/model"; -import type { RewriteRuntimeSettings } from "./model"; -import { REWRITE_DEVELOPER_INSTRUCTIONS, REWRITE_SERVICE_NAME } from "./prompt"; -import { RewriteOutputError, rewriteOutputParseResultFromTurn, type RewriteOutput } from "./output"; +import type { SelectionRewriteRuntimeSettings } from "./model"; +import { SELECTION_REWRITE_DEVELOPER_INSTRUCTIONS, SELECTION_REWRITE_SERVICE_NAME } from "./prompt"; +import { SelectionRewriteOutputError, selectionRewriteOutputParseResultFromTurn, type SelectionRewriteOutput } from "./output"; -const REWRITE_TIMEOUT_MS = 120_000; +const SELECTION_REWRITE_TIMEOUT_MS = 120_000; -const REWRITE_OUTPUT_SCHEMA: JsonValue = { +const SELECTION_REWRITE_OUTPUT_SCHEMA: JsonValue = { type: "object", properties: { replacementText: { @@ -23,19 +23,19 @@ const REWRITE_OUTPUT_SCHEMA: JsonValue = { additionalProperties: false, }; -export interface RunRewriteSelectionOptions { +export interface RunSelectionRewriteOptions { codexPath: string; cwd: string; prompt: string; - runtimeSettings?: RewriteRuntimeSettings; - onActivity?: (activity: RewriteActivity) => void; + runtimeSettings?: SelectionRewriteRuntimeSettings; + onActivity?: (activity: SelectionRewriteActivity) => void; onPreview?: (text: string) => void; signal?: AbortSignal; } -export type RewriteActivity = "reasoning" | "writing"; +export type SelectionRewriteActivity = "reasoning" | "writing"; -export async function runRewriteSelection(options: RunRewriteSelectionOptions): Promise { +export async function runSelectionRewrite(options: RunSelectionRewriteOptions): Promise { throwIfAborted(options.signal); let threadId: string | null = null; let expectedTurnId: string | null = null; @@ -52,7 +52,7 @@ export async function runRewriteSelection(options: RunRewriteSelectionOptions): if (completed) return; completed = true; reject(new Error("Timed out while rewriting the selection.")); - }, REWRITE_TIMEOUT_MS); + }, SELECTION_REWRITE_TIMEOUT_MS); handleNotification = (notification): void => { if (completed) return; @@ -101,22 +101,22 @@ export async function runRewriteSelection(options: RunRewriteSelectionOptions): onExit: () => { if (completed) return; completed = true; - rejectCompletedTurn?.(new Error("Codex rewrite app-server exited.")); + rejectCompletedTurn?.(new Error("Selection rewrite app-server exited.")); }, }); try { await abortable(client.connect(), options.signal); const runtime = options.runtimeSettings - ? await abortable(rewriteRuntimeForClient(client, options.runtimeSettings), options.signal) + ? await abortable(selectionRewriteRuntimeForClient(client, options.runtimeSettings), options.signal) : {}; const threadResponse = await abortable( - client.startEphemeralThread(options.cwd, REWRITE_SERVICE_NAME, REWRITE_DEVELOPER_INSTRUCTIONS), + client.startEphemeralThread(options.cwd, SELECTION_REWRITE_SERVICE_NAME, SELECTION_REWRITE_DEVELOPER_INSTRUCTIONS), options.signal, ); threadId = threadResponse.thread.id; const turnResponse = await abortable( - client.startStructuredTurn(threadId, options.cwd, options.prompt, REWRITE_OUTPUT_SCHEMA, runtime.model, runtime.effort), + client.startStructuredTurn(threadId, options.cwd, options.prompt, SELECTION_REWRITE_OUTPUT_SCHEMA, runtime.model, runtime.effort), options.signal, ); expectedTurnId = turnResponse.turn.id; @@ -124,8 +124,8 @@ export async function runRewriteSelection(options: RunRewriteSelectionOptions): turnResponse.turn.status === "completed" ? turnWithCollectedItems(turnResponse.turn, completedItems) : await abortable(completedTurn, options.signal); - const { output, rawText } = rewriteOutputParseResultFromTurn(turn); - if (!output) throw new RewriteOutputError("Codex did not return a valid rewrite patch.", rawText); + const { output, rawText } = selectionRewriteOutputParseResultFromTurn(turn); + if (!output) throw new SelectionRewriteOutputError("Codex did not return a valid selection rewrite response.", rawText); return output; } finally { completed = true; @@ -135,7 +135,7 @@ export async function runRewriteSelection(options: RunRewriteSelectionOptions): } function throwIfAborted(signal: AbortSignal | undefined): void { - if (signal?.aborted) throw rewriteAbortError(); + if (signal?.aborted) throw selectionRewriteAbortError(); } function abortable(promise: Promise, signal: AbortSignal | undefined): Promise { @@ -143,7 +143,7 @@ function abortable(promise: Promise, signal: AbortSignal | undefined): Pro throwIfAborted(signal); return new Promise((resolve, reject) => { const onAbort = (): void => { - reject(rewriteAbortError()); + reject(selectionRewriteAbortError()); }; signal.addEventListener("abort", onAbort, { once: true }); promise.then(resolve, reject).finally(() => { @@ -152,7 +152,7 @@ function abortable(promise: Promise, signal: AbortSignal | undefined): Pro }); } -function rewriteAbortError(): Error { +function selectionRewriteAbortError(): Error { return new Error("Selection rewrite cancelled."); } @@ -161,25 +161,28 @@ function turnWithCollectedItems(turn: Turn, completedItems: ThreadItem[]): Turn return { ...turn, items: completedItems }; } -export interface RewriteRuntime { +export interface SelectionRewriteRuntime { model?: string; effort?: ReasoningEffort; } -export function rewriteRuntime(settings: RewriteRuntimeSettings): RewriteRuntime { +export function selectionRewriteRuntime(settings: SelectionRewriteRuntimeSettings): SelectionRewriteRuntime { return runtimeOverride({ model: settings.rewriteSelectionModel, effort: settings.rewriteSelectionEffort }); } -export function validatedRewriteRuntime(settings: RewriteRuntimeSettings, models: Model[]): RewriteRuntime { +export function validatedSelectionRewriteRuntime(settings: SelectionRewriteRuntimeSettings, models: Model[]): SelectionRewriteRuntime { return validatedRuntimeOverride({ model: settings.rewriteSelectionModel, effort: settings.rewriteSelectionEffort }, models); } -async function rewriteRuntimeForClient(client: AppServerClient, settings: RewriteRuntimeSettings): Promise { - const runtime = rewriteRuntime(settings); +async function selectionRewriteRuntimeForClient( + client: AppServerClient, + settings: SelectionRewriteRuntimeSettings, +): Promise { + const runtime = selectionRewriteRuntime(settings); if (!runtime.model || !runtime.effort) return runtime; try { const response = await client.listModels(false); - return validatedRewriteRuntime(settings, response.data); + return validatedSelectionRewriteRuntime(settings, response.data); } catch { return runtime; } diff --git a/src/main.ts b/src/main.ts index 99a62c19..3b59f346 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,12 +1,12 @@ import { Plugin, type WorkspaceLeaf } from "obsidian"; import { VIEW_TYPE_CODEX_PANEL, VIEW_TYPE_CODEX_TURN_DIFF } from "./constants"; -import { registerRewriteSelectionCommand } from "./features/selection-rewrite/command"; -import { CodexPanelView } from "./features/chat/view"; -import { CodexTurnDiffView } from "./features/chat/turn-diff-view"; +import { registerSelectionRewriteCommand } from "./features/selection-rewrite/command"; +import { CodexChatView } from "./features/chat/view"; +import { CodexChatTurnDiffView } from "./features/chat/chat-turn-diff-view"; import { DEFAULT_SETTINGS, getVaultPath, normalizeSettings, settingsMatchNormalizedData, type CodexPanelSettings } from "./settings/model"; import { CodexPanelSettingTab } from "./settings/tab"; -import { persistedTurnDiffViewState, type TurnDiffViewState } from "./features/chat/ui/turn-diff"; +import { persistedChatTurnDiffViewState, type ChatTurnDiffViewState } from "./features/chat/ui/turn-diff"; export default class CodexPanelPlugin extends Plugin { settings: CodexPanelSettings = DEFAULT_SETTINGS; @@ -16,8 +16,8 @@ export default class CodexPanelPlugin extends Plugin { this.vaultPath = getVaultPath(this.app); await this.loadSettings(); - this.registerView(VIEW_TYPE_CODEX_PANEL, (leaf) => new CodexPanelView(leaf, this)); - this.registerView(VIEW_TYPE_CODEX_TURN_DIFF, (leaf) => new CodexTurnDiffView(leaf)); + this.registerView(VIEW_TYPE_CODEX_PANEL, (leaf) => new CodexChatView(leaf, this)); + this.registerView(VIEW_TYPE_CODEX_TURN_DIFF, (leaf) => new CodexChatTurnDiffView(leaf)); this.addRibbonIcon("bot-message-square", "Open panel", () => { void this.activateView(); @@ -44,40 +44,40 @@ export default class CodexPanelPlugin extends Plugin { }, }); - registerRewriteSelectionCommand(this); + registerSelectionRewriteCommand(this); this.addSettingTab(new CodexPanelSettingTab(this.app, this)); } - async activateView(): Promise { + async activateView(): Promise { const leaf = await this.app.workspace.ensureSideLeaf(VIEW_TYPE_CODEX_PANEL, "right", { active: true, reveal: true, }); - return leaf.view as CodexPanelView; + return leaf.view as CodexChatView; } - async activateNewView(): Promise { + async activateNewView(): Promise { const leaf = this.createRightSidebarTab(); if (!leaf) throw new Error("Could not create a right sidebar leaf."); await leaf.setViewState({ type: VIEW_TYPE_CODEX_PANEL, active: true }); await this.app.workspace.revealLeaf(leaf); - return leaf.view as CodexPanelView; + return leaf.view as CodexChatView; } - async openThreadInNewView(threadId: string): Promise { + async openThreadInNewView(threadId: string): Promise { const view = await this.activateNewView(); await view.openThread(threadId); return view; } - async openTurnDiff(state: TurnDiffViewState): Promise { + async openTurnDiff(state: ChatTurnDiffViewState): Promise { const existing = this.app.workspace.getLeavesOfType(VIEW_TYPE_CODEX_TURN_DIFF).at(0); const leaf = existing ?? this.app.workspace.getLeaf("tab"); - await leaf.setViewState({ type: VIEW_TYPE_CODEX_TURN_DIFF, active: true, state: { ...persistedTurnDiffViewState(state) } }); + await leaf.setViewState({ type: VIEW_TYPE_CODEX_TURN_DIFF, active: true, state: { ...persistedChatTurnDiffViewState(state) } }); await leaf.loadIfDeferred(); - if (leaf.view instanceof CodexTurnDiffView) { + if (leaf.view instanceof CodexChatTurnDiffView) { leaf.view.setDiffPayload(state); } await this.app.workspace.revealLeaf(leaf); @@ -85,7 +85,7 @@ export default class CodexPanelPlugin extends Plugin { refreshOpenViews(): void { for (const leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_CODEX_PANEL)) { - if (leaf.view instanceof CodexPanelView) { + if (leaf.view instanceof CodexChatView) { leaf.view.refreshSettings(); } } @@ -93,7 +93,7 @@ export default class CodexPanelPlugin extends Plugin { refreshOpenThreadLists(): void { for (const leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_CODEX_PANEL)) { - if (leaf.view instanceof CodexPanelView) { + if (leaf.view instanceof CodexChatView) { leaf.view.refreshThreadList(); } } diff --git a/styles.css b/styles.css index b68b307c..a6d65de1 100644 --- a/styles.css +++ b/styles.css @@ -1,6 +1,6 @@ .codex-panel, .codex-panel-settings, -.codex-panel-rewrite-popover { +.codex-panel-selection-rewrite { --codex-panel-top-icon-size: var(--icon-s, 16px); --codex-panel-icon-button-inline-size: calc(var(--codex-panel-top-icon-size) + var(--size-2-3, 6px) * 2); --codex-panel-icon-button-block-size: calc(var(--codex-panel-top-icon-size) + var(--size-2-2, 4px) * 2); @@ -51,7 +51,7 @@ color: var(--text-normal); } -.workspace-leaf-content[data-type="codex-app-server-panel"] .view-content.codex-panel { +.workspace-leaf-content[data-type="codex-panel-view"] .view-content.codex-panel { padding: 0; overflow: hidden; } @@ -696,7 +696,7 @@ .codex-panel__config-title, .codex-panel__approval-details summary, .codex-panel__output summary, -.codex-panel__diff-file summary, +.codex-panel-diff-file summary, .codex-panel__output-title { color: var(--text-muted); font-size: var(--font-ui-small); @@ -704,7 +704,7 @@ .codex-panel__approval-details summary:hover, .codex-panel__output summary:hover, -.codex-panel__diff-file summary:hover { +.codex-panel-diff-file summary:hover { color: var(--text-normal); } @@ -1309,10 +1309,10 @@ } .codex-panel__tool-result > .codex-panel__output, -.codex-panel__tool-result > .codex-panel__diff-file, +.codex-panel__tool-result > .codex-panel-diff-file, .codex-panel__tool-result > .codex-panel__meta-grid, .codex-panel__tool-result-details > .codex-panel__output, -.codex-panel__tool-result-details > .codex-panel__diff-file, +.codex-panel__tool-result-details > .codex-panel-diff-file, .codex-panel__tool-result-details > .codex-panel__meta-grid { margin-left: 0; } @@ -1395,7 +1395,7 @@ } .codex-panel__output, -.codex-panel__diff-file { +.codex-panel-diff-file { margin-top: 8px; } @@ -1404,7 +1404,7 @@ } .codex-panel__output pre, -.codex-panel__diff { +.codex-panel-diff { max-height: 420px; overflow: auto; margin: 6px 0 0; @@ -1422,41 +1422,41 @@ white-space: pre-wrap; } -.codex-panel__diff { +.codex-panel-diff { white-space: pre; } -.codex-panel__diff-line { +.codex-panel-diff__line { display: block; border-left: 2px solid transparent; padding-left: var(--size-2-3, 6px); min-height: 1.35em; } -.codex-panel__diff-line--added { +.codex-panel-diff__line--added { border-left-color: var(--text-success); color: var(--text-normal); } -.codex-panel__diff-line--removed { +.codex-panel-diff__line--removed { border-left-color: var(--text-error); color: var(--text-normal); } -.codex-panel__diff-line--hunk { +.codex-panel-diff__line--hunk { color: var(--text-accent); } -.codex-panel__diff-line--file { +.codex-panel-diff__line--file { color: var(--text-normal); font-weight: var(--font-semibold); } -.codex-panel__diff-line--context { +.codex-panel-diff__line--context { color: var(--text-muted); } -.codex-panel-turn-diff { +.codex-panel-chat-turn-diff { display: flex; flex-direction: column; gap: var(--size-4-3, 12px); @@ -1465,51 +1465,51 @@ overflow: auto; } -.codex-panel-turn-diff__header { +.codex-panel-chat-turn-diff__header { display: flex; align-items: flex-start; gap: var(--size-4-2, 8px); } -.codex-panel-turn-diff__title-block { +.codex-panel-chat-turn-diff__title-block { flex: 1 1 auto; min-width: 0; } -.codex-panel-turn-diff__title { +.codex-panel-chat-turn-diff__title { color: var(--text-normal); font-size: var(--font-ui-medium); font-weight: var(--font-semibold); line-height: var(--line-height-tight); } -.codex-panel-turn-diff__meta, -.codex-panel-turn-diff__files, -.codex-panel-turn-diff__empty { +.codex-panel-chat-turn-diff__meta, +.codex-panel-chat-turn-diff__files, +.codex-panel-chat-turn-diff__empty { color: var(--text-muted); font-size: var(--font-ui-small); } -.codex-panel-turn-diff__copy { +.codex-panel-chat-turn-diff__copy { flex: 0 0 auto; } -.codex-panel-turn-diff__files { +.codex-panel-chat-turn-diff__files { margin: 0; } -.codex-panel-turn-diff__files ul { +.codex-panel-chat-turn-diff__files ul { margin-block: var(--size-2-2, 4px) 0; } -.codex-panel-turn-diff__diff.codex-panel__diff { +.codex-panel-chat-turn-diff__diff.codex-panel-diff { max-height: none; margin: 0; overflow: visible; white-space: pre-wrap; } -.codex-panel-rewrite-popover { +.codex-panel-selection-rewrite { position: fixed; z-index: var(--layer-popover, 30); box-sizing: border-box; @@ -1527,59 +1527,59 @@ color: var(--text-normal); } -.codex-panel-rewrite-popover__prompt-row { +.codex-panel-selection-rewrite__prompt-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--codex-panel-section-gap); align-items: stretch; } -.codex-panel-rewrite-popover__result-row { +.codex-panel-selection-rewrite__result-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--codex-panel-section-gap); align-items: start; } -.codex-panel-rewrite-popover__result-row.is-hidden, -.codex-panel-rewrite-popover__diff:empty { +.codex-panel-selection-rewrite__result-row.is-hidden, +.codex-panel-selection-rewrite__diff:empty { display: none; } -.codex-panel-rewrite-popover__icon-button.is-hidden { +.codex-panel-selection-rewrite__icon-button.is-hidden { display: none; } -.codex-panel-rewrite-popover__status { +.codex-panel-selection-rewrite__status { color: var(--text-muted); font-size: var(--font-ui-small); } -.codex-panel-rewrite-popover__status:empty { +.codex-panel-selection-rewrite__status:empty { display: none; } -.codex-panel-rewrite-popover__status.is-active { +.codex-panel-selection-rewrite__status.is-active { color: var(--text-faint); } -.codex-panel-rewrite-popover__status-dots span { +.codex-panel-selection-rewrite__status-dots span { opacity: 0.35; } -.codex-panel-rewrite-popover__status.is-active .codex-panel-rewrite-popover__status-dots span { +.codex-panel-selection-rewrite__status.is-active .codex-panel-selection-rewrite__status-dots span { animation: codex-panel-reasoning-dot 1.2s infinite ease-in-out; } -.codex-panel-rewrite-popover__status.is-active .codex-panel-rewrite-popover__status-dots span:nth-child(2) { +.codex-panel-selection-rewrite__status.is-active .codex-panel-selection-rewrite__status-dots span:nth-child(2) { animation-delay: 0.18s; } -.codex-panel-rewrite-popover__status.is-active .codex-panel-rewrite-popover__status-dots span:nth-child(3) { +.codex-panel-selection-rewrite__status.is-active .codex-panel-selection-rewrite__status-dots span:nth-child(3) { animation-delay: 0.36s; } -.codex-panel-rewrite-popover__stream-preview { +.codex-panel-selection-rewrite__stream-preview { box-sizing: border-box; max-height: min(180px, 28vh); margin: 0; @@ -1592,11 +1592,11 @@ white-space: pre-wrap; } -.codex-panel-rewrite-popover__stream-preview.is-hidden { +.codex-panel-selection-rewrite__stream-preview.is-hidden { display: none; } -.codex-panel-rewrite-popover__diff-body { +.codex-panel-selection-rewrite__diff-body { box-sizing: border-box; max-height: min(300px, 38vh); width: 100%; @@ -1606,16 +1606,16 @@ white-space: pre-wrap; } -.codex-panel-rewrite-popover__debug { +.codex-panel-selection-rewrite__debug { color: var(--text-muted); font-size: var(--font-ui-small); } -.codex-panel-rewrite-popover__debug.is-hidden { +.codex-panel-selection-rewrite__debug.is-hidden { display: none; } -.codex-panel-rewrite-popover__debug pre { +.codex-panel-selection-rewrite__debug pre { max-height: 160px; overflow: auto; margin: var(--size-2-3, 6px) 0 0; diff --git a/tests/threads/export.test.ts b/tests/domain/threads/export.test.ts similarity index 96% rename from tests/threads/export.test.ts rename to tests/domain/threads/export.test.ts index cae6ea0b..54ef5530 100644 --- a/tests/threads/export.test.ts +++ b/tests/domain/threads/export.test.ts @@ -1,15 +1,15 @@ import { describe, expect, it } from "vitest"; -import type { Thread } from "../../src/generated/app-server/v2/Thread"; -import type { ThreadItem } from "../../src/generated/app-server/v2/ThreadItem"; -import type { Turn } from "../../src/generated/app-server/v2/Turn"; +import type { Thread } from "../../../src/generated/app-server/v2/Thread"; +import type { ThreadItem } from "../../../src/generated/app-server/v2/ThreadItem"; +import type { Turn } from "../../../src/generated/app-server/v2/Turn"; import { exportArchivedThreadMarkdown, markdownFromThread, normalizedArchiveTags, type ArchiveExportAdapter, -} from "../../src/domain/threads/export"; -import { referencedThreadPrompt } from "../../src/domain/threads/reference"; +} from "../../../src/domain/threads/export"; +import { referencedThreadPrompt } from "../../../src/domain/threads/reference"; describe("thread archive export", () => { it("writes frontmatter and readable user/codex turns with turn timestamps", () => { diff --git a/tests/threads/reference.test.ts b/tests/domain/threads/reference.test.ts similarity index 93% rename from tests/threads/reference.test.ts rename to tests/domain/threads/reference.test.ts index 2c5a28fd..07266bf8 100644 --- a/tests/threads/reference.test.ts +++ b/tests/domain/threads/reference.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "vitest"; -import type { Thread } from "../../src/generated/app-server/v2/Thread"; -import type { Turn } from "../../src/generated/app-server/v2/Turn"; -import { referencedThreadDisplayFromPrompt, referencedThreadPrompt, referencedThreadTurns } from "../../src/domain/threads/reference"; +import type { Thread } from "../../../src/generated/app-server/v2/Thread"; +import type { Turn } from "../../../src/generated/app-server/v2/Turn"; +import { referencedThreadDisplayFromPrompt, referencedThreadPrompt, referencedThreadTurns } from "../../../src/domain/threads/reference"; function thread(overrides: Partial = {}): Thread { return { diff --git a/tests/threads/threads.test.ts b/tests/domain/threads/threads.test.ts similarity index 94% rename from tests/threads/threads.test.ts rename to tests/domain/threads/threads.test.ts index bc8c5d2b..1629cfb6 100644 --- a/tests/threads/threads.test.ts +++ b/tests/domain/threads/threads.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import type { Thread } from "../../src/generated/app-server/v2/Thread"; -import { codexPanelDisplayTitle, inheritedForkThreadName, upsertThread } from "../../src/domain/threads/model"; +import type { Thread } from "../../../src/generated/app-server/v2/Thread"; +import { codexPanelDisplayTitle, inheritedForkThreadName, upsertThread } from "../../../src/domain/threads/model"; describe("thread helpers", () => { it("formats Codex panel display titles from the active thread", () => { diff --git a/tests/editor-rewrite/editor-rewrite.test.ts b/tests/editor-rewrite/editor-rewrite.test.ts deleted file mode 100644 index 78fb5ff2..00000000 --- a/tests/editor-rewrite/editor-rewrite.test.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { buildSelectionUnifiedDiff } from "../../src/features/selection-rewrite/diff"; -import { isRewriteActionKey, isRewriteGenerateKey, type RewriteGenerateKeyEvent } from "../../src/features/selection-rewrite/keys"; -import { canApplyRewrite, type RewriteSession } from "../../src/features/selection-rewrite/model"; -import { parseRewriteOutput, rewriteOutputFromTurn, rewriteOutputParseResultFromTurn } from "../../src/features/selection-rewrite/output"; -import { buildRewritePrompt } from "../../src/features/selection-rewrite/prompt"; -import { rewriteRuntime, validatedRewriteRuntime } from "../../src/features/selection-rewrite/runner"; -import type { Model } from "../../src/generated/app-server/v2/Model"; -import type { Turn } from "../../src/generated/app-server/v2/Turn"; - -describe("editor rewrite output", () => { - it("parses valid rewrite JSON", () => { - expect(parseRewriteOutput('{"replacementText":"rewritten"}')).toEqual({ replacementText: "rewritten" }); - }); - - it("rejects invalid rewrite JSON", () => { - expect(parseRewriteOutput("replacementText: rewritten")).toBeNull(); - expect(parseRewriteOutput('{"replacementText":42}')).toBeNull(); - expect(parseRewriteOutput('{"text":"rewritten"}')).toBeNull(); - }); - - it("extracts the final rewrite JSON from a completed turn", () => { - expect( - rewriteOutputFromTurn( - turn([ - { type: "agentMessage", id: "a1", text: '{"replacementText":"first"}', phase: "final_answer", memoryCitation: null }, - { type: "agentMessage", id: "a2", text: '{"replacementText":"final"}', phase: "final_answer", memoryCitation: null }, - ]), - ), - ).toEqual({ replacementText: "final" }); - }); - - it("keeps raw model text when rewrite output parsing fails", () => { - expect( - rewriteOutputParseResultFromTurn( - turn([{ type: "agentMessage", id: "a1", text: "replacementText: final", phase: "final_answer", memoryCitation: null }]), - ), - ).toEqual({ output: null, rawText: "replacementText: final" }); - }); -}); - -describe("editor rewrite prompt", () => { - it("always includes note context with the selected text", () => { - const prompt = buildRewritePrompt(session()); - - expect(prompt).toContain("Context mode: Selection + note context"); - expect(prompt).toContain("Current note context:"); - expect(prompt).toContain("# Heading"); - }); - - it("uses fences that cannot be closed by note code blocks", () => { - const prompt = buildRewritePrompt( - session({ - originalText: "```ts\nconst value = 1;\n```", - noteText: "````markdown\n```ts\nconst value = 1;\n```\n````", - }), - ); - - expect(prompt).toContain("````text\n```ts"); - expect(prompt).toContain("`````text\n````markdown"); - }); -}); - -describe("editor rewrite diff", () => { - it("renders replacements in a selection-scoped unified diff", () => { - const diff = buildSelectionUnifiedDiff("Note.md", "alpha\nbeta", "alpha\ngamma"); - - expect(diff).toContain("diff --git a/Note.md b/Note.md"); - expect(diff).toContain("@@ -1,2 +1,2 @@"); - expect(diff).toContain(" alpha"); - expect(diff).toContain("-beta"); - expect(diff).toContain("+gamma"); - }); - - it("renders additions and deletions", () => { - expect(buildSelectionUnifiedDiff("Note.md", "", "added")).toContain("+added"); - expect(buildSelectionUnifiedDiff("Note.md", "removed", "")).toContain("-removed"); - }); - - it("renders unchanged text as context", () => { - expect(buildSelectionUnifiedDiff("Note.md", "same", "same")).toContain(" same"); - }); -}); - -describe("editor rewrite apply guard", () => { - it("allows apply only when the current range still matches the original text", () => { - expect(canApplyRewrite("original", "original")).toBe(true); - expect(canApplyRewrite("changed", "original")).toBe(false); - }); -}); - -describe("editor rewrite runtime", () => { - it("uses explicit rewrite runtime settings", () => { - expect(rewriteRuntime({ rewriteSelectionModel: "gpt-5.4-mini", rewriteSelectionEffort: "minimal" })).toEqual({ - model: "gpt-5.4-mini", - effort: "minimal", - }); - }); - - it("omits rewrite runtime overrides that are set to Codex default", () => { - expect(rewriteRuntime({ rewriteSelectionModel: null, rewriteSelectionEffort: null })).toEqual({}); - }); - - it("omits an explicit rewrite effort when the selected model does not support it", () => { - expect( - validatedRewriteRuntime({ rewriteSelectionModel: "gpt-5.4-mini", rewriteSelectionEffort: "minimal" }, [ - model("gpt-5.4-mini", ["low", "medium", "high", "xhigh"]), - ]), - ).toEqual({ model: "gpt-5.4-mini" }); - }); -}); - -describe("editor rewrite keys", () => { - const baseEvent: RewriteGenerateKeyEvent = { - key: "Enter", - shiftKey: false, - metaKey: false, - ctrlKey: false, - altKey: false, - isComposing: false, - }; - - it("generates on plain Enter in Enter mode", () => { - expect(isRewriteGenerateKey(baseEvent, "enter")).toBe(true); - expect(isRewriteGenerateKey({ ...baseEvent, shiftKey: true }, "enter")).toBe(false); - expect(isRewriteGenerateKey({ ...baseEvent, metaKey: true }, "enter")).toBe(false); - }); - - it("generates on Cmd/Ctrl+Enter in mod-enter mode", () => { - expect(isRewriteGenerateKey({ ...baseEvent, metaKey: true }, "mod-enter")).toBe(true); - expect(isRewriteGenerateKey({ ...baseEvent, ctrlKey: true }, "mod-enter")).toBe(true); - expect(isRewriteGenerateKey(baseEvent, "mod-enter")).toBe(false); - }); - - it("treats plain Enter and Cmd/Ctrl+Enter as preview action keys", () => { - expect(isRewriteActionKey(baseEvent)).toBe(true); - expect(isRewriteActionKey({ ...baseEvent, metaKey: true })).toBe(true); - expect(isRewriteActionKey({ ...baseEvent, ctrlKey: true })).toBe(true); - expect(isRewriteActionKey({ ...baseEvent, shiftKey: true })).toBe(false); - expect(isRewriteActionKey({ ...baseEvent, isComposing: true })).toBe(false); - }); -}); - -function session(overrides: Partial = {}): RewriteSession { - return { - filePath: "Note.md", - targetRange: { - from: { line: 1, ch: 0 }, - to: { line: 1, ch: 22 }, - }, - originalText: "Rewrite this sentence.", - noteText: "# Heading\n\nRewrite this sentence.\n\nNext paragraph.", - instruction: "Make it clearer.", - status: "editing-prompt", - streamText: "", - replacementText: null, - debugText: null, - ...overrides, - }; -} - -function turn(items: Turn["items"], overrides: Partial = {}): Turn { - return { - id: "turn", - items, - itemsView: "full", - status: "completed", - error: null, - startedAt: null, - completedAt: null, - durationMs: null, - ...overrides, - }; -} - -function model(name: string, efforts: Model["supportedReasoningEfforts"][number]["reasoningEffort"][]): Model { - return { - id: name, - model: name, - upgrade: null, - upgradeInfo: null, - availabilityNux: null, - displayName: name, - description: "", - hidden: false, - supportedReasoningEfforts: efforts.map((reasoningEffort) => ({ reasoningEffort, description: "" })), - defaultReasoningEffort: efforts[0] ?? "low", - inputModalities: ["text"], - supportsPersonality: false, - additionalSpeedTiers: [], - serviceTiers: [], - defaultServiceTier: null, - isDefault: false, - }; -} diff --git a/tests/app-server/app-server-logs.test.ts b/tests/features/chat/app-server-logs.test.ts similarity index 95% rename from tests/app-server/app-server-logs.test.ts rename to tests/features/chat/app-server-logs.test.ts index ac0f3866..616c2709 100644 --- a/tests/app-server/app-server-logs.test.ts +++ b/tests/features/chat/app-server-logs.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { classifyAppServerLog } from "../../src/features/chat/app-server-logs"; +import { classifyAppServerLog } from "../../../src/features/chat/app-server-logs"; describe("app-server log classification", () => { it("suppresses raw MCP token refresh stderr", () => { diff --git a/tests/approvals/approvals.test.ts b/tests/features/chat/approvals/approvals.test.ts similarity index 96% rename from tests/approvals/approvals.test.ts rename to tests/features/chat/approvals/approvals.test.ts index c40105ef..b1065740 100644 --- a/tests/approvals/approvals.test.ts +++ b/tests/features/chat/approvals/approvals.test.ts @@ -7,9 +7,9 @@ import { approvalSummary, approvalTitle, toPendingApproval, -} from "../../src/features/chat/approvals/model"; -import type { ServerRequest } from "../../src/generated/app-server/ServerRequest"; -import type { CommandExecutionApprovalDecision } from "../../src/generated/app-server/v2/CommandExecutionApprovalDecision"; +} from "../../../../src/features/chat/approvals/model"; +import type { ServerRequest } from "../../../../src/generated/app-server/ServerRequest"; +import type { CommandExecutionApprovalDecision } from "../../../../src/generated/app-server/v2/CommandExecutionApprovalDecision"; function expectPresent(value: T | null | undefined): T { if (value === null || value === undefined) throw new Error("Expected value to be present"); diff --git a/tests/panel/panel-controller.test.ts b/tests/features/chat/chat-controller.test.ts similarity index 95% rename from tests/panel/panel-controller.test.ts rename to tests/features/chat/chat-controller.test.ts index 7dc9334d..4f7b00d1 100644 --- a/tests/panel/panel-controller.test.ts +++ b/tests/features/chat/chat-controller.test.ts @@ -1,18 +1,18 @@ import { describe, expect, it, vi } from "vitest"; -import { PanelController } from "../../src/features/chat/controller"; -import { attachHookRunsToTurn } from "../../src/features/chat/hook-display"; -import { createPanelState } from "../../src/features/chat/state"; -import type { ServerNotification } from "../../src/generated/app-server/ServerNotification"; -import type { ServerRequest } from "../../src/generated/app-server/ServerRequest"; -import type { Thread } from "../../src/generated/app-server/v2/Thread"; -import type { Turn } from "../../src/generated/app-server/v2/Turn"; +import { ChatController } from "../../../src/features/chat/chat-controller"; +import { attachHookRunsToTurn } from "../../../src/features/chat/hook-display"; +import { createChatState } from "../../../src/features/chat/chat-state"; +import type { ServerNotification } from "../../../src/generated/app-server/ServerNotification"; +import type { ServerRequest } from "../../../src/generated/app-server/ServerRequest"; +import type { Thread } from "../../../src/generated/app-server/v2/Thread"; +import type { Turn } from "../../../src/generated/app-server/v2/Turn"; function controllerForState( - state = createPanelState(), - actions: Partial[1]> = {}, -): PanelController { - return new PanelController(state, { + state = createChatState(), + actions: Partial[1]> = {}, +): ChatController { + return new ChatController(state, { refreshThreads: vi.fn(), refreshSkills: vi.fn(), maybeNameThread: vi.fn(), @@ -28,10 +28,10 @@ function expectPresent(value: T | null | undefined): T { return value; } -describe("PanelController", () => { +describe("ChatController", () => { describe("active turn routing", () => { it("applies matching streaming deltas as assistant markdown", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeTurnId = "turn-active"; const controller = controllerForState(state); @@ -45,7 +45,7 @@ describe("PanelController", () => { }); it("marks active reasoning completed when assistant text starts", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeTurnId = "turn-active"; state.busy = true; @@ -66,7 +66,7 @@ describe("PanelController", () => { }); it("streams plan deltas as plain assistant text until completion", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeTurnId = "turn-active"; const controller = controllerForState(state); @@ -80,7 +80,7 @@ describe("PanelController", () => { }); it("updates structured turn plan progress", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeTurnId = "turn-active"; const controller = controllerForState(state); @@ -110,7 +110,7 @@ describe("PanelController", () => { describe("app-server source of truth updates", () => { it("refreshes skills from disk when app-server reports skill changes", () => { const refreshSkills = vi.fn(); - const controller = controllerForState(createPanelState(), { refreshSkills }); + const controller = controllerForState(createChatState(), { refreshSkills }); controller.handleNotification({ method: "skills/changed", @@ -121,7 +121,7 @@ describe("PanelController", () => { }); it("stores the latest aggregated turn diff for the active turn", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeTurnId = "turn-active"; const controller = controllerForState(state); @@ -139,7 +139,7 @@ describe("PanelController", () => { }); it("ignores aggregated turn diffs outside the active scope", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeTurnId = "turn-active"; const controller = controllerForState(state); @@ -157,7 +157,7 @@ describe("PanelController", () => { }); it("formats hook runs as compact summaries with details", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeTurnId = "turn-active"; const controller = controllerForState(state); @@ -210,7 +210,7 @@ describe("PanelController", () => { }); it("omits hook duration details while duration is unavailable", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeTurnId = "turn-active"; const controller = controllerForState(state); @@ -253,7 +253,7 @@ describe("PanelController", () => { }); it("attaches unscoped hook runs to the active turn while streaming", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeTurnId = "turn-active"; state.busy = true; @@ -287,7 +287,7 @@ describe("PanelController", () => { }); it("leaves non-prompt unscoped hook runs outside the active turn", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeTurnId = "turn-active"; state.busy = true; @@ -322,7 +322,7 @@ describe("PanelController", () => { }); it("keeps repeated hook runs with the same run id as separate display items", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeTurnId = "turn-active"; const controller = controllerForState(state); @@ -356,7 +356,7 @@ describe("PanelController", () => { }); it("attaches pre-turn prompt submit hook runs when the turn starts", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.pendingTurnStart = { anchorItemId: "local-user-1", promptSubmitHookItemIds: ["hook-hook-1-1"] }; state.displayItems = [ @@ -426,7 +426,7 @@ describe("PanelController", () => { }); it("captures only prompt-submit hooks observed during the pending turn start", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.pendingTurnStart = { anchorItemId: "local-user-1", promptSubmitHookItemIds: [] }; const controller = controllerForState(state); @@ -461,7 +461,7 @@ describe("PanelController", () => { }); it("keeps pre-turn prompt submit hooks through turn start and completed-turn reconciliation", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.pendingTurnStart = { anchorItemId: "local-user-1", promptSubmitHookItemIds: [] }; state.displayItems = [{ id: "local-user-1", kind: "message", role: "user", text: "hello", markdown: true }]; @@ -530,7 +530,7 @@ describe("PanelController", () => { }); it("stores account rate limit updates outside thread scope", () => { - const state = createPanelState(); + const state = createChatState(); const controller = controllerForState(state); controller.handleNotification({ @@ -555,7 +555,7 @@ describe("PanelController", () => { }); it("records MCP startup status for diagnostics without a chat system message", () => { - const state = createPanelState(); + const state = createChatState(); const recordMcpStartupStatus = vi.fn(); const controller = controllerForState(state, { recordMcpStartupStatus }); @@ -575,7 +575,7 @@ describe("PanelController", () => { describe("interactive server requests", () => { it("queues and resolves requestUserInput server requests", () => { - const state = createPanelState(); + const state = createChatState(); const respondToServerRequest = vi.fn(() => true); const controller = controllerForState(state, { respondToServerRequest }); @@ -613,7 +613,7 @@ describe("PanelController", () => { }); it("rejects cancelled requestUserInput server requests", () => { - const state = createPanelState(); + const state = createChatState(); const rejectServerRequest = vi.fn(() => true); const controller = controllerForState(state, { rejectServerRequest }); @@ -640,7 +640,7 @@ describe("PanelController", () => { }); it("records manual permission approvals as colored result items", () => { - const state = createPanelState(); + const state = createChatState(); const respondToServerRequest = vi.fn(() => true); const controller = controllerForState(state, { respondToServerRequest }); @@ -674,7 +674,7 @@ describe("PanelController", () => { }); it("handles known server request families and rejects unsupported requests by default", () => { - const state = createPanelState(); + const state = createChatState(); const rejectServerRequest = vi.fn(() => true); const controller = controllerForState(state, { rejectServerRequest }); @@ -718,7 +718,7 @@ describe("PanelController", () => { }); it("rejects server requests scoped to a different active thread or turn", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeTurnId = "turn-active"; const rejectServerRequest = vi.fn(() => true); @@ -762,7 +762,7 @@ describe("PanelController", () => { }); it("keeps pending requests when response delivery fails", () => { - const state = createPanelState(); + const state = createChatState(); const respondToServerRequest = vi.fn(() => false); const controller = controllerForState(state, { respondToServerRequest }); @@ -776,7 +776,7 @@ describe("PanelController", () => { }); it("clears pending request state when app-server resolves a request", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.approvals = [ { @@ -816,7 +816,7 @@ describe("PanelController", () => { describe("thread lifecycle and reconciliation", () => { it("keeps user-visible app-server notices in the message stream", () => { - const state = createPanelState(); + const state = createChatState(); const controller = controllerForState(state); controller.handleNotification({ @@ -833,7 +833,7 @@ describe("PanelController", () => { }); it("clears all active-thread scoped state when the active thread is archived", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeTurnId = "turn-active"; state.activeModel = "gpt-5.5"; @@ -898,7 +898,7 @@ describe("PanelController", () => { }); it("does not replace the active cwd from unrelated thread-started notifications", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeThreadCwd = "/workspace/active"; const controller = controllerForState(state); @@ -912,7 +912,7 @@ describe("PanelController", () => { }); it("records cwd from active thread-started notifications", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; const controller = controllerForState(state); @@ -925,7 +925,7 @@ describe("PanelController", () => { }); it("replaces optimistic user echoes when completed turns are reconciled", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeTurnId = "turn-active"; state.displayItems = [ @@ -970,7 +970,7 @@ describe("PanelController", () => { }); it("asks the view to auto-name completed turns", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeTurnId = "turn-active"; const maybeNameThread = vi.fn(); @@ -998,7 +998,7 @@ describe("PanelController", () => { }); it("updates listed thread names from thread name notifications", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.listedThreads = [thread("thread-active", "/workspace/active")]; const refreshThreads = vi.fn(); @@ -1014,7 +1014,7 @@ describe("PanelController", () => { }); it("syncs active runtime state from thread settings notifications", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; const controller = controllerForState(state); @@ -1050,7 +1050,7 @@ describe("PanelController", () => { }); it("syncs null service tier from settings notifications", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeServiceTier = "standard"; const controller = controllerForState(state); @@ -1114,7 +1114,7 @@ describe("PanelController", () => { ]; for (const { notification, text } of cases) { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; const controller = controllerForState(state); @@ -1127,7 +1127,7 @@ describe("PanelController", () => { describe("auto-review display", () => { it("renders guardian warnings as review results instead of system messages", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; const controller = controllerForState(state); @@ -1147,7 +1147,7 @@ describe("PanelController", () => { }); it("renders auto approval review notifications as upserted review results", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeTurnId = "turn-active"; const controller = controllerForState(state); @@ -1194,7 +1194,7 @@ describe("PanelController", () => { }); it("replaces guardian auto-review warnings when structured auto-review notifications arrive", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeTurnId = "turn-active"; const controller = controllerForState(state); @@ -1228,7 +1228,7 @@ describe("PanelController", () => { }); it("ignores guardian auto-review warnings after structured auto-review notifications", () => { - const state = createPanelState(); + const state = createChatState(); state.activeThreadId = "thread-active"; state.activeTurnId = "turn-active"; const controller = controllerForState(state); diff --git a/tests/composer/composer-keys.test.ts b/tests/features/chat/composer/composer-keys.test.ts similarity index 96% rename from tests/composer/composer-keys.test.ts rename to tests/features/chat/composer/composer-keys.test.ts index 1a47ec9f..e6711cd3 100644 --- a/tests/composer/composer-keys.test.ts +++ b/tests/features/chat/composer/composer-keys.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { isComposerSendKey, type ComposerSendKeyEvent } from "../../src/features/chat/composer/keys"; +import { isComposerSendKey, type ComposerSendKeyEvent } from "../../../../src/features/chat/composer/keys"; const baseEvent: ComposerSendKeyEvent = { key: "Enter", diff --git a/tests/composer/composer-suggestions.test.ts b/tests/features/chat/composer/composer-suggestions.test.ts similarity index 97% rename from tests/composer/composer-suggestions.test.ts rename to tests/features/chat/composer/composer-suggestions.test.ts index c41c3786..3486be9e 100644 --- a/tests/composer/composer-suggestions.test.ts +++ b/tests/features/chat/composer/composer-suggestions.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "vitest"; -import type { ReasoningEffort } from "../../src/generated/app-server/ReasoningEffort"; -import type { Model } from "../../src/generated/app-server/v2/Model"; -import type { Thread } from "../../src/generated/app-server/v2/Thread"; +import type { ReasoningEffort } from "../../../../src/generated/app-server/ReasoningEffort"; +import type { Model } from "../../../../src/generated/app-server/v2/Model"; +import type { Thread } from "../../../../src/generated/app-server/v2/Thread"; import { activeComposerSuggestions, applyComposerSuggestionInsertion, @@ -11,7 +11,7 @@ import { findWikiLinkSuggestions, nextComposerSuggestionIndex, parseSlashCommand, -} from "../../src/features/chat/composer/suggestions"; +} from "../../../../src/features/chat/composer/suggestions"; function expectPresent(value: T | null | undefined): T { if (value === null || value === undefined) throw new Error("Expected value to be present"); diff --git a/tests/composer/obsidian-context.test.ts b/tests/features/chat/composer/obsidian-context.test.ts similarity index 96% rename from tests/composer/obsidian-context.test.ts rename to tests/features/chat/composer/obsidian-context.test.ts index 7c21e124..099fc5c8 100644 --- a/tests/composer/obsidian-context.test.ts +++ b/tests/features/chat/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/composer/obsidian-context"; +import { noteCandidates, resolveWikiLinkMention } from "../../../../src/features/chat/composer/obsidian-context"; describe("Obsidian composer context", () => { it("builds note candidates from markdown files", () => { diff --git a/tests/composer/slash-commands.test.ts b/tests/features/chat/composer/slash-commands.test.ts similarity index 98% rename from tests/composer/slash-commands.test.ts rename to tests/features/chat/composer/slash-commands.test.ts index 169425e4..b6f34633 100644 --- a/tests/composer/slash-commands.test.ts +++ b/tests/features/chat/composer/slash-commands.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi } from "vitest"; -import { slashCommandHelpLines, slashCommandHelpRows } from "../../src/features/chat/composer/slash-commands"; -import type { Thread } from "../../src/generated/app-server/v2/Thread"; -import { executeSlashCommand, type SlashCommandExecutionContext } from "../../src/features/chat/slash-commands"; +import { slashCommandHelpLines, slashCommandHelpRows } from "../../../../src/features/chat/composer/slash-commands"; +import type { Thread } from "../../../../src/generated/app-server/v2/Thread"; +import { executeSlashCommand, type SlashCommandExecutionContext } from "../../../../src/features/chat/slash-commands"; function context(overrides: Partial = {}): SlashCommandExecutionContext { return { diff --git a/tests/composer/wikilink-context.test.ts b/tests/features/chat/composer/wikilink-context.test.ts similarity index 98% rename from tests/composer/wikilink-context.test.ts rename to tests/features/chat/composer/wikilink-context.test.ts index d2c81993..f1d065c6 100644 --- a/tests/composer/wikilink-context.test.ts +++ b/tests/features/chat/composer/wikilink-context.test.ts @@ -5,7 +5,7 @@ import { parsedWikiLinks, userInputWithWikiLinkMentions, userInputWithWikiLinkMentionsAndSkills, -} from "../../src/features/chat/composer/wikilink-context"; +} from "../../../../src/features/chat/composer/wikilink-context"; describe("wikilink context", () => { it("parses aliases, subpaths, and duplicate links", () => { diff --git a/tests/panel/diagnostics.test.ts b/tests/features/chat/diagnostics.test.ts similarity index 97% rename from tests/panel/diagnostics.test.ts rename to tests/features/chat/diagnostics.test.ts index db5e9be4..fa513cde 100644 --- a/tests/panel/diagnostics.test.ts +++ b/tests/features/chat/diagnostics.test.ts @@ -5,8 +5,8 @@ import { capabilityProbeOk, createAppServerDiagnostics, upsertMcpServerDiagnostic, -} from "../../src/app-server/compatibility"; -import { connectionDiagnosticSections, diagnosticAlertLevel } from "../../src/features/chat/diagnostics"; +} from "../../../src/app-server/compatibility"; +import { connectionDiagnosticSections, diagnosticAlertLevel } from "../../../src/features/chat/diagnostics"; describe("connection diagnostics", () => { it("formats base rows, capability probes, and MCP issues for /doctor", () => { diff --git a/tests/display/display-model.test.ts b/tests/features/chat/display/display-model.test.ts similarity index 98% rename from tests/display/display-model.test.ts rename to tests/features/chat/display/display-model.test.ts index 13de510c..3411a566 100644 --- a/tests/display/display-model.test.ts +++ b/tests/features/chat/display/display-model.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import { activeAgentRunSummary } from "../../src/features/chat/display/agent"; -import { displayBlocksForItems } from "../../src/features/chat/display/blocks"; +import { activeAgentRunSummary } from "../../../../src/features/chat/display/agent"; +import { displayBlocksForItems } from "../../../../src/features/chat/display/blocks"; import { appendAssistantDelta, appendItemOutput, @@ -9,15 +9,15 @@ import { appendPlanDelta, appendToolOutput, upsertDisplayItem, -} from "../../src/features/chat/display/stream-updates"; -import { normalizeProposedPlanMarkdown, planProgressDisplayItem } from "../../src/features/chat/display/plan"; -import { pathRelativeToRoot } from "../../src/features/chat/display/paths"; -import { createAutoReviewResultItem, createReviewResultItem } from "../../src/features/chat/display/review"; -import { executionState } from "../../src/features/chat/display/state"; -import { displayItemFromThreadItem, displayItemsFromTurns } from "../../src/features/chat/display/thread-items"; -import type { DisplayItem } from "../../src/features/chat/display/types"; -import type { ThreadItem } from "../../src/generated/app-server/v2/ThreadItem"; -import type { Turn } from "../../src/generated/app-server/v2/Turn"; +} from "../../../../src/features/chat/display/stream-updates"; +import { normalizeProposedPlanMarkdown, planProgressDisplayItem } from "../../../../src/features/chat/display/plan"; +import { pathRelativeToRoot } from "../../../../src/features/chat/display/paths"; +import { createAutoReviewResultItem, createReviewResultItem } from "../../../../src/features/chat/display/review"; +import { executionState } from "../../../../src/features/chat/display/state"; +import { displayItemFromThreadItem, displayItemsFromTurns } from "../../../../src/features/chat/display/thread-items"; +import type { DisplayItem } from "../../../../src/features/chat/display/types"; +import type { ThreadItem } from "../../../../src/generated/app-server/v2/ThreadItem"; +import type { Turn } from "../../../../src/generated/app-server/v2/Turn"; function expectPresent(value: T | null | undefined): T { if (value === null || value === undefined) throw new Error("Expected value to be present"); diff --git a/tests/panel/panel-inbound-routing.test.ts b/tests/features/chat/inbound-routing.test.ts similarity index 96% rename from tests/panel/panel-inbound-routing.test.ts rename to tests/features/chat/inbound-routing.test.ts index a5f96a08..461c146b 100644 --- a/tests/panel/panel-inbound-routing.test.ts +++ b/tests/features/chat/inbound-routing.test.ts @@ -6,13 +6,13 @@ import { messageTurnId, routeServerNotification, routeServerRequest, -} from "../../src/features/chat/inbound-routing"; -import type { ServerNotification } from "../../src/generated/app-server/ServerNotification"; -import type { ServerRequest } from "../../src/generated/app-server/ServerRequest"; +} from "../../../src/features/chat/inbound-routing"; +import type { ServerNotification } from "../../../src/generated/app-server/ServerNotification"; +import type { ServerRequest } from "../../../src/generated/app-server/ServerRequest"; const activeScope = { activeThreadId: "thread-active", activeTurnId: "turn-active" }; -describe("panel inbound routing", () => { +describe("chat inbound routing", () => { it("extracts thread and turn ids from app-server messages", () => { const notification = { method: "turn/completed", diff --git a/tests/panel/markdown-file-links.test.ts b/tests/features/chat/markdown-file-links.test.ts similarity index 96% rename from tests/panel/markdown-file-links.test.ts rename to tests/features/chat/markdown-file-links.test.ts index ae5efb8e..6457cff2 100644 --- a/tests/panel/markdown-file-links.test.ts +++ b/tests/features/chat/markdown-file-links.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { TFile, type App } from "obsidian"; -import { markdownFileLinkTarget } from "../../src/features/chat/markdown-file-links"; +import { markdownFileLinkTarget } from "../../../src/features/chat/markdown-file-links"; describe("markdown file links", () => { it("resolves absolute vault paths", () => { diff --git a/tests/panel/mcp-status.test.ts b/tests/features/chat/mcp-status.test.ts similarity index 88% rename from tests/panel/mcp-status.test.ts rename to tests/features/chat/mcp-status.test.ts index 38353992..0a2d486c 100644 --- a/tests/panel/mcp-status.test.ts +++ b/tests/features/chat/mcp-status.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import type { McpServerStatus } from "../../src/generated/app-server/v2/McpServerStatus"; -import { mcpStatusLines } from "../../src/features/chat/mcp-status"; +import type { McpServerStatus } from "../../../src/generated/app-server/v2/McpServerStatus"; +import { mcpStatusLines } from "../../../src/features/chat/mcp-status"; function mcpServer(overrides: Partial = {}): McpServerStatus { return { diff --git a/tests/panel/rollback.test.ts b/tests/features/chat/rollback.test.ts similarity index 93% rename from tests/panel/rollback.test.ts rename to tests/features/chat/rollback.test.ts index 71b1b0bd..73d1234c 100644 --- a/tests/panel/rollback.test.ts +++ b/tests/features/chat/rollback.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import { isRollbackCandidateItem, rollbackCandidateFromItems } from "../../src/features/chat/rollback"; -import type { DisplayItem } from "../../src/features/chat/display/types"; +import { isRollbackCandidateItem, rollbackCandidateFromItems } from "../../../src/features/chat/rollback"; +import type { DisplayItem } from "../../../src/features/chat/display/types"; function expectPresent(value: T | null | undefined): T { if (value === null || value === undefined) throw new Error("Expected value to be present"); diff --git a/tests/panel/status-lines.test.ts b/tests/features/chat/status-lines.test.ts similarity index 86% rename from tests/panel/status-lines.test.ts rename to tests/features/chat/status-lines.test.ts index 686aa5e2..aee59df8 100644 --- a/tests/panel/status-lines.test.ts +++ b/tests/features/chat/status-lines.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import { statusValue, usageLimitStatusLines } from "../../src/features/chat/status-lines"; -import type { RateLimitSummary } from "../../src/runtime/view"; +import { statusValue, usageLimitStatusLines } from "../../../src/features/chat/status-lines"; +import type { RateLimitSummary } from "../../../src/runtime/view"; describe("status line helpers", () => { it("formats primitive and structured diagnostic values", () => { diff --git a/tests/panel/thread-naming.test.ts b/tests/features/chat/thread-naming.test.ts similarity index 97% rename from tests/panel/thread-naming.test.ts rename to tests/features/chat/thread-naming.test.ts index bb86b070..b5038a78 100644 --- a/tests/panel/thread-naming.test.ts +++ b/tests/features/chat/thread-naming.test.ts @@ -10,9 +10,9 @@ import { normalizeGeneratedTitle, titleFromNamingTurn, validatedNamingRuntime, -} from "../../src/features/chat/thread-naming"; -import type { Model } from "../../src/generated/app-server/v2/Model"; -import type { Turn } from "../../src/generated/app-server/v2/Turn"; +} from "../../../src/features/chat/thread-naming"; +import type { Model } from "../../../src/generated/app-server/v2/Model"; +import type { Turn } from "../../../src/generated/app-server/v2/Turn"; describe("thread naming", () => { it("extracts the first user request and final assistant response from a completed turn", () => { diff --git a/tests/ui/dom-test-helpers.ts b/tests/features/chat/ui/dom-test-helpers.ts similarity index 100% rename from tests/ui/dom-test-helpers.ts rename to tests/features/chat/ui/dom-test-helpers.ts diff --git a/tests/ui/view-dom.test.ts b/tests/features/chat/ui/view-dom.test.ts similarity index 93% rename from tests/ui/view-dom.test.ts rename to tests/features/chat/ui/view-dom.test.ts index 3c25f9e8..c5ecbd7d 100644 --- a/tests/ui/view-dom.test.ts +++ b/tests/features/chat/ui/view-dom.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest"; -import { renderTextWithWikiLinks, shortSignature } from "../../src/shared/ui/dom"; +import { renderTextWithWikiLinks, shortSignature } from "../../../../src/shared/ui/dom"; import { installObsidianDomShims } from "./dom-test-helpers"; installObsidianDomShims(); diff --git a/tests/ui/view-renderers.test.ts b/tests/features/chat/ui/view-renderers.test.ts similarity index 97% rename from tests/ui/view-renderers.test.ts rename to tests/features/chat/ui/view-renderers.test.ts index bd765828..4edb87a6 100644 --- a/tests/ui/view-renderers.test.ts +++ b/tests/features/chat/ui/view-renderers.test.ts @@ -2,21 +2,21 @@ import { describe, expect, it, vi } from "vitest"; -import type { PendingApproval } from "../../src/features/chat/approvals/model"; -import type { PendingUserInput } from "../../src/features/chat/user-input/model"; +import type { PendingApproval } from "../../../../src/features/chat/approvals/model"; +import type { PendingUserInput } from "../../../../src/features/chat/user-input/model"; import { renderComposerShell, renderComposerSuggestions, scrollComposerSuggestionIntoView, syncComposerControls, syncComposerHeight, -} from "../../src/features/chat/ui/composer"; -import { renderPendingRequestMessage } from "../../src/features/chat/ui/pending-request-message"; -import { renderToolbar, toolbarSignature, type ToolbarViewModel } from "../../src/features/chat/ui/toolbar"; -import { displayItemSignature } from "../../src/features/chat/display/signature"; -import { implementPlanCandidateFromState } from "../../src/features/chat/message-renderer"; -import { messageRenderBlocks as rawMessageRenderBlocks, syncMessageRenderBlocks } from "../../src/features/chat/ui/message-stream"; -import { displayDiffLines, persistedTurnDiffViewState, renderTurnDiffView } from "../../src/features/chat/ui/turn-diff"; +} from "../../../../src/features/chat/ui/composer"; +import { renderPendingRequestMessage } from "../../../../src/features/chat/ui/pending-request-message"; +import { renderToolbar, toolbarSignature, type ToolbarViewModel } from "../../../../src/features/chat/ui/toolbar"; +import { displayItemSignature } from "../../../../src/features/chat/display/signature"; +import { implementPlanCandidateFromState } from "../../../../src/features/chat/chat-message-renderer"; +import { messageRenderBlocks as rawMessageRenderBlocks, syncMessageRenderBlocks } from "../../../../src/features/chat/ui/message-stream"; +import { displayDiffLines, persistedChatTurnDiffViewState, renderChatTurnDiffView } from "../../../../src/features/chat/ui/turn-diff"; import { composerSuggestionScrollFixture, installObsidianDomShims, topLevelDetailsSummaries } from "./dom-test-helpers"; installObsidianDomShims(); @@ -950,12 +950,12 @@ describe("message stream block identity and message actions", () => { }); }); -describe("turn diff view decisions", () => { +describe("chat turn diff view decisions", () => { it("renders the turn diff view with classified unified diff lines", () => { const parent = document.createElement("div"); const copyDiff = vi.fn(); - renderTurnDiffView( + renderChatTurnDiffView( parent, { threadId: "019e061e-0046-7653-a362-86de9a47cb5c", @@ -967,26 +967,26 @@ describe("turn diff view decisions", () => { { copyDiff }, ); - expect(parent.querySelector(".codex-panel-turn-diff__title")?.textContent).toBe("Turn diff"); - expect(parent.querySelector(".codex-panel-turn-diff__meta")?.textContent).toContain("019e061e"); - expect(parent.querySelector(".codex-panel-turn-diff__files summary")?.textContent).toBe("Changed files"); - expect(parent.querySelector(".codex-panel-turn-diff__files")?.textContent).toContain("src/main.ts"); - expect(parent.querySelector(".codex-panel__diff-line--file")?.textContent).toBe("src/main.ts"); + expect(parent.querySelector(".codex-panel-chat-turn-diff__title")?.textContent).toBe("Turn diff"); + expect(parent.querySelector(".codex-panel-chat-turn-diff__meta")?.textContent).toContain("019e061e"); + expect(parent.querySelector(".codex-panel-chat-turn-diff__files summary")?.textContent).toBe("Changed files"); + expect(parent.querySelector(".codex-panel-chat-turn-diff__files")?.textContent).toContain("src/main.ts"); + expect(parent.querySelector(".codex-panel-diff__line--file")?.textContent).toBe("src/main.ts"); expect(parent.textContent).not.toContain("diff --git"); expect(parent.textContent).not.toContain("+++ b/src/main.ts"); expect(parent.textContent).not.toContain("-old"); expect(parent.textContent).not.toContain("+new"); expect(parent.textContent).toContain("old"); expect(parent.textContent).toContain("new"); - expect(parent.querySelectorAll(".codex-panel__diff-line--hunk")).toHaveLength(1); - expect(parent.querySelectorAll(".codex-panel__diff-line--removed")).toHaveLength(1); - expect(parent.querySelectorAll(".codex-panel__diff-line--added")).toHaveLength(1); - parent.querySelector(".codex-panel-turn-diff__copy")?.click(); + expect(parent.querySelectorAll(".codex-panel-diff__line--hunk")).toHaveLength(1); + expect(parent.querySelectorAll(".codex-panel-diff__line--removed")).toHaveLength(1); + expect(parent.querySelectorAll(".codex-panel-diff__line--added")).toHaveLength(1); + parent.querySelector(".codex-panel-chat-turn-diff__copy")?.click(); expect(copyDiff).toHaveBeenCalled(); }); it("keeps unified diff text out of persisted turn diff view state", () => { - const persisted = persistedTurnDiffViewState({ + const persisted = persistedChatTurnDiffViewState({ threadId: "thread", turnId: "turn", cwd: "/vault/project", @@ -1006,12 +1006,12 @@ describe("turn diff view decisions", () => { it("renders restored turn diff metadata without unavailable diff text", () => { const parent = document.createElement("div"); - renderTurnDiffView(parent, null, {}, { threadId: "thread", turnId: "turn", cwd: "/vault/project", files: ["src/main.ts"] }); + renderChatTurnDiffView(parent, null, {}, { threadId: "thread", turnId: "turn", cwd: "/vault/project", files: ["src/main.ts"] }); - expect(parent.querySelector(".codex-panel-turn-diff__meta")?.textContent).toContain("thread / turn"); + expect(parent.querySelector(".codex-panel-chat-turn-diff__meta")?.textContent).toContain("thread / turn"); expect(parent.textContent).toContain("Turn diff is no longer available."); - expect(parent.querySelector(".codex-panel-turn-diff__copy")).toBeNull(); - expect(parent.querySelector(".codex-panel-turn-diff__diff")).toBeNull(); + expect(parent.querySelector(".codex-panel-chat-turn-diff__copy")).toBeNull(); + expect(parent.querySelector(".codex-panel-chat-turn-diff__diff")).toBeNull(); }); it("simplifies git diff file headers for turn diff display", () => { diff --git a/tests/ui/view-scroll.test.ts b/tests/features/chat/ui/view-scroll.test.ts similarity index 97% rename from tests/ui/view-scroll.test.ts rename to tests/features/chat/ui/view-scroll.test.ts index 868fd6c5..d6cf5dee 100644 --- a/tests/ui/view-scroll.test.ts +++ b/tests/features/chat/ui/view-scroll.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; -import { bottomScrollTop, captureScrollAnchor, isNearScrollBottom, restoreScrollAnchor } from "../../src/features/chat/ui/scroll"; +import { bottomScrollTop, captureScrollAnchor, isNearScrollBottom, restoreScrollAnchor } from "../../../../src/features/chat/ui/scroll"; describe("message scroll helpers", () => { it("detects whether the transcript is pinned near the bottom", () => { diff --git a/tests/user-input/user-input.test.ts b/tests/features/chat/user-input/user-input.test.ts similarity index 91% rename from tests/user-input/user-input.test.ts rename to tests/features/chat/user-input/user-input.test.ts index 3c25d1bc..4a3ee40c 100644 --- a/tests/user-input/user-input.test.ts +++ b/tests/features/chat/user-input/user-input.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "vitest"; -import { questionDefaultAnswer, toPendingUserInput, userInputResponse } from "../../src/features/chat/user-input/model"; -import { pendingRequestsSignature } from "../../src/features/chat/request-state"; -import type { ServerRequest } from "../../src/generated/app-server/ServerRequest"; +import { questionDefaultAnswer, toPendingUserInput, userInputResponse } from "../../../../src/features/chat/user-input/model"; +import { pendingRequestsSignature } from "../../../../src/features/chat/request-state"; +import type { ServerRequest } from "../../../../src/generated/app-server/ServerRequest"; function expectPresent(value: T | null | undefined): T { if (value === null || value === undefined) throw new Error("Expected value to be present"); diff --git a/tests/features/selection-rewrite/selection-rewrite.test.ts b/tests/features/selection-rewrite/selection-rewrite.test.ts new file mode 100644 index 00000000..327d0980 --- /dev/null +++ b/tests/features/selection-rewrite/selection-rewrite.test.ts @@ -0,0 +1,204 @@ +import { describe, expect, it } from "vitest"; + +import { buildSelectionUnifiedDiff } from "../../../src/features/selection-rewrite/diff"; +import { + isSelectionRewriteActionKey, + isSelectionRewriteGenerateKey, + type SelectionRewriteGenerateKeyEvent, +} from "../../../src/features/selection-rewrite/keys"; +import { canApplySelectionRewrite, type SelectionRewriteSession } from "../../../src/features/selection-rewrite/model"; +import { + parseSelectionRewriteOutput, + selectionRewriteOutputFromTurn, + selectionRewriteOutputParseResultFromTurn, +} from "../../../src/features/selection-rewrite/output"; +import { buildSelectionRewritePrompt } from "../../../src/features/selection-rewrite/prompt"; +import { selectionRewriteRuntime, validatedSelectionRewriteRuntime } from "../../../src/features/selection-rewrite/runner"; +import type { Model } from "../../../src/generated/app-server/v2/Model"; +import type { Turn } from "../../../src/generated/app-server/v2/Turn"; + +describe("selection selection rewrite output", () => { + it("parses valid selection rewrite JSON", () => { + expect(parseSelectionRewriteOutput('{"replacementText":"rewritten"}')).toEqual({ replacementText: "rewritten" }); + }); + + it("rejects invalid selection rewrite JSON", () => { + expect(parseSelectionRewriteOutput("replacementText: rewritten")).toBeNull(); + expect(parseSelectionRewriteOutput('{"replacementText":42}')).toBeNull(); + expect(parseSelectionRewriteOutput('{"text":"rewritten"}')).toBeNull(); + }); + + it("extracts the final selection rewrite JSON from a completed turn", () => { + expect( + selectionRewriteOutputFromTurn( + turn([ + { type: "agentMessage", id: "a1", text: '{"replacementText":"first"}', phase: "final_answer", memoryCitation: null }, + { type: "agentMessage", id: "a2", text: '{"replacementText":"final"}', phase: "final_answer", memoryCitation: null }, + ]), + ), + ).toEqual({ replacementText: "final" }); + }); + + it("keeps raw model text when selection rewrite output parsing fails", () => { + expect( + selectionRewriteOutputParseResultFromTurn( + turn([{ type: "agentMessage", id: "a1", text: "replacementText: final", phase: "final_answer", memoryCitation: null }]), + ), + ).toEqual({ output: null, rawText: "replacementText: final" }); + }); +}); + +describe("selection rewrite prompt", () => { + it("always includes note context with the selected text", () => { + const prompt = buildSelectionRewritePrompt(session()); + + expect(prompt).toContain("Context mode: Selection + note context"); + expect(prompt).toContain("Current note context:"); + expect(prompt).toContain("# Heading"); + }); + + it("uses fences that cannot be closed by note code blocks", () => { + const prompt = buildSelectionRewritePrompt( + session({ + originalText: "```ts\nconst value = 1;\n```", + noteText: "````markdown\n```ts\nconst value = 1;\n```\n````", + }), + ); + + expect(prompt).toContain("````text\n```ts"); + expect(prompt).toContain("`````text\n````markdown"); + }); +}); + +describe("selection rewrite diff", () => { + it("renders replacements in a selection-scoped unified diff", () => { + const diff = buildSelectionUnifiedDiff("Note.md", "alpha\nbeta", "alpha\ngamma"); + + expect(diff).toContain("diff --git a/Note.md b/Note.md"); + expect(diff).toContain("@@ -1,2 +1,2 @@"); + expect(diff).toContain(" alpha"); + expect(diff).toContain("-beta"); + expect(diff).toContain("+gamma"); + }); + + it("renders additions and deletions", () => { + expect(buildSelectionUnifiedDiff("Note.md", "", "added")).toContain("+added"); + expect(buildSelectionUnifiedDiff("Note.md", "removed", "")).toContain("-removed"); + }); + + it("renders unchanged text as context", () => { + expect(buildSelectionUnifiedDiff("Note.md", "same", "same")).toContain(" same"); + }); +}); + +describe("selection rewrite apply guard", () => { + it("allows apply only when the current range still matches the original text", () => { + expect(canApplySelectionRewrite("original", "original")).toBe(true); + expect(canApplySelectionRewrite("changed", "original")).toBe(false); + }); +}); + +describe("selection selection rewrite runtime", () => { + it("uses explicit selection rewrite runtime settings", () => { + expect(selectionRewriteRuntime({ rewriteSelectionModel: "gpt-5.4-mini", rewriteSelectionEffort: "minimal" })).toEqual({ + model: "gpt-5.4-mini", + effort: "minimal", + }); + }); + + it("omits selection rewrite runtime overrides that are set to Codex default", () => { + expect(selectionRewriteRuntime({ rewriteSelectionModel: null, rewriteSelectionEffort: null })).toEqual({}); + }); + + it("omits an explicit selection rewrite effort when the selected model does not support it", () => { + expect( + validatedSelectionRewriteRuntime({ rewriteSelectionModel: "gpt-5.4-mini", rewriteSelectionEffort: "minimal" }, [ + model("gpt-5.4-mini", ["low", "medium", "high", "xhigh"]), + ]), + ).toEqual({ model: "gpt-5.4-mini" }); + }); +}); + +describe("selection rewrite keys", () => { + const baseEvent: SelectionRewriteGenerateKeyEvent = { + key: "Enter", + shiftKey: false, + metaKey: false, + ctrlKey: false, + altKey: false, + isComposing: false, + }; + + it("generates on plain Enter in Enter mode", () => { + expect(isSelectionRewriteGenerateKey(baseEvent, "enter")).toBe(true); + expect(isSelectionRewriteGenerateKey({ ...baseEvent, shiftKey: true }, "enter")).toBe(false); + expect(isSelectionRewriteGenerateKey({ ...baseEvent, metaKey: true }, "enter")).toBe(false); + }); + + it("generates on Cmd/Ctrl+Enter in mod-enter mode", () => { + expect(isSelectionRewriteGenerateKey({ ...baseEvent, metaKey: true }, "mod-enter")).toBe(true); + expect(isSelectionRewriteGenerateKey({ ...baseEvent, ctrlKey: true }, "mod-enter")).toBe(true); + expect(isSelectionRewriteGenerateKey(baseEvent, "mod-enter")).toBe(false); + }); + + it("treats plain Enter and Cmd/Ctrl+Enter as preview action keys", () => { + expect(isSelectionRewriteActionKey(baseEvent)).toBe(true); + expect(isSelectionRewriteActionKey({ ...baseEvent, metaKey: true })).toBe(true); + expect(isSelectionRewriteActionKey({ ...baseEvent, ctrlKey: true })).toBe(true); + expect(isSelectionRewriteActionKey({ ...baseEvent, shiftKey: true })).toBe(false); + expect(isSelectionRewriteActionKey({ ...baseEvent, isComposing: true })).toBe(false); + }); +}); + +function session(overrides: Partial = {}): SelectionRewriteSession { + return { + filePath: "Note.md", + targetRange: { + from: { line: 1, ch: 0 }, + to: { line: 1, ch: 22 }, + }, + originalText: "Revise this sentence.", + noteText: "# Heading\n\nRevise this sentence.\n\nNext paragraph.", + instruction: "Make it clearer.", + status: "editing-prompt", + streamText: "", + replacementText: null, + debugText: null, + ...overrides, + }; +} + +function turn(items: Turn["items"], overrides: Partial = {}): Turn { + return { + id: "turn", + items, + itemsView: "full", + status: "completed", + error: null, + startedAt: null, + completedAt: null, + durationMs: null, + ...overrides, + }; +} + +function model(name: string, efforts: Model["supportedReasoningEfforts"][number]["reasoningEffort"][]): Model { + return { + id: name, + model: name, + upgrade: null, + upgradeInfo: null, + availabilityNux: null, + displayName: name, + description: "", + hidden: false, + supportedReasoningEfforts: efforts.map((reasoningEffort) => ({ reasoningEffort, description: "" })), + defaultReasoningEffort: efforts[0] ?? "low", + inputModalities: ["text"], + supportsPersonality: false, + additionalSpeedTiers: [], + serviceTiers: [], + defaultServiceTier: null, + isDefault: false, + }; +}