mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 17:30:31 +00:00
Extract view open close controller
This commit is contained in:
parent
a7d68ea27e
commit
3fc2fc2e08
3 changed files with 239 additions and 36 deletions
70
src/features/chat/view-open-close-controller.ts
Normal file
70
src/features/chat/view-open-close-controller.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import type { EventRef, WorkspaceLeaf } from "obsidian";
|
||||
|
||||
import { unmountReactRoot } from "../../shared/ui/react-root";
|
||||
import { unmountChatPanelShell } from "./ui/shell";
|
||||
|
||||
export interface ChatViewOpenCloseControllerHost {
|
||||
setOpened: (opened: boolean) => void;
|
||||
setClosing: (closing: boolean) => void;
|
||||
registerEvent: (eventRef: EventRef) => void;
|
||||
registerComposerNoteIndexInvalidation: (register: (eventRef: EventRef) => void) => void;
|
||||
registerPointerDown: (handler: (event: PointerEvent) => void) => void;
|
||||
registerActiveLeafChange: (handler: (leaf: WorkspaceLeaf | null) => void) => void;
|
||||
isOwnLeaf: (leaf: WorkspaceLeaf | null) => boolean;
|
||||
scrollMessagesToBottomOnFocus: () => void;
|
||||
applyCachedSharedAppServerState: () => void;
|
||||
render: () => void;
|
||||
scheduleDeferredAppServerWarmup: () => void;
|
||||
scheduleDeferredRestoredThreadHydration: () => void;
|
||||
closeToolbarPanelOnOutsidePointer: (event: PointerEvent) => void;
|
||||
invalidateConnectionWork: () => void;
|
||||
invalidateResumeWork: () => void;
|
||||
clearDeferredTasks: () => void;
|
||||
panelRoot: () => HTMLElement | null;
|
||||
disposeMessages: () => void;
|
||||
disposeComposer: () => void;
|
||||
disconnect: () => void;
|
||||
clearClient: () => void;
|
||||
refreshLiveState: () => void;
|
||||
deferRefreshLiveState: () => void;
|
||||
}
|
||||
|
||||
export class ChatViewOpenCloseController {
|
||||
constructor(private readonly host: ChatViewOpenCloseControllerHost) {}
|
||||
|
||||
open(): void {
|
||||
this.host.setOpened(true);
|
||||
this.host.setClosing(false);
|
||||
this.host.registerComposerNoteIndexInvalidation((eventRef) => {
|
||||
this.host.registerEvent(eventRef);
|
||||
});
|
||||
this.host.registerPointerDown((event) => {
|
||||
this.host.closeToolbarPanelOnOutsidePointer(event);
|
||||
});
|
||||
this.host.registerActiveLeafChange((leaf) => {
|
||||
if (this.host.isOwnLeaf(leaf)) this.host.scrollMessagesToBottomOnFocus();
|
||||
});
|
||||
this.host.applyCachedSharedAppServerState();
|
||||
this.host.render();
|
||||
this.host.scheduleDeferredAppServerWarmup();
|
||||
this.host.scheduleDeferredRestoredThreadHydration();
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.host.setOpened(false);
|
||||
this.host.setClosing(true);
|
||||
this.host.invalidateConnectionWork();
|
||||
this.host.invalidateResumeWork();
|
||||
this.host.clearDeferredTasks();
|
||||
const panelRoot = this.host.panelRoot();
|
||||
unmountReactRoot(panelRoot?.querySelector<HTMLElement>(".codex-panel__toolbar") ?? null);
|
||||
this.host.disposeMessages();
|
||||
this.host.disposeComposer();
|
||||
unmountReactRoot(panelRoot?.querySelector<HTMLElement>(".codex-panel__slot--composer") ?? null);
|
||||
unmountChatPanelShell(panelRoot);
|
||||
this.host.disconnect();
|
||||
this.host.clearClient();
|
||||
this.host.refreshLiveState();
|
||||
this.host.deferRefreshLiveState();
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@ import type { CodexPanelSettings } from "../../settings/model";
|
|||
import { ChatComposerController } from "./chat-composer-controller";
|
||||
import { activeTurnId, chatTurnBusy, createChatStateStore, type ChatState, type ChatAction } from "./chat-state";
|
||||
import { renderToolbar, type ToolbarViewModel } from "./ui/toolbar";
|
||||
import { unmountChatPanelShell } from "./ui/shell";
|
||||
import type { ChatTurnDiffViewState } from "./ui/turn-diff";
|
||||
import { ChatMessageRenderer } from "./chat-message-renderer";
|
||||
import type { OpenCodexPanelSnapshot } from "../../runtime/open-panel-snapshot";
|
||||
|
|
@ -27,7 +26,6 @@ import type { SharedAppServerMetadata } from "../../runtime/shared-app-server-st
|
|||
import { ChatThreadActionController } from "./thread-actions";
|
||||
import { ChatRuntimeSettingsController } from "./runtime-settings-controller";
|
||||
import { RestoredThreadController } from "./restored-thread-controller";
|
||||
import { unmountReactRoot } from "../../shared/ui/react-root";
|
||||
import {
|
||||
activeComposerThreadName as buildActiveComposerThreadName,
|
||||
activeThreadTitle as buildActiveThreadTitle,
|
||||
|
|
@ -59,6 +57,7 @@ import { ChatConnectionController } from "./connection-controller";
|
|||
import { ThreadIdentityController } from "./thread-identity-controller";
|
||||
import { ThreadResumeController } from "./thread-resume-controller";
|
||||
import { ChatViewRenderController } from "./view-render-controller";
|
||||
import { ChatViewOpenCloseController } from "./view-open-close-controller";
|
||||
|
||||
export interface CodexChatHost {
|
||||
readonly settings: CodexPanelSettings;
|
||||
|
|
@ -99,6 +98,7 @@ export class CodexChatView extends ItemView {
|
|||
private readonly composerController: ChatComposerController;
|
||||
private readonly messageRenderer: ChatMessageRenderer;
|
||||
private readonly renderController: ChatViewRenderController;
|
||||
private readonly openCloseController: ChatViewOpenCloseController;
|
||||
private readonly messageScroll: ChatMessageScrollController;
|
||||
private readonly turnSubmission: TurnSubmissionController;
|
||||
private readonly slashCommands: SlashCommandController;
|
||||
|
|
@ -277,6 +277,75 @@ export class CodexChatView extends ItemView {
|
|||
this.render();
|
||||
},
|
||||
});
|
||||
this.openCloseController = new ChatViewOpenCloseController({
|
||||
setOpened: (opened) => {
|
||||
this.opened = opened;
|
||||
},
|
||||
setClosing: (closing) => {
|
||||
this.closing = closing;
|
||||
},
|
||||
registerEvent: (eventRef) => {
|
||||
this.registerEvent(eventRef);
|
||||
},
|
||||
registerComposerNoteIndexInvalidation: (register) => {
|
||||
this.composerController.registerNoteIndexInvalidation(register);
|
||||
},
|
||||
registerPointerDown: (handler) => {
|
||||
this.registerDomEvent(this.containerEl.doc, "pointerdown", handler);
|
||||
},
|
||||
registerActiveLeafChange: (handler) => {
|
||||
this.registerEvent(this.app.workspace.on("active-leaf-change", handler));
|
||||
},
|
||||
isOwnLeaf: (leaf) => leaf === this.leaf,
|
||||
scrollMessagesToBottomOnFocus: () => {
|
||||
this.messageScroll.scrollToBottomOnFocus();
|
||||
},
|
||||
applyCachedSharedAppServerState: () => {
|
||||
this.applyCachedSharedAppServerState();
|
||||
},
|
||||
render: () => {
|
||||
this.render();
|
||||
},
|
||||
scheduleDeferredAppServerWarmup: () => {
|
||||
this.scheduleDeferredAppServerWarmup();
|
||||
},
|
||||
scheduleDeferredRestoredThreadHydration: () => {
|
||||
this.scheduleDeferredRestoredThreadHydration();
|
||||
},
|
||||
closeToolbarPanelOnOutsidePointer: (event) => {
|
||||
this.closeToolbarPanelOnOutsidePointer(event);
|
||||
},
|
||||
invalidateConnectionWork: () => {
|
||||
this.invalidateConnectionWork();
|
||||
},
|
||||
invalidateResumeWork: () => {
|
||||
this.invalidateResumeWork();
|
||||
},
|
||||
clearDeferredTasks: () => {
|
||||
this.deferredTasks.clearAll();
|
||||
},
|
||||
panelRoot: () => this.panelRoot(),
|
||||
disposeMessages: () => {
|
||||
this.messageRenderer.dispose();
|
||||
},
|
||||
disposeComposer: () => {
|
||||
this.composerController.dispose();
|
||||
},
|
||||
disconnect: () => {
|
||||
this.connection.disconnect();
|
||||
},
|
||||
clearClient: () => {
|
||||
this.client = null;
|
||||
},
|
||||
refreshLiveState: () => {
|
||||
this.plugin.refreshThreadsViewLiveState();
|
||||
},
|
||||
deferRefreshLiveState: () => {
|
||||
this.containerEl.win.setTimeout(() => {
|
||||
this.plugin.refreshThreadsViewLiveState();
|
||||
}, 0);
|
||||
},
|
||||
});
|
||||
this.controller = new ChatController(this.chatState, {
|
||||
refreshThreads: () => {
|
||||
void this.refreshThreads();
|
||||
|
|
@ -665,43 +734,11 @@ export class CodexChatView extends ItemView {
|
|||
}
|
||||
|
||||
override async onOpen(): Promise<void> {
|
||||
this.opened = true;
|
||||
this.closing = false;
|
||||
this.composerController.registerNoteIndexInvalidation((eventRef) => {
|
||||
this.registerEvent(eventRef);
|
||||
});
|
||||
this.registerDomEvent(this.containerEl.doc, "pointerdown", (event) => {
|
||||
this.closeToolbarPanelOnOutsidePointer(event);
|
||||
});
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("active-leaf-change", (leaf) => {
|
||||
if (leaf === this.leaf) this.messageScroll.scrollToBottomOnFocus();
|
||||
}),
|
||||
);
|
||||
this.applyCachedSharedAppServerState();
|
||||
this.render();
|
||||
this.scheduleDeferredAppServerWarmup();
|
||||
this.scheduleDeferredRestoredThreadHydration();
|
||||
this.openCloseController.open();
|
||||
}
|
||||
|
||||
override async onClose(): Promise<void> {
|
||||
this.opened = false;
|
||||
this.closing = true;
|
||||
this.invalidateConnectionWork();
|
||||
this.invalidateResumeWork();
|
||||
this.deferredTasks.clearAll();
|
||||
const panelRoot = this.panelRoot();
|
||||
unmountReactRoot(panelRoot?.querySelector<HTMLElement>(".codex-panel__toolbar") ?? null);
|
||||
this.messageRenderer.dispose();
|
||||
this.composerController.dispose();
|
||||
unmountReactRoot(panelRoot?.querySelector<HTMLElement>(".codex-panel__slot--composer") ?? null);
|
||||
unmountChatPanelShell(panelRoot);
|
||||
this.connection.disconnect();
|
||||
this.client = null;
|
||||
this.plugin.refreshThreadsViewLiveState();
|
||||
this.containerEl.win.setTimeout(() => {
|
||||
this.plugin.refreshThreadsViewLiveState();
|
||||
}, 0);
|
||||
this.openCloseController.close();
|
||||
}
|
||||
|
||||
setComposerText(text: string): void {
|
||||
|
|
|
|||
96
tests/features/chat/view-open-close-controller.test.ts
Normal file
96
tests/features/chat/view-open-close-controller.test.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { EventRef } from "obsidian";
|
||||
|
||||
import { ChatViewOpenCloseController, type ChatViewOpenCloseControllerHost } from "../../../src/features/chat/view-open-close-controller";
|
||||
import { unmountChatPanelShell } from "../../../src/features/chat/ui/shell";
|
||||
import { unmountReactRoot } from "../../../src/shared/ui/react-root";
|
||||
|
||||
vi.mock("../../../src/features/chat/ui/shell", () => ({
|
||||
unmountChatPanelShell: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../../src/shared/ui/react-root", () => ({
|
||||
unmountReactRoot: vi.fn(),
|
||||
}));
|
||||
|
||||
function createHost(overrides: Partial<ChatViewOpenCloseControllerHost> = {}) {
|
||||
const root = document.createElement("div");
|
||||
const toolbar = document.createElement("div");
|
||||
toolbar.className = "codex-panel__toolbar";
|
||||
root.appendChild(toolbar);
|
||||
const composer = document.createElement("div");
|
||||
composer.className = "codex-panel__slot--composer";
|
||||
root.appendChild(composer);
|
||||
const host: ChatViewOpenCloseControllerHost = {
|
||||
setOpened: vi.fn(),
|
||||
setClosing: vi.fn(),
|
||||
registerEvent: vi.fn(),
|
||||
registerComposerNoteIndexInvalidation: vi.fn((register) => {
|
||||
register({} as EventRef);
|
||||
}),
|
||||
registerPointerDown: vi.fn(),
|
||||
registerActiveLeafChange: vi.fn(),
|
||||
isOwnLeaf: vi.fn(() => false),
|
||||
scrollMessagesToBottomOnFocus: vi.fn(),
|
||||
applyCachedSharedAppServerState: vi.fn(),
|
||||
render: vi.fn(),
|
||||
scheduleDeferredAppServerWarmup: vi.fn(),
|
||||
scheduleDeferredRestoredThreadHydration: vi.fn(),
|
||||
closeToolbarPanelOnOutsidePointer: vi.fn(),
|
||||
invalidateConnectionWork: vi.fn(),
|
||||
invalidateResumeWork: vi.fn(),
|
||||
clearDeferredTasks: vi.fn(),
|
||||
panelRoot: () => root,
|
||||
disposeMessages: vi.fn(),
|
||||
disposeComposer: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
clearClient: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
deferRefreshLiveState: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
return { controller: new ChatViewOpenCloseController(host), host, root };
|
||||
}
|
||||
|
||||
describe("ChatViewOpenCloseController", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(unmountChatPanelShell).mockClear();
|
||||
vi.mocked(unmountReactRoot).mockClear();
|
||||
});
|
||||
|
||||
it("registers open events and schedules startup work", () => {
|
||||
const { controller, host } = createHost();
|
||||
|
||||
controller.open();
|
||||
|
||||
expect(host.setOpened).toHaveBeenCalledWith(true);
|
||||
expect(host.setClosing).toHaveBeenCalledWith(false);
|
||||
expect(host.registerEvent).toHaveBeenCalledOnce();
|
||||
expect(host.registerPointerDown).toHaveBeenCalledOnce();
|
||||
expect(host.registerActiveLeafChange).toHaveBeenCalledOnce();
|
||||
expect(host.applyCachedSharedAppServerState).toHaveBeenCalledOnce();
|
||||
expect(host.render).toHaveBeenCalledOnce();
|
||||
expect(host.scheduleDeferredAppServerWarmup).toHaveBeenCalledOnce();
|
||||
expect(host.scheduleDeferredRestoredThreadHydration).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("disposes mounted resources and refreshes live state on close", () => {
|
||||
const { controller, host, root } = createHost();
|
||||
|
||||
controller.close();
|
||||
|
||||
expect(host.setOpened).toHaveBeenCalledWith(false);
|
||||
expect(host.setClosing).toHaveBeenCalledWith(true);
|
||||
expect(host.clearDeferredTasks).toHaveBeenCalledOnce();
|
||||
expect(unmountReactRoot).toHaveBeenCalledWith(root.querySelector(".codex-panel__toolbar"));
|
||||
expect(host.disposeMessages).toHaveBeenCalledOnce();
|
||||
expect(host.disposeComposer).toHaveBeenCalledOnce();
|
||||
expect(unmountChatPanelShell).toHaveBeenCalledWith(root);
|
||||
expect(host.disconnect).toHaveBeenCalledOnce();
|
||||
expect(host.clearClient).toHaveBeenCalledOnce();
|
||||
expect(host.refreshLiveState).toHaveBeenCalledOnce();
|
||||
expect(host.deferRefreshLiveState).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue