From 3737dcf0bdf5089dcbaeb87bd0724152d51d6882 Mon Sep 17 00:00:00 2001 From: murashit Date: Mon, 15 Jun 2026 19:02:03 +0900 Subject: [PATCH] Inline thin chat host and thread surface adapters --- .../chat/application/ports/chat-host.ts | 37 ------ .../chat/application/threads/composition.ts | 16 ++- .../application/threads/lifecycle-parts.ts | 3 +- src/features/chat/host/composer.ts | 4 +- src/features/chat/host/connection-bundle.ts | 14 +- src/features/chat/host/conversation.ts | 8 +- src/features/chat/host/runtime.ts | 38 +++++- src/features/chat/host/view.ts | 2 +- src/plugin-runtime.ts | 72 +++++++++-- src/workspace/shared-thread-catalog.ts | 8 +- src/workspace/thread-surface-actions.ts | 68 ---------- tests/features/chat/view-connection.test.ts | 2 +- tests/main.test.ts | 2 +- tests/workspace/shared-thread-catalog.test.ts | 8 +- .../workspace/thread-surface-actions.test.ts | 121 ------------------ 15 files changed, 142 insertions(+), 261 deletions(-) delete mode 100644 src/features/chat/application/ports/chat-host.ts delete mode 100644 src/workspace/thread-surface-actions.ts delete mode 100644 tests/workspace/thread-surface-actions.test.ts diff --git a/src/features/chat/application/ports/chat-host.ts b/src/features/chat/application/ports/chat-host.ts deleted file mode 100644 index ebe2159d..00000000 --- a/src/features/chat/application/ports/chat-host.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { CodexPanelSettings } from "../../../../settings/model"; -import type { SharedThreadCatalog } from "../../../../workspace/shared-thread-catalog"; -import type { ChatTurnDiffViewState } from "../../domain/turn-diff"; - -export interface CodexChatHost { - readonly settingsRef: PluginSettingsRef; - readonly workspace: WorkspacePanels; - readonly threadCatalog: ThreadCatalogFacade; -} - -export interface PluginSettingsRef { - readonly settings: CodexPanelSettings; - readonly vaultPath: string; -} - -export interface WorkspacePanels { - openThreadInNewView(threadId: string): Promise; - focusThreadInOpenView(threadId: string): Promise; - openTurnDiff(state: ChatTurnDiffViewState): Promise; -} - -export type ThreadCatalogFacade = Pick< - SharedThreadCatalog, - | "archiveThreadInCatalog" - | "renameThreadInCatalog" - | "refreshThreadsViewLiveState" - | "refreshFromOpenSurface" - | "setActiveThreads" - | "setAppServerMetadata" - | "fetchActiveThreads" - | "activeThreadsSnapshot" - | "appServerMetadataSnapshot" - | "modelsSnapshot" - | "observeActiveThreads" - | "observeAppServerMetadata" - | "observeModels" ->; diff --git a/src/features/chat/application/threads/composition.ts b/src/features/chat/application/threads/composition.ts index 6cd23e1b..5911ad25 100644 --- a/src/features/chat/application/threads/composition.ts +++ b/src/features/chat/application/threads/composition.ts @@ -5,16 +5,20 @@ import type { GoalActions } from "./goal-actions"; import { createSelectionActions } from "./selection-actions"; import type { ChatResumeWorkTracker, ChatViewDeferredTasks } from "../lifecycle"; import type { ChatStateStore } from "../state/store"; -import type { PluginSettingsRef, ThreadCatalogFacade, WorkspacePanels } from "../ports/chat-host"; import type { AutoTitleController } from "./auto-title-controller"; import { ThreadRenameEditorController } from "./rename-editor-controller"; import { createThreadManagementActions, type ThreadManagementActions, type ThreadManagementActionsHost } from "./thread-management-actions"; import { createThreadLifecycleParts } from "./lifecycle-parts"; interface ThreadPartsContext { - settingsRef: PluginSettingsRef; - workspace: Pick; - threadCatalog: Pick; + settingsRef: { readonly vaultPath: string }; + workspace: { + focusThreadInOpenView: (threadId: string) => Promise; + openThreadInNewView: (threadId: string) => Promise; + }; + threadCatalog: { + refreshFromOpenSurface(): void; + }; state: { stateStore: ChatStateStore; }; @@ -134,7 +138,9 @@ export function createThreadParts(context: ThreadPartsContext) { } interface ThreadSelectionActionsContext { - workspace: Pick; + workspace: { + focusThreadInOpenView: (threadId: string) => Promise; + }; state: { stateStore: ChatStateStore; }; diff --git a/src/features/chat/application/threads/lifecycle-parts.ts b/src/features/chat/application/threads/lifecycle-parts.ts index 3ec403b3..0bab2afe 100644 --- a/src/features/chat/application/threads/lifecycle-parts.ts +++ b/src/features/chat/application/threads/lifecycle-parts.ts @@ -2,7 +2,6 @@ import type { AppServerClient } from "../../../../app-server/connection/client"; import { recoverRolloutTokenUsage } from "../../../../app-server/services/rollout-token-usage"; import type { ChatResumeWorkTracker, ChatViewDeferredTasks } from "../lifecycle"; import type { ChatStateStore } from "../state/store"; -import type { PluginSettingsRef } from "../ports/chat-host"; import type { GoalActions } from "./goal-actions"; import { HistoryController } from "./history-controller"; import { createIdentitySync } from "./identity-sync"; @@ -10,7 +9,7 @@ import { ResumeController } from "./resume-controller"; import { RestorationController } from "./restoration-controller"; export interface ThreadLifecyclePartsContext { - settingsRef: PluginSettingsRef; + settingsRef: { readonly vaultPath: string }; stateStore: ChatStateStore; client: { currentClient: () => AppServerClient | null; diff --git a/src/features/chat/host/composer.ts b/src/features/chat/host/composer.ts index 63677e1b..80e4b6a0 100644 --- a/src/features/chat/host/composer.ts +++ b/src/features/chat/host/composer.ts @@ -6,14 +6,14 @@ import { runtimeSnapshotForChatState } from "../application/runtime/snapshot"; import { activeTurnId } from "../application/state/root-reducer"; import type { ChatStateStore } from "../application/state/store"; import type { ChatPanelComposerShellState } from "../panel/shell-state"; -import type { PluginSettingsRef } from "../application/ports/chat-host"; +import type { CodexPanelSettings } from "../../../settings/model"; import type { ChatRuntimeSettingsActions } from "../application/runtime/settings-actions"; import { ChatComposerController } from "../panel/composer-controller"; import type { ChatPanelComposerProjection } from "../panel/surface/model"; export interface ConversationComposerContext { app: App; - settingsRef: PluginSettingsRef; + settingsRef: { readonly settings: CodexPanelSettings }; stateStore: ChatStateStore; viewId: string; surface: { diff --git a/src/features/chat/host/connection-bundle.ts b/src/features/chat/host/connection-bundle.ts index 84be2f34..a8b308f6 100644 --- a/src/features/chat/host/connection-bundle.ts +++ b/src/features/chat/host/connection-bundle.ts @@ -1,8 +1,9 @@ import { Notice } from "obsidian"; import type { ConnectionManager } from "../../../app-server/connection/connection-manager"; +import type { SharedServerMetadata } from "../../../domain/server/metadata"; +import type { Thread } from "../../../domain/threads/model"; import type { ConnectionWorkTracker } from "../../../shared/lifecycle/connection-work"; -import type { ThreadCatalogFacade } from "../application/ports/chat-host"; import type { ChatViewDeferredTasks } from "../application/lifecycle"; import { ChatConnectionController, handleChatConnectionExit } from "../application/connection/connection-controller"; import { createChatServerDiagnosticsActions, type ChatServerDiagnosticsActions } from "../app-server/actions/diagnostics"; @@ -42,10 +43,13 @@ export interface ChatConnectionBundleContext { vaultPath: string; connectionWork: ConnectionWorkTracker; deferredTasks: ChatViewDeferredTasks; - threadCatalog: Pick< - ThreadCatalogFacade, - "setActiveThreads" | "setAppServerMetadata" | "fetchActiveThreads" | "archiveThreadInCatalog" | "renameThreadInCatalog" - >; + threadCatalog: { + setActiveThreads(threads: readonly Thread[]): void; + setAppServerMetadata(metadata: SharedServerMetadata): void; + fetchActiveThreads(fetchThreads: () => Promise): Promise; + archiveThreadInCatalog(threadId: string): void; + renameThreadInCatalog(threadId: string, name: string | null): void; + }; goalSync: ThreadGoalSyncActions; autoTitle: AutoTitleController; status: ChatConnectionBundleStatus; diff --git a/src/features/chat/host/conversation.ts b/src/features/chat/host/conversation.ts index 5afdc018..1194819c 100644 --- a/src/features/chat/host/conversation.ts +++ b/src/features/chat/host/conversation.ts @@ -8,7 +8,7 @@ import type { GoalActions } from "../application/threads/goal-actions"; import type { HistoryController } from "../application/threads/history-controller"; import type { ChatInboundController } from "../app-server/inbound/controller"; import type { MessageStreamItem, MessageStreamNoticeSection } from "../domain/message-stream/items"; -import type { PluginSettingsRef, WorkspacePanels } from "../application/ports/chat-host"; +import type { ChatTurnDiffViewState } from "../domain/turn-diff"; import { MessageStreamPresenter } from "../panel/surface/message-stream-presenter"; import type { ChatMessageScrollIntentState } from "../panel/surface/message-stream-scroll-intent"; import type { ConversationComposerParts } from "./composer"; @@ -20,8 +20,10 @@ interface ConversationPartsContext { owner: Component; viewId: string; }; - settingsRef: PluginSettingsRef; - workspace: WorkspacePanels; + settingsRef: { readonly vaultPath: string }; + workspace: { + openTurnDiff(state: ChatTurnDiffViewState): Promise; + }; state: { stateStore: ChatStateStore; }; diff --git a/src/features/chat/host/runtime.ts b/src/features/chat/host/runtime.ts index 5f2503ec..33a2f319 100644 --- a/src/features/chat/host/runtime.ts +++ b/src/features/chat/host/runtime.ts @@ -2,7 +2,9 @@ import { Notice, type App, type Component, type EventRef } from "obsidian"; import { ConnectionManager } from "../../../app-server/connection/connection-manager"; import type { ArchiveExportAdapter } from "../../../app-server/services/thread-archive-markdown"; +import type { CodexPanelSettings } from "../../../settings/model"; import type { ConnectionWorkTracker } from "../../../shared/lifecycle/connection-work"; +import type { SharedThreadCatalog } from "../../../workspace/shared-thread-catalog"; import { runtimeSnapshotForChatState } from "../application/runtime/snapshot"; import { createChatRuntimeSettingsActions } from "../application/runtime/settings-actions"; import type { ChatConnectionPhase, ChatAction, ChatState } from "../application/state/root-reducer"; @@ -34,9 +36,43 @@ import type { MessageStreamItem, MessageStreamNoticeSection } from "../domain/me import { pendingRequestsSignature } from "../domain/pending-requests/signatures"; import { ThreadOperations } from "../../threads/thread-operations"; import { ThreadTitleService } from "../../threads/thread-title-service"; -import type { CodexChatHost } from "../application/ports/chat-host"; import { messageStreamItems } from "../application/state/message-stream"; import { threadTitleContextFromMessageStreamItems } from "../application/threads/title-context"; +import type { ChatTurnDiffViewState } from "../domain/turn-diff"; + +export interface CodexChatHost { + readonly settingsRef: PluginSettingsRef; + readonly workspace: WorkspacePanels; + readonly threadCatalog: ChatThreadCatalog; +} + +export interface PluginSettingsRef { + readonly settings: CodexPanelSettings; + readonly vaultPath: string; +} + +interface WorkspacePanels { + openThreadInNewView(threadId: string): Promise; + focusThreadInOpenView(threadId: string): Promise; + openTurnDiff(state: ChatTurnDiffViewState): Promise; +} + +type ChatThreadCatalog = Pick< + SharedThreadCatalog, + | "archiveThreadInCatalog" + | "renameThreadInCatalog" + | "refreshThreadsViewLiveState" + | "refreshFromOpenSurface" + | "setActiveThreads" + | "setAppServerMetadata" + | "fetchActiveThreads" + | "activeThreadsSnapshot" + | "appServerMetadataSnapshot" + | "modelsSnapshot" + | "observeActiveThreads" + | "observeAppServerMetadata" + | "observeModels" +>; export interface ChatPanelEnvironment { obsidian: { diff --git a/src/features/chat/host/view.ts b/src/features/chat/host/view.ts index 2e8fd01e..849a3896 100644 --- a/src/features/chat/host/view.ts +++ b/src/features/chat/host/view.ts @@ -1,7 +1,7 @@ import { ItemView, type ViewStateResult, type WorkspaceLeaf } from "obsidian"; import { VIEW_TYPE_CODEX_PANEL } from "../../../constants"; -import type { CodexChatHost } from "../application/ports/chat-host"; +import type { CodexChatHost } from "./runtime"; import { ChatPanelSession } from "./session"; import type { ChatSurfaceHandle } from "./surface-handle"; diff --git a/src/plugin-runtime.ts b/src/plugin-runtime.ts index 48316504..f9a04f86 100644 --- a/src/plugin-runtime.ts +++ b/src/plugin-runtime.ts @@ -3,15 +3,14 @@ import type { App } from "obsidian"; import { VIEW_TYPE_CODEX_THREADS, VIEW_TYPE_CODEX_TURN_DIFF } from "./constants"; import { AppServerQueryCache } from "./app-server/query/cache"; import type { AppServerQueryContext } from "./app-server/query/keys"; -import type { CodexChatHost, PluginSettingsRef } from "./features/chat/application/ports/chat-host"; import type { ChatTurnDiffViewState } from "./features/chat/domain/turn-diff"; +import type { CodexChatHost, PluginSettingsRef } from "./features/chat/host/runtime"; import { persistedChatTurnDiffViewState } from "./features/chat/domain/turn-diff"; import { CodexChatTurnDiffView } from "./features/chat/ui/turn-diff/view"; import { openThreadPicker, type ThreadPickerHost } from "./features/thread-picker/modal"; -import type { CodexThreadsHost, CodexThreadsView } from "./features/threads-view/view"; +import { CodexThreadsView, type CodexThreadsHost } from "./features/threads-view/view"; import type { CodexPanelSettingTabHost } from "./settings/tab"; import { WorkspacePanelCoordinator } from "./workspace/panel-coordinator"; -import { createThreadSurfaceActions, type ThreadSurfaceActions } from "./workspace/thread-surface-actions"; import { SharedThreadCatalog } from "./workspace/shared-thread-catalog"; export interface CodexPanelRuntimeOptions { @@ -23,7 +22,6 @@ export interface CodexPanelRuntimeOptions { export class CodexPanelRuntime { private readonly appServerQueries = new AppServerQueryCache(); readonly panels: WorkspacePanelCoordinator; - private readonly threadSurfaces: ThreadSurfaceActions; readonly threadCatalog: SharedThreadCatalog; constructor(private readonly options: CodexPanelRuntimeOptions) { @@ -33,13 +31,22 @@ export class CodexPanelRuntime { this.threadCatalog.refreshThreadsViewLiveState(); }, }); - this.threadSurfaces = createThreadSurfaceActions({ - app: options.app, - panels: this.panels, - }); this.threadCatalog = new SharedThreadCatalog({ cache: this.appServerQueries, - surfaces: this.threadSurfaces, + surfaces: { + invalidateThreadsFromOpenSurface: () => { + this.invalidateThreadsFromOpenSurface(); + }, + applyThreadArchived: (threadId, archiveOptions) => { + this.applyThreadArchived(threadId, archiveOptions); + }, + applyThreadRenamed: (threadId, name) => { + this.applyThreadRenamed(threadId, name); + }, + refreshThreadsViewLiveState: () => { + this.refreshThreadsViewLiveState(); + }, + }, context: () => this.appServerQueryContext(), }); } @@ -117,7 +124,7 @@ export class CodexPanelRuntime { vaultPath: this.options.settingsRef.vaultPath, saveSettings: () => this.options.saveSettings(), refreshOpenViews: () => { - this.threadSurfaces.refreshOpenViews(); + this.refreshOpenViews(); }, threadCatalog: this.threadCatalog, }; @@ -145,6 +152,51 @@ export class CodexPanelRuntime { await this.options.app.workspace.revealLeaf(leaf); } + private refreshOpenViews(): void { + for (const view of this.panels.panelViews()) { + view.surface.refreshSettings(); + } + } + + private invalidateThreadsFromOpenSurface(): void { + const chatView = this.panels.panelViews().find((view) => view.surface.openPanelSnapshot().connected); + if (chatView) { + void chatView.surface.refreshSharedThreadList(); + return; + } + + const threadsView = this.threadsViews().at(0); + if (threadsView) void threadsView.refresh(); + } + + private applyThreadArchived(threadId: string, archiveOptions: { closeOpenPanels?: boolean } = {}): void { + const leavesToClose = archiveOptions.closeOpenPanels ? this.panels.panelLeavesForThread(threadId) : []; + for (const view of this.panels.panelViews()) { + view.surface.applyThreadArchived(threadId); + } + for (const leaf of leavesToClose) { + leaf.detach(); + } + } + + private applyThreadRenamed(threadId: string, name: string | null): void { + for (const view of this.panels.panelViews()) { + view.surface.applyThreadRenamed(threadId, name); + } + } + + private refreshThreadsViewLiveState(): void { + for (const view of this.threadsViews()) { + view.refreshLiveState(); + } + } + + private threadsViews(): CodexThreadsView[] { + return this.options.app.workspace + .getLeavesOfType(VIEW_TYPE_CODEX_THREADS) + .flatMap((leaf) => (leaf.view instanceof CodexThreadsView ? [leaf.view] : [])); + } + private appServerQueryContext(): AppServerQueryContext { return { codexPath: this.options.settingsRef.settings.codexPath, diff --git a/src/workspace/shared-thread-catalog.ts b/src/workspace/shared-thread-catalog.ts index 39dd8ca8..75eb73c5 100644 --- a/src/workspace/shared-thread-catalog.ts +++ b/src/workspace/shared-thread-catalog.ts @@ -3,7 +3,13 @@ import type { SharedServerMetadata } from "../domain/server/metadata"; import type { Thread } from "../domain/threads/model"; import type { AppServerQueryCache } from "../app-server/query/cache"; import { appServerQueryContextMatches, cloneAppServerQueryContext, type AppServerQueryContext } from "../app-server/query/keys"; -import type { ThreadSurfaceActions } from "./thread-surface-actions"; + +interface ThreadSurfaceActions { + invalidateThreadsFromOpenSurface(): void; + applyThreadArchived(threadId: string, options?: { closeOpenPanels?: boolean }): void; + applyThreadRenamed(threadId: string, name: string | null): void; + refreshThreadsViewLiveState(): void; +} export interface SharedThreadCatalogOptions { cache: AppServerQueryCache; diff --git a/src/workspace/thread-surface-actions.ts b/src/workspace/thread-surface-actions.ts deleted file mode 100644 index c105e2c9..00000000 --- a/src/workspace/thread-surface-actions.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { App } from "obsidian"; - -import { VIEW_TYPE_CODEX_THREADS } from "../constants"; -import { CodexThreadsView } from "../features/threads-view/view"; -import type { WorkspacePanelCoordinator } from "./panel-coordinator"; - -export interface ThreadSurfaceActionsOptions { - app: App; - panels: WorkspacePanelCoordinator; -} - -export interface ThreadSurfaceActions { - refreshOpenViews(): void; - invalidateThreadsFromOpenSurface(): void; - applyThreadArchived(threadId: string, options?: { closeOpenPanels?: boolean }): void; - applyThreadRenamed(threadId: string, name: string | null): void; - refreshThreadsViewLiveState(): void; -} - -export function createThreadSurfaceActions(options: ThreadSurfaceActionsOptions): ThreadSurfaceActions { - const threadsViews = (): CodexThreadsView[] => - options.app.workspace - .getLeavesOfType(VIEW_TYPE_CODEX_THREADS) - .flatMap((leaf) => (leaf.view instanceof CodexThreadsView ? [leaf.view] : [])); - - const invalidateThreadsFromOpenSurface = (): void => { - const chatView = options.panels.panelViews().find((view) => view.surface.openPanelSnapshot().connected); - if (chatView) { - void chatView.surface.refreshSharedThreadList(); - return; - } - - const threadsView = threadsViews().at(0); - if (threadsView) void threadsView.refresh(); - }; - - return { - refreshOpenViews(): void { - for (const view of options.panels.panelViews()) { - view.surface.refreshSettings(); - } - }, - - invalidateThreadsFromOpenSurface, - - applyThreadArchived(threadId: string, archiveOptions: { closeOpenPanels?: boolean } = {}): void { - const leavesToClose = archiveOptions.closeOpenPanels ? options.panels.panelLeavesForThread(threadId) : []; - for (const view of options.panels.panelViews()) { - view.surface.applyThreadArchived(threadId); - } - for (const leaf of leavesToClose) { - leaf.detach(); - } - }, - - applyThreadRenamed(threadId: string, name: string | null): void { - for (const view of options.panels.panelViews()) { - view.surface.applyThreadRenamed(threadId, name); - } - }, - - refreshThreadsViewLiveState(): void { - for (const view of threadsViews()) { - view.refreshLiveState(); - } - }, - }; -} diff --git a/tests/features/chat/view-connection.test.ts b/tests/features/chat/view-connection.test.ts index 03701a03..692f2f66 100644 --- a/tests/features/chat/view-connection.test.ts +++ b/tests/features/chat/view-connection.test.ts @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_SETTINGS } from "../../../src/settings/model"; -import type { CodexChatHost } from "../../../src/features/chat/application/ports/chat-host"; +import type { CodexChatHost } from "../../../src/features/chat/host/runtime"; import { createServerDiagnostics } from "../../../src/domain/server/diagnostics"; import type { Thread } from "../../../src/domain/threads/model"; import type { ModelMetadata } from "../../../src/domain/catalog/metadata"; diff --git a/tests/main.test.ts b/tests/main.test.ts index 741a016c..fc101f61 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -7,7 +7,7 @@ import { VIEW_TYPE_CODEX_PANEL } from "../src/constants"; import { DEFAULT_SETTINGS } from "../src/settings/model"; import type CodexPanelPlugin from "../src/main"; import type { CodexChatView } from "../src/features/chat/host/view"; -import type { CodexChatHost } from "../src/features/chat/application/ports/chat-host"; +import type { CodexChatHost } from "../src/features/chat/host/runtime"; import type { Thread } from "../src/domain/threads/model"; import type { WorkspacePanelCoordinator } from "../src/workspace/panel-coordinator"; import type { SharedThreadCatalog } from "../src/workspace/shared-thread-catalog"; diff --git a/tests/workspace/shared-thread-catalog.test.ts b/tests/workspace/shared-thread-catalog.test.ts index db292126..451cfe97 100644 --- a/tests/workspace/shared-thread-catalog.test.ts +++ b/tests/workspace/shared-thread-catalog.test.ts @@ -6,12 +6,14 @@ import { createServerDiagnostics } from "../../src/domain/server/diagnostics"; import type { SharedServerMetadata } from "../../src/domain/server/metadata"; import type { Thread } from "../../src/domain/threads/model"; import { SharedThreadCatalog } from "../../src/workspace/shared-thread-catalog"; -import type { ThreadSurfaceActions } from "../../src/workspace/thread-surface-actions"; -type MockSurfaceActions = ThreadSurfaceActions & { +interface MockSurfaceActions { + refreshOpenViews: Mock<() => void>; + invalidateThreadsFromOpenSurface: Mock<() => void>; applyThreadArchived: Mock<(threadId: string, options?: { closeOpenPanels?: boolean }) => void>; applyThreadRenamed: Mock<(threadId: string, name: string | null) => void>; -}; + refreshThreadsViewLiveState: Mock<() => void>; +} describe("SharedThreadCatalog", () => { it("applies thread snapshots to the shared cache and active observers", () => { diff --git a/tests/workspace/thread-surface-actions.test.ts b/tests/workspace/thread-surface-actions.test.ts deleted file mode 100644 index a32c9734..00000000 --- a/tests/workspace/thread-surface-actions.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -import { VIEW_TYPE_CODEX_THREADS } from "../../src/constants"; -import { CodexThreadsView } from "../../src/features/threads-view/view"; -import { createThreadSurfaceActions } from "../../src/workspace/thread-surface-actions"; -import type { OpenCodexPanelSnapshot } from "../../src/workspace/open-panel-snapshot"; - -describe("createThreadSurfaceActions", () => { - it("falls back to the threads view when no connected chat panel can refresh shared threads", () => { - const disconnectedPanelRefresh = vi.fn().mockResolvedValue(undefined); - const threadsRefresh = vi.fn().mockResolvedValue(undefined); - const threadSurfaces = createThreadSurfaceActions({ - app: { - workspace: { - getLeavesOfType: vi.fn((type: string) => - type === VIEW_TYPE_CODEX_THREADS ? [{ view: threadsView({ refresh: threadsRefresh }) }] : [], - ), - }, - } as never, - panels: { - panelViews: () => [ - { - surface: { - openPanelSnapshot: () => panelSnapshot({ connected: false }), - refreshSharedThreadList: disconnectedPanelRefresh, - }, - }, - ], - } as never, - }); - - threadSurfaces.invalidateThreadsFromOpenSurface(); - - expect(disconnectedPanelRefresh).not.toHaveBeenCalled(); - expect(threadsRefresh).toHaveBeenCalledOnce(); - }); - - it("uses a connected chat panel before falling back to the threads view", () => { - const disconnectedPanelRefresh = vi.fn().mockResolvedValue(undefined); - const connectedPanelRefresh = vi.fn().mockResolvedValue(undefined); - const threadsRefresh = vi.fn().mockResolvedValue(undefined); - const threadSurfaces = createThreadSurfaceActions({ - app: { - workspace: { - getLeavesOfType: vi.fn((type: string) => - type === VIEW_TYPE_CODEX_THREADS ? [{ view: threadsView({ refresh: threadsRefresh }) }] : [], - ), - }, - } as never, - panels: { - panelViews: () => [ - { - surface: { - openPanelSnapshot: () => panelSnapshot({ viewId: "disconnected", connected: false }), - refreshSharedThreadList: disconnectedPanelRefresh, - }, - }, - { - surface: { - openPanelSnapshot: () => panelSnapshot({ viewId: "connected", connected: true }), - refreshSharedThreadList: connectedPanelRefresh, - }, - }, - ], - } as never, - }); - - threadSurfaces.invalidateThreadsFromOpenSurface(); - - expect(disconnectedPanelRefresh).not.toHaveBeenCalled(); - expect(connectedPanelRefresh).toHaveBeenCalledOnce(); - expect(threadsRefresh).not.toHaveBeenCalled(); - }); - - it("applies known thread mutations without refreshing thread lists", () => { - const panel = { - surface: { - openPanelSnapshot: () => panelSnapshot({ connected: true }), - refreshSharedThreadList: vi.fn().mockResolvedValue(undefined), - applyThreadArchived: vi.fn(), - applyThreadRenamed: vi.fn(), - }, - }; - const threadSurfaces = createThreadSurfaceActions({ - app: { - workspace: { - getLeavesOfType: vi.fn(() => []), - }, - } as never, - panels: { - panelViews: () => [panel], - panelLeavesForThread: vi.fn(() => []), - } as never, - }); - - threadSurfaces.applyThreadRenamed("thread", "Renamed"); - threadSurfaces.applyThreadArchived("thread"); - - expect(panel.surface.applyThreadRenamed).toHaveBeenCalledWith("thread", "Renamed"); - expect(panel.surface.applyThreadArchived).toHaveBeenCalledWith("thread"); - expect(panel.surface.refreshSharedThreadList).not.toHaveBeenCalled(); - }); -}); - -function threadsView(overrides: Partial = {}): CodexThreadsView { - return Object.assign(Object.create(CodexThreadsView.prototype), overrides) as CodexThreadsView; -} - -function panelSnapshot(overrides: Partial = {}): OpenCodexPanelSnapshot { - return { - viewId: "panel", - threadId: "thread", - lastFocused: false, - turnLifecycle: { kind: "idle" }, - pendingApprovals: 0, - pendingUserInputs: 0, - hasComposerDraft: false, - connected: true, - ...overrides, - }; -}