mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Extract turn diff view feature
This commit is contained in:
parent
727f5393af
commit
ca53d77fed
18 changed files with 171 additions and 166 deletions
|
|
@ -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/**"
|
||||
|
|
|
|||
|
|
@ -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/`.
|
||||
|
||||
|
|
|
|||
|
|
@ -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<void>;
|
||||
focusThreadInOpenView(threadId: string): Promise<boolean>;
|
||||
openTurnDiff(state: ChatTurnDiffViewState): Promise<void>;
|
||||
openTurnDiff(state: TurnDiffViewState): Promise<void>;
|
||||
refreshThreadsViewLiveState(): void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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, <ChatTurnDiffView state={state} actions={actions} metadata={metadata} />);
|
||||
}
|
||||
|
||||
function ChatTurnDiffView({
|
||||
state,
|
||||
actions,
|
||||
metadata,
|
||||
}: {
|
||||
state: ChatTurnDiffViewState | null;
|
||||
actions: ChatTurnDiffViewActions;
|
||||
metadata: PersistedChatTurnDiffViewState | null;
|
||||
}): UiNode {
|
||||
if (!state) {
|
||||
if (metadata) {
|
||||
return (
|
||||
<>
|
||||
<TurnDiffHeader state={metadata} copyDiff={null} />
|
||||
<div className="codex-panel-chat-turn-diff__empty">Turn diff is no longer available.</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <div className="codex-panel-chat-turn-diff__empty">No turn diff selected.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TurnDiffHeader state={state} copyDiff={actions.copyDiff ?? null} />
|
||||
{state.files.length > 0 ? <ChangedFiles files={state.files} /> : null}
|
||||
<UnifiedDiffView diff={state.diff} className="codex-panel-chat-turn-diff__diff" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TurnDiffHeader({
|
||||
state,
|
||||
copyDiff,
|
||||
}: {
|
||||
state: PersistedChatTurnDiffViewState;
|
||||
copyDiff: ChatTurnDiffViewActions["copyDiff"] | null;
|
||||
}): UiNode {
|
||||
return (
|
||||
<div className="codex-panel-chat-turn-diff__header">
|
||||
<div className="codex-panel-chat-turn-diff__title-block">
|
||||
<div className="codex-panel-chat-turn-diff__title">Turn diff</div>
|
||||
<div className="codex-panel-chat-turn-diff__meta">
|
||||
{shortThreadId(state.threadId)} / {shortThreadId(state.turnId)} ·{" "}
|
||||
{state.files.length === 1 ? "Edited 1 file" : `Edited ${String(state.files.length)} files`}
|
||||
</div>
|
||||
</div>
|
||||
{copyDiff ? (
|
||||
<IconButton icon="copy" label="Copy diff" className="clickable-icon codex-panel-chat-turn-diff__copy" onClick={copyDiff} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChangedFiles({ files }: { files: string[] }): UiNode {
|
||||
return (
|
||||
<details className="codex-panel-chat-turn-diff__files">
|
||||
<summary tabIndex={-1}>Changed files</summary>
|
||||
<ul>
|
||||
{files.map((file) => (
|
||||
<li key={file}>{file}</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<ChatTurnDiffViewState, "diff">;
|
||||
export type PersistedTurnDiffViewState = Omit<TurnDiffViewState, "diff">;
|
||||
|
||||
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<PersistedChatTurnDiffViewState>;
|
||||
const record = value as Partial<PersistedTurnDiffViewState>;
|
||||
return (
|
||||
typeof record.threadId === "string" &&
|
||||
typeof record.turnId === "string" &&
|
||||
87
src/features/turn-diff/render.dom.tsx
Normal file
87
src/features/turn-diff/render.dom.tsx
Normal file
|
|
@ -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, <TurnDiffView state={state} actions={actions} metadata={metadata} />);
|
||||
}
|
||||
|
||||
function TurnDiffView({
|
||||
state,
|
||||
actions,
|
||||
metadata,
|
||||
}: {
|
||||
state: TurnDiffViewState | null;
|
||||
actions: TurnDiffViewActions;
|
||||
metadata: PersistedTurnDiffViewState | null;
|
||||
}): UiNode {
|
||||
if (!state) {
|
||||
if (metadata) {
|
||||
return (
|
||||
<>
|
||||
<TurnDiffHeader state={metadata} copyDiff={null} />
|
||||
<div className="codex-panel-turn-diff__empty">Turn diff is no longer available.</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <div className="codex-panel-turn-diff__empty">No turn diff selected.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TurnDiffHeader state={state} copyDiff={actions.copyDiff ?? null} />
|
||||
{state.files.length > 0 ? <ChangedFiles files={state.files} /> : null}
|
||||
<UnifiedDiffView diff={state.diff} className="codex-panel-turn-diff__diff" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TurnDiffHeader({
|
||||
state,
|
||||
copyDiff,
|
||||
}: {
|
||||
state: PersistedTurnDiffViewState;
|
||||
copyDiff: TurnDiffViewActions["copyDiff"] | null;
|
||||
}): UiNode {
|
||||
return (
|
||||
<div className="codex-panel-turn-diff__header">
|
||||
<div className="codex-panel-turn-diff__title-block">
|
||||
<div className="codex-panel-turn-diff__title">Turn diff</div>
|
||||
<div className="codex-panel-turn-diff__meta">
|
||||
{shortThreadId(state.threadId)} / {shortThreadId(state.turnId)} ·{" "}
|
||||
{state.files.length === 1 ? "Edited 1 file" : `Edited ${String(state.files.length)} files`}
|
||||
</div>
|
||||
</div>
|
||||
{copyDiff ? (
|
||||
<IconButton icon="copy" label="Copy diff" className="clickable-icon codex-panel-turn-diff__copy" onClick={copyDiff} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChangedFiles({ files }: { files: string[] }): UiNode {
|
||||
return (
|
||||
<details className="codex-panel-turn-diff__files">
|
||||
<summary tabIndex={-1}>Changed files</summary>
|
||||
<ul>
|
||||
{files.map((file) => (
|
||||
<li key={file}>{file}</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<void> {
|
||||
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 ?? "") } : {},
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
private async openTurnDiff(state: TurnDiffViewState): Promise<void> {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<HTMLElement>(".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<HTMLElement>(".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<HTMLButtonElement>(".codex-panel-chat-turn-diff__copy")?.click();
|
||||
parent.querySelector<HTMLButtonElement>(".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();
|
||||
|
||||
|
|
@ -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 = <li />;
|
|||
path.join(cwd, "src/features/threads-view/shell.dom.tsx"),
|
||||
`
|
||||
export const value = <main />;
|
||||
`.trimStart(),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(cwd, "src/features/turn-diff/render.dom.tsx"),
|
||||
`
|
||||
export const value = <main />;
|
||||
`.trimStart(),
|
||||
);
|
||||
await writeFile(
|
||||
|
|
@ -1025,6 +1032,7 @@ export const value = <pre />;
|
|||
"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 });
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ describe("panel CSS token scope", () => {
|
|||
const tokenScope = /^(?<selectors>(?:\.[^{]+,\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");
|
||||
|
|
|
|||
Loading…
Reference in a new issue