From ca53d77fedf434317b67f6776c86d28331a1b4c8 Mon Sep 17 00:00:00 2001 From: murashit Date: Sat, 27 Jun 2026 21:49:26 +0900 Subject: [PATCH] Extract turn diff view feature --- biome.jsonc | 1 + docs/development.md | 5 +- src/features/chat/host/contracts.ts | 4 +- .../panel/surface/message-stream-presenter.ts | 4 +- .../surface/message-stream-projection.ts | 4 +- .../chat/ui/message-stream/context.ts | 4 +- src/features/chat/ui/turn-diff/render.dom.tsx | 87 ------------------- .../turn-diff.ts => turn-diff/model.ts} | 10 +-- src/features/turn-diff/render.dom.tsx | 87 +++++++++++++++++++ .../{chat/ui => }/turn-diff/view.obsidian.ts | 31 +++---- src/main.ts | 4 +- src/plugin-runtime.ts | 11 ++- src/styles/00-tokens.css | 2 +- ...rn-diff-view.css => 30-turn-diff-view.css} | 26 +++--- src/styles/order.json | 2 +- .../{chat/ui => turn-diff}/turn-diff.test.ts | 44 +++++----- tests/scripts/grit-policy.test.mjs | 9 ++ tests/styles.test.ts | 2 +- 18 files changed, 171 insertions(+), 166 deletions(-) delete mode 100644 src/features/chat/ui/turn-diff/render.dom.tsx rename src/features/{chat/domain/turn-diff.ts => turn-diff/model.ts} (56%) create mode 100644 src/features/turn-diff/render.dom.tsx rename src/features/{chat/ui => }/turn-diff/view.obsidian.ts (59%) rename src/styles/{30-chat-turn-diff-view.css => 30-turn-diff-view.css} (59%) rename tests/features/{chat/ui => turn-diff}/turn-diff.test.ts (77%) diff --git a/biome.jsonc b/biome.jsonc index 98788190..d999c022 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -155,6 +155,7 @@ "!**/src/features/chat/panel/**", "!**/src/features/chat/ui/**", "!**/src/features/selection-rewrite/*.dom.tsx", + "!**/src/features/turn-diff/*.dom.tsx", "!**/src/features/threads-view/*.dom.tsx", "!**/src/settings/**", "!**/src/shared/ui/**" diff --git a/docs/development.md b/docs/development.md index bfd3b20a..e9066a6f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -38,6 +38,7 @@ The source tree is organized by implementation ownership, not by the single Obsi - `src/main.ts` and `src/plugin-runtime.ts` own Obsidian plugin registration, lifecycle wiring, and cross-surface runtime coordination. - `src/app-server/` owns the app-server boundary through responsibility subfolders such as `connection/`, `protocol/`, `query/`, `routing/`, and `services/`. Do not add root-level app-server modules. - `src/features/` contains feature-owned surfaces and workflows. Feature-to-feature imports are acceptable only when the imported feature owns a concrete capability. +- `src/features/turn-diff/` owns the turn diff Obsidian view, renderer, and persisted view state; chat provides current turn diff state through its workspace contract. - `src/workspace/` and `src/settings/` own Obsidian workspace coordination and settings-tab behavior. - `src/domain/` contains panel-owned, generated-independent meaning models and pure derivations. Domain code must not import app-server protocol, generated bindings, feature modules, UI, or Obsidian APIs. - `src/shared/` contains feature-neutral helpers. Move behavior here only when it is genuinely shared outside one feature. @@ -45,13 +46,13 @@ The source tree is organized by implementation ownership, not by the single Obsi Within `src/features/chat/`: -- `domain/` owns chat-local meaning models and pure derivations for message stream items, pending request display facts, runtime intent, local IDs, and turn diffs. +- `domain/` owns chat-local meaning models and pure derivations for message stream items, pending request display facts, runtime intent, and local IDs. - `application/` owns chat state transitions and workflow orchestration for composer input, turn submission, thread lifecycle, pending requests, runtime settings, connection status, and reducer state. - `app-server/` adapts app-server notifications, requests, thread payloads, diagnostics, and turn items into chat-owned actions, projections, and message stream models. - `host/` wires each chat session to Obsidian views, workspace services, app-server clients, plugin settings, shared runtime state, lifecycle cleanup, and cross-surface handles. - `panel/` owns the Preact shell, shell-state projection, panel controllers, toolbar/composer/message-stream surface adapters, and Obsidian-facing panel snapshots. - `presentation/` owns view-model projection, grouping, formatting, and status text for chat UI surfaces. -- `ui/` owns Preact and explicit DOM bridge rendering pieces for the composer, toolbar, goal view, message stream, and turn diff view. +- `ui/` owns Preact and explicit DOM bridge rendering pieces for the composer, toolbar, goal view, and message stream. Tests should mirror the ownership boundary of the code under test. Keep chat feature tests under `tests/features/chat/app-server/`, `application/`, `domain/`, `host/`, `panel/`, `presentation/`, or `ui/` instead of flattened shortcut folders such as `connection/`, `composer/`, `message-stream/`, `runtime/`, `threads/`, or `conversation/`. Feature-neutral tests belong under `tests/shared/`. diff --git a/src/features/chat/host/contracts.ts b/src/features/chat/host/contracts.ts index 66aca2c9..daee5924 100644 --- a/src/features/chat/host/contracts.ts +++ b/src/features/chat/host/contracts.ts @@ -10,7 +10,7 @@ import type { SharedServerMetadata } from "../../../domain/server/metadata"; import type { ArchiveExportSettings } from "../../../domain/threads/archive-markdown"; import type { ObservedResultListener } from "../../../shared/query/observed-result"; import type { SendShortcut } from "../../../shared/ui/keyboard"; -import type { ChatTurnDiffViewState } from "../domain/turn-diff"; +import type { TurnDiffViewState } from "../../turn-diff/model"; export interface CodexChatHost { readonly settingsRef: ChatPanelSettingsRef; @@ -38,7 +38,7 @@ export interface ChatPanelSettingsAccess { interface WorkspacePanels { openThreadInNewView(threadId: string): Promise; focusThreadInOpenView(threadId: string): Promise; - openTurnDiff(state: ChatTurnDiffViewState): Promise; + openTurnDiff(state: TurnDiffViewState): Promise; refreshThreadsViewLiveState(): void; } diff --git a/src/features/chat/panel/surface/message-stream-presenter.ts b/src/features/chat/panel/surface/message-stream-presenter.ts index 2856ecb9..0c6fe099 100644 --- a/src/features/chat/panel/surface/message-stream-presenter.ts +++ b/src/features/chat/panel/surface/message-stream-presenter.ts @@ -2,10 +2,10 @@ import type { App, Component } from "obsidian"; import type { ComponentChild as UiNode } from "preact"; import { h } from "preact"; import { copyTextWithNotice } from "../../../../shared/ui/clipboard"; +import type { TurnDiffViewState } from "../../../turn-diff/model"; import type { PendingRequestBlockActions } from "../../application/pending-requests/block"; import type { ChatAction } from "../../application/state/root-reducer"; import type { ChatStateStore } from "../../application/state/store"; -import type { ChatTurnDiffViewState } from "../../domain/turn-diff"; import type { MessageStreamScrollControllerBinding } from "../../ui/message-stream/flow-scroll.measure"; import { MarkdownMessageRenderer, renderStreamMarkdown } from "../../ui/message-stream/markdown-renderer.obsidian"; import { MessageStreamViewport, type MessageStreamViewportState } from "../../ui/message-stream/stream-blocks"; @@ -32,7 +32,7 @@ interface ChatMessageStreamActions { rollbackThread: (threadId: string) => void; forkThreadFromTurn: (threadId: string, turnId: string, archiveSource: boolean) => void; implementPlan: (itemId: string) => void; - openTurnDiff: (state: ChatTurnDiffViewState) => void; + openTurnDiff: (state: TurnDiffViewState) => void; } interface ChatMessageStreamRequests { diff --git a/src/features/chat/panel/surface/message-stream-projection.ts b/src/features/chat/panel/surface/message-stream-projection.ts index 9a4fb7d0..369e5f99 100644 --- a/src/features/chat/panel/surface/message-stream-projection.ts +++ b/src/features/chat/panel/surface/message-stream-projection.ts @@ -1,10 +1,10 @@ +import type { TurnDiffViewState } from "../../../turn-diff/model"; import { type PendingRequestBlockActions, pendingRequestBlockStateFromChatState } from "../../application/pending-requests/block"; import type { MessageStreamRollbackCandidate } from "../../application/state/message-stream"; import type { ChatAction } from "../../application/state/root-reducer"; import type { ChatDisclosureBucket, ChatDisclosureUiState } from "../../application/state/ui-state"; import { type ForkCandidate, messageStreamSegmentsEmpty, type PlanImplementationTarget } from "../../domain/message-stream/selectors"; import { pendingRequestsSignature } from "../../domain/pending-requests/signatures"; -import type { ChatTurnDiffViewState } from "../../domain/turn-diff"; import type { MessageStreamTextActionTargets } from "../../presentation/message-stream/text-view"; import { type MessageStreamViewBlock, messageStreamViewBlocks } from "../../presentation/message-stream/view-model"; import { type PendingRequestBlockSnapshot, pendingRequestBlockSnapshotFromState } from "../../presentation/pending-requests/view-model"; @@ -15,7 +15,7 @@ interface ChatMessageStreamActions { rollbackThread: (threadId: string) => void; forkThreadFromTurn: (threadId: string, turnId: string, archiveSource: boolean) => void; implementPlan: (itemId: string) => void; - openTurnDiff: (state: ChatTurnDiffViewState) => void; + openTurnDiff: (state: TurnDiffViewState) => void; } interface ChatMessageStreamRequests { diff --git a/src/features/chat/ui/message-stream/context.ts b/src/features/chat/ui/message-stream/context.ts index 33b8bcae..69e9c06a 100644 --- a/src/features/chat/ui/message-stream/context.ts +++ b/src/features/chat/ui/message-stream/context.ts @@ -1,6 +1,6 @@ import type { ApprovalAction, McpElicitationAction, PendingRequestId } from "../../../../domain/pending-requests/model"; +import type { TurnDiffViewState } from "../../../turn-diff/model"; import type { PlanImplementationTarget } from "../../domain/message-stream/selectors"; -import type { ChatTurnDiffViewState } from "../../domain/turn-diff"; import type { MessageStreamForkTarget } from "../../presentation/message-stream/text-view"; import type { PendingRequestBlockSnapshot } from "../../presentation/pending-requests/view-model"; @@ -53,7 +53,7 @@ export interface TextItemActionContext extends TextItemDetailStateContext { export interface TextItemMetadataContext extends TextItemDetailStateContext { activeThreadId: string | null; workspaceRoot?: string | null; - openTurnDiff?: (state: ChatTurnDiffViewState) => void; + openTurnDiff?: (state: TurnDiffViewState) => void; } interface MessageStreamRenderContext { diff --git a/src/features/chat/ui/turn-diff/render.dom.tsx b/src/features/chat/ui/turn-diff/render.dom.tsx deleted file mode 100644 index 14a2fc10..00000000 --- a/src/features/chat/ui/turn-diff/render.dom.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import type { ComponentChild as UiNode } from "preact"; - -import { shortThreadId } from "../../../../shared/id/thread-id"; -import { IconButton } from "../../../../shared/ui/components.obsidian"; -import { UnifiedDiffView } from "../../../../shared/ui/diff"; -import { renderUiRoot } from "../../../../shared/ui/ui-root.dom"; -import type { ChatTurnDiffViewState, PersistedChatTurnDiffViewState } from "../../domain/turn-diff"; - -export interface ChatTurnDiffViewActions { - copyDiff?: () => void; -} - -export function renderChatTurnDiffView( - parent: HTMLElement, - state: ChatTurnDiffViewState | null, - actions: ChatTurnDiffViewActions = {}, - metadata: PersistedChatTurnDiffViewState | null = null, -): void { - parent.addClass("codex-panel-chat-turn-diff"); - renderUiRoot(parent, ); -} - -function ChatTurnDiffView({ - state, - actions, - metadata, -}: { - state: ChatTurnDiffViewState | null; - actions: ChatTurnDiffViewActions; - metadata: PersistedChatTurnDiffViewState | null; -}): UiNode { - if (!state) { - if (metadata) { - return ( - <> - -
Turn diff is no longer available.
- - ); - } - return
No turn diff selected.
; - } - - return ( - <> - - {state.files.length > 0 ? : null} - - - ); -} - -function TurnDiffHeader({ - state, - copyDiff, -}: { - state: PersistedChatTurnDiffViewState; - copyDiff: ChatTurnDiffViewActions["copyDiff"] | null; -}): UiNode { - return ( -
-
-
Turn diff
-
- {shortThreadId(state.threadId)} / {shortThreadId(state.turnId)} ·{" "} - {state.files.length === 1 ? "Edited 1 file" : `Edited ${String(state.files.length)} files`} -
-
- {copyDiff ? ( - - ) : null} -
- ); -} - -function ChangedFiles({ files }: { files: string[] }): UiNode { - return ( -
- Changed files -
    - {files.map((file) => ( -
  • {file}
  • - ))} -
-
- ); -} diff --git a/src/features/chat/domain/turn-diff.ts b/src/features/turn-diff/model.ts similarity index 56% rename from src/features/chat/domain/turn-diff.ts rename to src/features/turn-diff/model.ts index c4cbe651..ecff6118 100644 --- a/src/features/chat/domain/turn-diff.ts +++ b/src/features/turn-diff/model.ts @@ -1,4 +1,4 @@ -export interface ChatTurnDiffViewState { +export interface TurnDiffViewState { threadId: string; turnId: string; cwd: string | null; @@ -6,9 +6,9 @@ export interface ChatTurnDiffViewState { diff: string; } -export type PersistedChatTurnDiffViewState = Omit; +export type PersistedTurnDiffViewState = Omit; -export function persistedChatTurnDiffViewState(state: ChatTurnDiffViewState): PersistedChatTurnDiffViewState { +export function persistedTurnDiffViewState(state: TurnDiffViewState): PersistedTurnDiffViewState { return { threadId: state.threadId, turnId: state.turnId, @@ -17,9 +17,9 @@ export function persistedChatTurnDiffViewState(state: ChatTurnDiffViewState): Pe }; } -export function isPersistedChatTurnDiffViewState(value: unknown): value is PersistedChatTurnDiffViewState { +export function isPersistedTurnDiffViewState(value: unknown): value is PersistedTurnDiffViewState { 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" && diff --git a/src/features/turn-diff/render.dom.tsx b/src/features/turn-diff/render.dom.tsx new file mode 100644 index 00000000..1842cd12 --- /dev/null +++ b/src/features/turn-diff/render.dom.tsx @@ -0,0 +1,87 @@ +import type { ComponentChild as UiNode } from "preact"; + +import { shortThreadId } from "../../shared/id/thread-id"; +import { IconButton } from "../../shared/ui/components.obsidian"; +import { UnifiedDiffView } from "../../shared/ui/diff"; +import { renderUiRoot } from "../../shared/ui/ui-root.dom"; +import type { PersistedTurnDiffViewState, TurnDiffViewState } from "./model"; + +export interface TurnDiffViewActions { + copyDiff?: () => void; +} + +export function renderTurnDiffView( + parent: HTMLElement, + state: TurnDiffViewState | null, + actions: TurnDiffViewActions = {}, + metadata: PersistedTurnDiffViewState | null = null, +): void { + parent.addClass("codex-panel-turn-diff"); + renderUiRoot(parent, ); +} + +function TurnDiffView({ + state, + actions, + metadata, +}: { + state: TurnDiffViewState | null; + actions: TurnDiffViewActions; + metadata: PersistedTurnDiffViewState | null; +}): UiNode { + if (!state) { + if (metadata) { + return ( + <> + +
Turn diff is no longer available.
+ + ); + } + return
No turn diff selected.
; + } + + return ( + <> + + {state.files.length > 0 ? : null} + + + ); +} + +function TurnDiffHeader({ + state, + copyDiff, +}: { + state: PersistedTurnDiffViewState; + copyDiff: TurnDiffViewActions["copyDiff"] | null; +}): UiNode { + return ( +
+
+
Turn diff
+
+ {shortThreadId(state.threadId)} / {shortThreadId(state.turnId)} ·{" "} + {state.files.length === 1 ? "Edited 1 file" : `Edited ${String(state.files.length)} files`} +
+
+ {copyDiff ? ( + + ) : null} +
+ ); +} + +function ChangedFiles({ files }: { files: string[] }): UiNode { + return ( +
+ Changed files +
    + {files.map((file) => ( +
  • {file}
  • + ))} +
+
+ ); +} diff --git a/src/features/chat/ui/turn-diff/view.obsidian.ts b/src/features/turn-diff/view.obsidian.ts similarity index 59% rename from src/features/chat/ui/turn-diff/view.obsidian.ts rename to src/features/turn-diff/view.obsidian.ts index fb09f53d..a9594387 100644 --- a/src/features/chat/ui/turn-diff/view.obsidian.ts +++ b/src/features/turn-diff/view.obsidian.ts @@ -1,26 +1,21 @@ import { ItemView, type ViewStateResult } from "obsidian"; -import { VIEW_TYPE_CODEX_TURN_DIFF } from "../../../../constants"; -import { copyTextWithNotice } from "../../../../shared/ui/clipboard"; -import { unmountUiRoot } from "../../../../shared/ui/ui-root.dom"; -import { - type ChatTurnDiffViewState, - isPersistedChatTurnDiffViewState, - type PersistedChatTurnDiffViewState, - persistedChatTurnDiffViewState, -} from "../../domain/turn-diff"; -import { renderChatTurnDiffView } from "./render.dom"; +import { VIEW_TYPE_CODEX_TURN_DIFF } from "../../constants"; +import { copyTextWithNotice } from "../../shared/ui/clipboard"; +import { unmountUiRoot } from "../../shared/ui/ui-root.dom"; +import { isPersistedTurnDiffViewState, type PersistedTurnDiffViewState, persistedTurnDiffViewState, type TurnDiffViewState } from "./model"; +import { renderTurnDiffView } from "./render.dom"; -export class CodexChatTurnDiffView extends ItemView { - private metadata: PersistedChatTurnDiffViewState | null = null; - private payload: ChatTurnDiffViewState | null = null; +export class CodexTurnDiffView extends ItemView { + private metadata: PersistedTurnDiffViewState | null = null; + private payload: TurnDiffViewState | null = null; override getViewType(): string { return VIEW_TYPE_CODEX_TURN_DIFF; } override getDisplayText(): string { - return "Codex chat turn diff"; + return "Codex turn diff"; } override getIcon(): string { @@ -33,7 +28,7 @@ export class CodexChatTurnDiffView extends ItemView { override async setState(state: unknown, result: ViewStateResult): Promise { await super.setState(state, result); - this.metadata = isPersistedChatTurnDiffViewState(state) + this.metadata = isPersistedTurnDiffViewState(state) ? { threadId: state.threadId, turnId: state.turnId, @@ -53,15 +48,15 @@ export class CodexChatTurnDiffView extends ItemView { unmountUiRoot(this.contentEl); } - setDiffPayload(payload: ChatTurnDiffViewState): void { - this.metadata = persistedChatTurnDiffViewState(payload); + setDiffPayload(payload: TurnDiffViewState): void { + this.metadata = persistedTurnDiffViewState(payload); this.payload = payload; this.render(); } private render(): void { const root = this.contentEl; - renderChatTurnDiffView( + renderTurnDiffView( root, this.payload, this.payload ? { copyDiff: () => void this.copyDiff(this.payload?.diff ?? "") } : {}, diff --git a/src/main.ts b/src/main.ts index 32dbc58f..a04d952e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,9 +2,9 @@ import { Plugin } from "obsidian"; import { VIEW_TYPE_CODEX_PANEL, VIEW_TYPE_CODEX_THREADS, VIEW_TYPE_CODEX_TURN_DIFF } from "./constants"; import { CodexChatView } from "./features/chat/host/view.obsidian"; -import { CodexChatTurnDiffView } from "./features/chat/ui/turn-diff/view.obsidian"; import { registerSelectionRewriteCommand } from "./features/selection-rewrite/command.obsidian"; import { CodexThreadsView } from "./features/threads-view/view.obsidian"; +import { CodexTurnDiffView } from "./features/turn-diff/view.obsidian"; import { CodexPanelRuntime } from "./plugin-runtime"; import { type CodexPanelSettings, DEFAULT_SETTINGS, getVaultPath, normalizeSettings, settingsMatchStoredSettings } from "./settings/model"; import { CodexPanelSettingTab } from "./settings/tab.obsidian"; @@ -24,7 +24,7 @@ export default class CodexPanelPlugin extends Plugin { await this.loadSettings(); this.registerView(VIEW_TYPE_CODEX_PANEL, (leaf) => new CodexChatView(leaf, this.runtime.chatHost())); - this.registerView(VIEW_TYPE_CODEX_TURN_DIFF, (leaf) => new CodexChatTurnDiffView(leaf)); + this.registerView(VIEW_TYPE_CODEX_TURN_DIFF, (leaf) => new CodexTurnDiffView(leaf)); this.registerView(VIEW_TYPE_CODEX_THREADS, (leaf) => new CodexThreadsView(leaf, this.runtime.threadsHost())); this.registerEvent( this.app.workspace.on("active-leaf-change", (leaf) => { diff --git a/src/plugin-runtime.ts b/src/plugin-runtime.ts index 289f65b4..a1adc436 100644 --- a/src/plugin-runtime.ts +++ b/src/plugin-runtime.ts @@ -8,8 +8,6 @@ import { AppServerSharedQueries } from "./app-server/query/shared-queries"; import { createThreadCatalog, type ThreadCatalog, type ThreadCatalogEvent } from "./app-server/query/thread-catalog"; import { VIEW_TYPE_CODEX_THREADS, VIEW_TYPE_CODEX_TURN_DIFF } from "./constants"; import { hasPendingRequests } from "./domain/pending-requests/aggregate"; -import type { ChatTurnDiffViewState } from "./features/chat/domain/turn-diff"; -import { persistedChatTurnDiffViewState } from "./features/chat/domain/turn-diff"; import type { ChatPanelClientSurface, ChatPanelSettingsAccess, @@ -18,11 +16,12 @@ import type { ChatWorkspacePanelSurface, CodexChatHost, } from "./features/chat/host/contracts"; -import { CodexChatTurnDiffView } from "./features/chat/ui/turn-diff/view.obsidian"; import { openThreadPicker, type ThreadPickerHost } from "./features/thread-picker/modal.obsidian"; import type { ThreadsViewHost, ThreadsViewSettingsAccess } from "./features/threads-view/session"; import type { ThreadsViewPanelActivity } from "./features/threads-view/state"; import { CodexThreadsView } from "./features/threads-view/view.obsidian"; +import { persistedTurnDiffViewState, type TurnDiffViewState } from "./features/turn-diff/model"; +import { CodexTurnDiffView } from "./features/turn-diff/view.obsidian"; import type { CodexPanelSettingTabHost } from "./settings/host"; import type { CodexPanelSettings } from "./settings/model"; import { WorkspacePanelCoordinator } from "./workspace/panel-coordinator"; @@ -200,12 +199,12 @@ export class CodexPanelRuntime implements AppServerClientAccess { return this.runWithAppServerClient(this.appServerQueryContext(), operation, options); } - private async openTurnDiff(state: ChatTurnDiffViewState): Promise { + private async openTurnDiff(state: TurnDiffViewState): Promise { const existing = this.options.app.workspace.getLeavesOfType(VIEW_TYPE_CODEX_TURN_DIFF).at(0); const leaf = existing ?? this.options.app.workspace.getLeaf("tab"); - await leaf.setViewState({ type: VIEW_TYPE_CODEX_TURN_DIFF, active: true, state: { ...persistedChatTurnDiffViewState(state) } }); + await leaf.setViewState({ type: VIEW_TYPE_CODEX_TURN_DIFF, active: true, state: { ...persistedTurnDiffViewState(state) } }); await leaf.loadIfDeferred(); - if (leaf.view instanceof CodexChatTurnDiffView) { + if (leaf.view instanceof CodexTurnDiffView) { leaf.view.setDiffPayload(state); } await this.options.app.workspace.revealLeaf(leaf); diff --git a/src/styles/00-tokens.css b/src/styles/00-tokens.css index ea295ecf..f7383437 100644 --- a/src/styles/00-tokens.css +++ b/src/styles/00-tokens.css @@ -1,5 +1,5 @@ .codex-panel, -.codex-panel-chat-turn-diff, +.codex-panel-turn-diff, .codex-panel-settings, .codex-panel-threads, .codex-panel-selection-rewrite { diff --git a/src/styles/30-chat-turn-diff-view.css b/src/styles/30-turn-diff-view.css similarity index 59% rename from src/styles/30-chat-turn-diff-view.css rename to src/styles/30-turn-diff-view.css index 8b16884c..7108c899 100644 --- a/src/styles/30-chat-turn-diff-view.css +++ b/src/styles/30-turn-diff-view.css @@ -1,4 +1,4 @@ -.codex-panel-chat-turn-diff { +.codex-panel-turn-diff { display: flex; flex-direction: column; gap: var(--codex-panel-edge-padding-x); @@ -7,52 +7,52 @@ overflow: auto; } -.codex-panel-chat-turn-diff__header { +.codex-panel-turn-diff__header { display: flex; align-items: flex-start; gap: var(--codex-panel-section-gap); } -.codex-panel-chat-turn-diff__title-block { +.codex-panel-turn-diff__title-block { flex: 1 1 auto; min-width: 0; } -.codex-panel-chat-turn-diff__title { +.codex-panel-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-chat-turn-diff__meta, -.codex-panel-chat-turn-diff__files, -.codex-panel-chat-turn-diff__empty { +.codex-panel-turn-diff__meta, +.codex-panel-turn-diff__files, +.codex-panel-turn-diff__empty { color: var(--text-muted); font-size: var(--font-ui-small); } -.codex-panel-chat-turn-diff__copy { +.codex-panel-turn-diff__copy { flex: 0 0 auto; } -.codex-panel-chat-turn-diff__files { +.codex-panel-turn-diff__files { margin: 0; } -.codex-panel-chat-turn-diff__files > summary { +.codex-panel-turn-diff__files > summary { cursor: default; } -.codex-panel-chat-turn-diff__files > summary:hover { +.codex-panel-turn-diff__files > summary:hover { color: var(--nav-item-color-hover, var(--text-normal)); } -.codex-panel-chat-turn-diff__files ul { +.codex-panel-turn-diff__files ul { margin-block: var(--codex-panel-control-gap) 0; } -.codex-panel-chat-turn-diff__diff.codex-panel-diff { +.codex-panel-turn-diff__diff.codex-panel-diff { max-height: none; margin: 0; overflow: visible; diff --git a/src/styles/order.json b/src/styles/order.json index ece6fc76..23e98350 100644 --- a/src/styles/order.json +++ b/src/styles/order.json @@ -10,7 +10,7 @@ "24-chat-stream-items.css", "25-chat-pending-requests.css", "26-chat-composer.css", - "30-chat-turn-diff-view.css", + "30-turn-diff-view.css", "40-threads-view.css", "50-selection-rewrite.css", "60-settings.css" diff --git a/tests/features/chat/ui/turn-diff.test.ts b/tests/features/turn-diff/turn-diff.test.ts similarity index 77% rename from tests/features/chat/ui/turn-diff.test.ts rename to tests/features/turn-diff/turn-diff.test.ts index ef878f7e..8f71a299 100644 --- a/tests/features/chat/ui/turn-diff.test.ts +++ b/tests/features/turn-diff/turn-diff.test.ts @@ -3,19 +3,19 @@ import type { WorkspaceLeaf } from "obsidian"; import { describe, expect, it, vi } from "vitest"; -import { persistedChatTurnDiffViewState } from "../../../../src/features/chat/domain/turn-diff"; -import { renderChatTurnDiffView } from "../../../../src/features/chat/ui/turn-diff/render.dom"; -import { CodexChatTurnDiffView } from "../../../../src/features/chat/ui/turn-diff/view.obsidian"; -import { installObsidianDomShims } from "../../../support/dom"; +import { persistedTurnDiffViewState } from "../../../src/features/turn-diff/model"; +import { renderTurnDiffView } from "../../../src/features/turn-diff/render.dom"; +import { CodexTurnDiffView } from "../../../src/features/turn-diff/view.obsidian"; +import { installObsidianDomShims } from "../../support/dom"; installObsidianDomShims(); -describe("chat turn diff view decisions", () => { +describe("turn diff view decisions", () => { it("renders the turn diff view with classified unified diff lines", () => { const parent = document.createElement("div"); const copyDiff = vi.fn(); - renderChatTurnDiffView( + renderTurnDiffView( parent, { threadId: "019e061e-0046-7653-a362-86de9a47cb5c", @@ -27,12 +27,12 @@ describe("chat turn diff view decisions", () => { { copyDiff }, ); - 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"); - const changedFilesSummary = parent.querySelector(".codex-panel-chat-turn-diff__files summary"); + expect(parent.querySelector(".codex-panel-turn-diff__title")?.textContent).toBe("Turn diff"); + expect(parent.querySelector(".codex-panel-turn-diff__meta")?.textContent).toContain("019e061e"); + const changedFilesSummary = parent.querySelector(".codex-panel-turn-diff__files summary"); expect(changedFilesSummary?.textContent).toBe("Changed files"); expect(changedFilesSummary?.tabIndex).toBe(-1); - expect(parent.querySelector(".codex-panel-chat-turn-diff__files")?.textContent).toContain("src/main.ts"); + 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.textContent).not.toContain("diff --git"); expect(parent.textContent).not.toContain("+++ b/src/main.ts"); @@ -43,14 +43,14 @@ describe("chat turn diff view decisions", () => { 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(); + parent.querySelector(".codex-panel-turn-diff__copy")?.click(); expect(copyDiff).toHaveBeenCalled(); }); it("highlights changed English words inside adjacent removed and added lines", () => { const parent = document.createElement("div"); - renderChatTurnDiffView(parent, { + renderTurnDiffView(parent, { threadId: "thread", turnId: "turn", cwd: "/vault/project", @@ -67,7 +67,7 @@ describe("chat turn diff view decisions", () => { it("highlights changed Japanese words with Intl.Segmenter", () => { const parent = document.createElement("div"); - renderChatTurnDiffView(parent, { + renderTurnDiffView(parent, { threadId: "thread", turnId: "turn", cwd: "/vault/project", @@ -84,7 +84,7 @@ describe("chat turn diff view decisions", () => { it("pairs changed words by line inside multi-line replacement blocks", () => { const parent = document.createElement("div"); - renderChatTurnDiffView(parent, { + renderTurnDiffView(parent, { threadId: "thread", turnId: "turn", cwd: "/vault/project", @@ -114,7 +114,7 @@ describe("chat turn diff view decisions", () => { const oldText = `start ${"old ".repeat(600)}end`; const newText = `start ${"new ".repeat(600)}end`; - renderChatTurnDiffView(parent, { + renderTurnDiffView(parent, { threadId: "thread", turnId: "turn", cwd: "/vault/project", @@ -128,7 +128,7 @@ describe("chat turn diff view decisions", () => { }); it("keeps unified diff text out of persisted turn diff view state", () => { - const persisted = persistedChatTurnDiffViewState({ + const persisted = persistedTurnDiffViewState({ threadId: "thread", turnId: "turn", cwd: "/vault/project", @@ -148,17 +148,17 @@ describe("chat turn diff view decisions", () => { it("renders restored turn diff metadata without unavailable diff text", () => { const parent = document.createElement("div"); - renderChatTurnDiffView(parent, null, {}, { threadId: "thread", turnId: "turn", cwd: "/vault/project", files: ["src/main.ts"] }); + renderTurnDiffView(parent, null, {}, { threadId: "thread", turnId: "turn", cwd: "/vault/project", files: ["src/main.ts"] }); - expect(parent.querySelector(".codex-panel-chat-turn-diff__meta")?.textContent).toContain("thread / turn"); + expect(parent.querySelector(".codex-panel-turn-diff__meta")?.textContent).toContain("thread / turn"); expect(parent.textContent).toContain("Turn diff is no longer available."); - expect(parent.querySelector(".codex-panel-chat-turn-diff__copy")).toBeNull(); - expect(parent.querySelector(".codex-panel-chat-turn-diff__diff")).toBeNull(); + expect(parent.querySelector(".codex-panel-turn-diff__copy")).toBeNull(); + expect(parent.querySelector(".codex-panel-turn-diff__diff")).toBeNull(); }); it("unmounts the turn diff Preact root when the view closes", async () => { const containerEl = document.createElement("div"); - const view = new CodexChatTurnDiffView({ containerEl } as unknown as WorkspaceLeaf); + const view = new CodexTurnDiffView({ containerEl } as unknown as WorkspaceLeaf); view.setDiffPayload({ threadId: "thread", @@ -168,7 +168,7 @@ describe("chat turn diff view decisions", () => { diff: "diff --git a/src/main.ts b/src/main.ts\n@@\n-old\n+new", }); - expect(view.contentEl.querySelector(".codex-panel-chat-turn-diff__title")?.textContent).toBe("Turn diff"); + expect(view.contentEl.querySelector(".codex-panel-turn-diff__title")?.textContent).toBe("Turn diff"); await view.onClose(); diff --git a/tests/scripts/grit-policy.test.mjs b/tests/scripts/grit-policy.test.mjs index b351f947..e8643123 100644 --- a/tests/scripts/grit-policy.test.mjs +++ b/tests/scripts/grit-policy.test.mjs @@ -740,6 +740,7 @@ export const value = statusText; expect(pluginDiagnostics(report, "src/features/chat/ui/message.tsx")).toEqual([]); expect(pluginDiagnostics(report, "src/features/selection-rewrite/popover.dom.tsx")).toEqual([]); expect(pluginDiagnostics(report, "src/features/threads-view/shell.dom.tsx")).toEqual([]); + expect(pluginDiagnostics(report, "src/features/turn-diff/render.dom.tsx")).toEqual([]); expect(pluginDiagnostics(report, "src/settings/section.tsx")).toEqual([]); expect(pluginDiagnostics(report, "src/shared/ui/diff.tsx")).toEqual([]); }); @@ -950,6 +951,12 @@ export const value =
  • ; path.join(cwd, "src/features/threads-view/shell.dom.tsx"), ` export const value =
    ; +`.trimStart(), + ); + await writeFile( + path.join(cwd, "src/features/turn-diff/render.dom.tsx"), + ` +export const value =
    ; `.trimStart(), ); await writeFile( @@ -1025,6 +1032,7 @@ export const value =
    ;
           "src/features/selection-rewrite/popover.dom.tsx",
           "src/features/threads-view/inline-row.tsx",
           "src/features/threads-view/shell.dom.tsx",
    +      "src/features/turn-diff/render.dom.tsx",
           "src/features/chat/ui/message.tsx",
           "src/settings/section.tsx",
           "src/shared/ui/diff.tsx",
    @@ -1367,6 +1375,7 @@ async function tempBiomeWorkspace(plugins) {
       await mkdir(path.join(cwd, "src/features/threads/list"), { recursive: true });
       await mkdir(path.join(cwd, "src/features/threads/workflows"), { recursive: true });
       await mkdir(path.join(cwd, "src/features/threads-view"), { recursive: true });
    +  await mkdir(path.join(cwd, "src/features/turn-diff"), { recursive: true });
       await mkdir(path.join(cwd, "src/settings"), { recursive: true });
       await mkdir(path.join(cwd, "src/domain/threads"), { recursive: true });
       await mkdir(path.join(cwd, "src/app-server/connection"), { recursive: true });
    diff --git a/tests/styles.test.ts b/tests/styles.test.ts
    index 77c5658c..03242ddc 100644
    --- a/tests/styles.test.ts
    +++ b/tests/styles.test.ts
    @@ -12,7 +12,7 @@ describe("panel CSS token scope", () => {
         const tokenScope = /^(?(?:\.[^{]+,\n)*\.[^{]+) \{/m.exec(styles)?.groups?.["selectors"] ?? "";
     
         expect(tokenScope).toContain(".codex-panel");
    -    expect(tokenScope).toContain(".codex-panel-chat-turn-diff");
    +    expect(tokenScope).toContain(".codex-panel-turn-diff");
         expect(tokenScope).toContain(".codex-panel-settings");
         expect(tokenScope).toContain(".codex-panel-threads");
         expect(tokenScope).toContain(".codex-panel-selection-rewrite");