mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Separate workspace panel live-state contracts
This commit is contained in:
parent
56b4c7d0b9
commit
32fb1d629c
20 changed files with 257 additions and 217 deletions
|
|
@ -67,6 +67,14 @@
|
|||
"path": "./scripts/lint/no-chat-workspace-boundary-imports.grit",
|
||||
"includes": ["**/src/features/chat/**/*.ts", "**/src/features/chat/**/*.tsx"]
|
||||
},
|
||||
{
|
||||
"path": "./scripts/lint/no-feature-workspace-boundary-imports.grit",
|
||||
"includes": ["**/src/features/**/*.ts", "**/src/features/**/*.tsx", "!**/src/features/chat/**"]
|
||||
},
|
||||
{
|
||||
"path": "./scripts/lint/no-workspace-chat-internal-imports.grit",
|
||||
"includes": ["**/src/workspace/**/*.ts"]
|
||||
},
|
||||
{
|
||||
"path": "./scripts/lint/no-chat-panel-runtime-boundary-imports.grit",
|
||||
"includes": ["**/src/features/chat/panel/**/*.ts", "**/src/features/chat/panel/**/*.tsx"]
|
||||
|
|
|
|||
16
scripts/lint/no-feature-workspace-boundary-imports.grit
Normal file
16
scripts/lint/no-feature-workspace-boundary-imports.grit
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
language js
|
||||
|
||||
private pattern js_module_reference() {
|
||||
or {
|
||||
JsImport(),
|
||||
JsExportNamedFromClause(),
|
||||
JsExportFromClause(),
|
||||
TsImportType(),
|
||||
JsImportCallExpression()
|
||||
}
|
||||
}
|
||||
|
||||
js_module_reference() as $stmt where {
|
||||
$stmt <: contains `$source` where { $source <: r"^[\"'](?:(?:\.\./)+workspace|src/workspace)(?:/.*)?[\"']$" },
|
||||
register_diagnostic(span=$stmt, message="Feature modules must not import workspace modules; pass workspace capabilities through feature host contracts.", severity="error")
|
||||
}
|
||||
18
scripts/lint/no-workspace-chat-internal-imports.grit
Normal file
18
scripts/lint/no-workspace-chat-internal-imports.grit
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
language js
|
||||
|
||||
private pattern js_module_reference() {
|
||||
or {
|
||||
JsImport(),
|
||||
JsExportNamedFromClause(),
|
||||
JsExportFromClause(),
|
||||
TsImportType(),
|
||||
JsImportCallExpression()
|
||||
}
|
||||
}
|
||||
|
||||
js_module_reference() as $stmt where {
|
||||
$stmt <: contains `$source` where {
|
||||
$source <: r"^[\"'](?:\.\./features/chat/(?:application|app-server|domain|panel|presentation|ui)|src/features/chat/(?:application|app-server|domain|panel|presentation|ui))(?:/.*)?[\"']$"
|
||||
},
|
||||
register_diagnostic(span=$stmt, message="Workspace modules may coordinate chat only through chat host contracts and Obsidian views.", severity="error")
|
||||
}
|
||||
|
|
@ -35,3 +35,16 @@ export function transitionRestoredThreadLifecycle(
|
|||
return state.kind === "idle" ? state : { kind: "idle" };
|
||||
}
|
||||
}
|
||||
|
||||
export function parseRestoredThreadState(state: unknown): RestoredThreadState | null {
|
||||
if (!state || typeof state !== "object") return null;
|
||||
const record = state as Record<string, unknown>;
|
||||
const threadId = record["threadId"];
|
||||
if (typeof threadId !== "string" || threadId.trim().length === 0) return null;
|
||||
const title = record["threadTitle"];
|
||||
return {
|
||||
threadId,
|
||||
title: typeof title === "string" && title.trim().length > 0 ? title : null,
|
||||
explicitName: null,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import type { AppServerQueryContext } from "../../../app-server/query/keys";
|
|||
import type { ThreadCatalogActiveReader, ThreadCatalogEventSink } from "../../../app-server/query/thread-catalog";
|
||||
import type { ArchiveExportDestination } from "../../../app-server/services/thread-archive-markdown";
|
||||
import type { ModelMetadata } from "../../../domain/catalog/metadata";
|
||||
import type { PendingRequestCounts } from "../../../domain/pending-requests/aggregate";
|
||||
import type { SharedServerMetadata } from "../../../domain/server/metadata";
|
||||
import type { CodexPanelSettings } from "../../../settings/model";
|
||||
import type { ObservedResultListener } from "../../../shared/query/observed-result";
|
||||
import type { ChatTurnDiffViewState } from "../domain/turn-diff";
|
||||
import type { ChatPanelSnapshot } from "../panel/snapshot";
|
||||
|
||||
export interface CodexChatHost {
|
||||
readonly settingsRef: PluginSettingsRef;
|
||||
|
|
@ -70,8 +70,19 @@ export interface ChatViewLifecycleSurface {
|
|||
refreshSettings(): void;
|
||||
}
|
||||
|
||||
export type ChatWorkspacePanelTurnLifecycle = { kind: "idle" } | { kind: "starting" } | { kind: "running"; turnId: string };
|
||||
|
||||
export interface ChatWorkspacePanelSnapshot {
|
||||
viewId: string;
|
||||
threadId: string | null;
|
||||
turnLifecycle: ChatWorkspacePanelTurnLifecycle;
|
||||
pendingRequests: PendingRequestCounts;
|
||||
hasComposerDraft: boolean;
|
||||
connected: boolean;
|
||||
}
|
||||
|
||||
export interface ChatWorkspacePanelSurface {
|
||||
openPanelSnapshot(): ChatPanelSnapshot;
|
||||
openPanelSnapshot(): ChatWorkspacePanelSnapshot;
|
||||
openThread(threadId: string): Promise<void>;
|
||||
focusThread(threadId?: string | null): Promise<void>;
|
||||
hydrateRestoredThread(): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@ import { threadMeaningfulTitle, threadWindowTitle } from "../../../domain/thread
|
|||
import { ConnectionWorkTracker } from "../../../shared/lifecycle/connection-work";
|
||||
import type { ChatState } from "../application/state/root-reducer";
|
||||
import { type ChatStateStore, createChatStateStore } from "../application/state/store";
|
||||
import type { RestoredThreadPlaceholderState } from "../application/threads/restored-thread-lifecycle";
|
||||
import { parseRestoredThreadState, type RestoredThreadPlaceholderState } from "../application/threads/restored-thread-lifecycle";
|
||||
import { ChatResumeWorkTracker } from "../application/threads/resume-work";
|
||||
import { renderChatPanelShell, unmountChatPanelShell } from "../panel/shell.dom";
|
||||
import { type ChatPanelSnapshot, openPanelTurnLifecycle, parseRestoredThreadState } from "../panel/snapshot";
|
||||
import { type ChatMessageScrollController, createChatMessageScrollController } from "../panel/surface/message-stream-scroll";
|
||||
import type { ChatPanelEnvironment, ChatPanelHandle } from "./contracts";
|
||||
import type { ChatPanelEnvironment, ChatPanelHandle, ChatWorkspacePanelSnapshot } from "./contracts";
|
||||
import { type ChatViewDeferredTasks, createChatViewDeferredTasks } from "./deferred-work";
|
||||
import { type ChatPanelSessionGraph, createChatPanelSessionGraph } from "./session-graph";
|
||||
import { openPanelTurnLifecycle } from "./workspace-panel-snapshot";
|
||||
|
||||
export class ChatPanelSession implements ChatPanelHandle {
|
||||
private readonly stateStore: ChatStateStore = createChatStateStore();
|
||||
|
|
@ -100,15 +100,13 @@ export class ChatPanelSession implements ChatPanelHandle {
|
|||
return result;
|
||||
}
|
||||
|
||||
openPanelSnapshot(): ChatPanelSnapshot {
|
||||
openPanelSnapshot(): ChatWorkspacePanelSnapshot {
|
||||
const pendingRequests = pendingRequestCountsFromQueues(this.state.requests);
|
||||
return {
|
||||
viewId: this.environment.obsidian.viewId,
|
||||
threadId: this.closing ? null : this.panelThreadId(),
|
||||
turnLifecycle: openPanelTurnLifecycle(this.state.turn.lifecycle),
|
||||
pendingApprovals: pendingRequests.approvals,
|
||||
pendingUserInputs: pendingRequests.userInputs,
|
||||
pendingMcpElicitations: pendingRequests.mcpElicitations,
|
||||
pendingRequests,
|
||||
hasComposerDraft: this.state.composer.draft.trim().length > 0,
|
||||
connected: this.graph.connection.manager.isConnected(),
|
||||
};
|
||||
|
|
|
|||
8
src/features/chat/host/workspace-panel-snapshot.ts
Normal file
8
src/features/chat/host/workspace-panel-snapshot.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import type { ChatState } from "../application/state/root-reducer";
|
||||
import type { ChatWorkspacePanelTurnLifecycle } from "./contracts";
|
||||
|
||||
export function openPanelTurnLifecycle(state: ChatState["turn"]["lifecycle"]): ChatWorkspacePanelTurnLifecycle {
|
||||
if (state.kind === "running") return { kind: "running", turnId: state.turnId };
|
||||
if (state.kind === "starting") return { kind: "starting" };
|
||||
return { kind: "idle" };
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import type { ChatState } from "../application/state/root-reducer";
|
||||
import type { RestoredThreadState } from "../application/threads/restored-thread-lifecycle";
|
||||
|
||||
type OpenCodexPanelTurnLifecycle = { kind: "idle" } | { kind: "starting" } | { kind: "running"; turnId: string };
|
||||
|
||||
export interface ChatPanelSnapshot {
|
||||
viewId: string;
|
||||
threadId: string | null;
|
||||
turnLifecycle: OpenCodexPanelTurnLifecycle;
|
||||
pendingApprovals: number;
|
||||
pendingUserInputs: number;
|
||||
pendingMcpElicitations: number;
|
||||
hasComposerDraft: boolean;
|
||||
connected: boolean;
|
||||
}
|
||||
|
||||
export function openPanelTurnLifecycle(state: ChatState["turn"]["lifecycle"]): ChatPanelSnapshot["turnLifecycle"] {
|
||||
if (state.kind === "running") return { kind: "running", turnId: state.turnId };
|
||||
if (state.kind === "starting") return { kind: "starting" };
|
||||
return { kind: "idle" };
|
||||
}
|
||||
|
||||
export function parseRestoredThreadState(state: unknown): RestoredThreadState | null {
|
||||
if (!state || typeof state !== "object") return null;
|
||||
const record = state as Record<string, unknown>;
|
||||
const threadId = record["threadId"];
|
||||
if (typeof threadId !== "string" || threadId.trim().length === 0) return null;
|
||||
const title = record["threadTitle"];
|
||||
return {
|
||||
threadId,
|
||||
title: typeof title === "string" && title.trim().length > 0 ? title : null,
|
||||
explicitName: null,
|
||||
};
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ import type { ArchiveExportSettings } from "../../domain/threads/archive-markdow
|
|||
import type { Thread } from "../../domain/threads/model";
|
||||
import type { ObservedResult } from "../../shared/query/observed-result";
|
||||
import { observedInitialError, observedInitialLoading, observedValue } from "../../shared/query/observed-result";
|
||||
import type { OpenCodexPanelSnapshot } from "../../workspace/panel-coordinator";
|
||||
import { createThreadOperations, type ThreadOperations } from "../threads/thread-operations";
|
||||
import { createThreadTitleService, type ThreadTitleService } from "../threads/thread-title-service";
|
||||
import { isThreadsArchiveConfirmPointer, renderThreadsViewShell, unmountThreadsViewShell } from "./shell.dom";
|
||||
|
|
@ -17,6 +16,7 @@ import {
|
|||
type ThreadsGeneratingRenameState,
|
||||
type ThreadsRenameLifecycleEvent,
|
||||
type ThreadsRenameState,
|
||||
type ThreadsViewPanelActivity,
|
||||
threadRows,
|
||||
transitionThreadsRenameState,
|
||||
} from "./state";
|
||||
|
|
@ -35,7 +35,7 @@ export interface ThreadsViewHost {
|
|||
readonly threadCatalog: ThreadsViewThreadCatalog;
|
||||
openNewPanel(): Promise<unknown>;
|
||||
openThreadInAvailableView(threadId: string): Promise<void>;
|
||||
getOpenPanelSnapshots(): OpenCodexPanelSnapshot[];
|
||||
openPanelActivities(): readonly ThreadsViewPanelActivity[];
|
||||
}
|
||||
|
||||
type ThreadsViewThreadCatalog = ThreadCatalogActiveReader & ThreadCatalogEventSink;
|
||||
|
|
@ -210,7 +210,7 @@ export class ThreadsViewSession {
|
|||
loading: this.refreshLifecycle.kind === "loading",
|
||||
rows: threadRows(
|
||||
this.threads,
|
||||
this.host.getOpenPanelSnapshots(),
|
||||
this.host.openPanelActivities(),
|
||||
this.renameStates,
|
||||
this.archiveConfirmThreadId,
|
||||
this.host.settings.archiveExportEnabled(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import { hasPendingRequests, pendingRequestCounts } from "../../domain/pending-requests/aggregate";
|
||||
import { type Thread, threadRecencyAt } from "../../domain/threads/model";
|
||||
import type { OpenCodexPanelSnapshot } from "../../workspace/panel-coordinator";
|
||||
import {
|
||||
initialThreadRenameLifecycleState,
|
||||
type ThreadRenameLifecycleEvent as SharedThreadRenameLifecycleEvent,
|
||||
|
|
@ -17,6 +15,13 @@ interface ThreadsLiveState {
|
|||
status: ThreadsLiveStatus;
|
||||
}
|
||||
|
||||
export interface ThreadsViewPanelActivity {
|
||||
threadId: string | null;
|
||||
selected: boolean;
|
||||
pending: boolean;
|
||||
running: boolean;
|
||||
}
|
||||
|
||||
export interface ThreadsRowModel extends ThreadRowCoreProjection {
|
||||
live: ThreadsLiveState | null;
|
||||
}
|
||||
|
|
@ -40,20 +45,20 @@ const STATUS_PRIORITY: Record<ThreadsLiveStatus, number> = {
|
|||
|
||||
export function threadRows(
|
||||
threads: readonly Thread[],
|
||||
snapshots: OpenCodexPanelSnapshot[],
|
||||
panelActivities: readonly ThreadsViewPanelActivity[],
|
||||
renameStates: ReadonlyMap<string, ThreadsRenameState>,
|
||||
archiveConfirmThreadId: string | null = null,
|
||||
defaultArchiveSaveMarkdown = false,
|
||||
): ThreadsRowModel[] {
|
||||
const snapshotsByThread = snapshotsForThreads(snapshots);
|
||||
const panelActivitiesByThread = panelActivitiesForThreads(panelActivities);
|
||||
return [...threads]
|
||||
.sort((a, b) => threadRecencyAt(b) - threadRecencyAt(a))
|
||||
.map((thread) => {
|
||||
const threadSnapshots = snapshotsByThread.get(thread.id) ?? [];
|
||||
const live = liveStateForSnapshots(threadSnapshots);
|
||||
const threadPanelActivities = panelActivitiesByThread.get(thread.id) ?? [];
|
||||
const live = liveStateForPanelActivities(threadPanelActivities);
|
||||
const core = threadRowCoreProjection({
|
||||
thread,
|
||||
selected: threadSnapshots.some((snapshot) => snapshot.threadId !== null && snapshot.lastFocused),
|
||||
selected: threadPanelActivities.some((activity) => activity.threadId !== null && activity.selected),
|
||||
renameState: renameStates.get(thread.id),
|
||||
archiveConfirmActive: archiveConfirmThreadId === thread.id,
|
||||
defaultArchiveSaveMarkdown,
|
||||
|
|
@ -65,12 +70,14 @@ export function threadRows(
|
|||
});
|
||||
}
|
||||
|
||||
function liveStateForSnapshots(snapshots: OpenCodexPanelSnapshot[]): ThreadsLiveState | null {
|
||||
const liveSnapshots = snapshots.filter((snapshot) => snapshot.threadId !== null);
|
||||
if (liveSnapshots.length === 0) return null;
|
||||
const winner = [...liveSnapshots].sort((a, b) => STATUS_PRIORITY[snapshotStatus(b)] - STATUS_PRIORITY[snapshotStatus(a)]).at(0);
|
||||
function liveStateForPanelActivities(panelActivities: ThreadsViewPanelActivity[]): ThreadsLiveState | null {
|
||||
const livePanelActivities = panelActivities.filter((activity) => activity.threadId !== null);
|
||||
if (livePanelActivities.length === 0) return null;
|
||||
const winner = [...livePanelActivities]
|
||||
.sort((a, b) => STATUS_PRIORITY[panelActivityStatus(b)] - STATUS_PRIORITY[panelActivityStatus(a)])
|
||||
.at(0);
|
||||
if (!winner) return null;
|
||||
const status = snapshotStatus(winner);
|
||||
const status = panelActivityStatus(winner);
|
||||
return {
|
||||
status,
|
||||
};
|
||||
|
|
@ -106,19 +113,19 @@ function activeThreadsRenameState(state: SharedThreadRenameLifecycleState): Thre
|
|||
return state.kind === "idle" ? undefined : state;
|
||||
}
|
||||
|
||||
function snapshotsForThreads(snapshots: OpenCodexPanelSnapshot[]): Map<string, OpenCodexPanelSnapshot[]> {
|
||||
const map = new Map<string, OpenCodexPanelSnapshot[]>();
|
||||
for (const snapshot of snapshots) {
|
||||
if (!snapshot.threadId) continue;
|
||||
const existing = map.get(snapshot.threadId) ?? [];
|
||||
existing.push(snapshot);
|
||||
map.set(snapshot.threadId, existing);
|
||||
function panelActivitiesForThreads(panelActivities: readonly ThreadsViewPanelActivity[]): Map<string, ThreadsViewPanelActivity[]> {
|
||||
const map = new Map<string, ThreadsViewPanelActivity[]>();
|
||||
for (const activity of panelActivities) {
|
||||
if (!activity.threadId) continue;
|
||||
const existing = map.get(activity.threadId) ?? [];
|
||||
existing.push(activity);
|
||||
map.set(activity.threadId, existing);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function snapshotStatus(snapshot: OpenCodexPanelSnapshot): ThreadsLiveStatus {
|
||||
if (hasPendingRequests(pendingRequestCounts(snapshot))) return "pending";
|
||||
if (snapshot.turnLifecycle.kind !== "idle") return "running";
|
||||
function panelActivityStatus(activity: ThreadsViewPanelActivity): ThreadsLiveStatus {
|
||||
if (activity.pending) return "pending";
|
||||
if (activity.running) return "running";
|
||||
return "open";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { type AppServerQueryContext, appServerQueryContextIsComplete } from "./a
|
|||
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 {
|
||||
|
|
@ -20,6 +21,7 @@ import type {
|
|||
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 type { CodexPanelSettingTabHost } from "./settings/host";
|
||||
import { WorkspacePanelCoordinator } from "./workspace/panel-coordinator";
|
||||
|
|
@ -127,7 +129,7 @@ export class CodexPanelRuntime implements AppServerClientAccess {
|
|||
threadCatalog: this.threadCatalog,
|
||||
openNewPanel: () => this.panels.openNewPanel(),
|
||||
openThreadInAvailableView: (threadId) => this.panels.openThreadInAvailableView(threadId),
|
||||
getOpenPanelSnapshots: () => this.panels.getOpenPanelSnapshots(),
|
||||
openPanelActivities: () => this.openPanelActivities(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -227,6 +229,15 @@ export class CodexPanelRuntime implements AppServerClientAccess {
|
|||
}
|
||||
}
|
||||
|
||||
private openPanelActivities(): readonly ThreadsViewPanelActivity[] {
|
||||
return this.panels.getOpenPanelSnapshots().map((snapshot) => ({
|
||||
threadId: snapshot.threadId,
|
||||
selected: snapshot.lastFocused,
|
||||
pending: hasPendingRequests(snapshot.pendingRequests),
|
||||
running: snapshot.turnLifecycle.kind !== "idle",
|
||||
}));
|
||||
}
|
||||
|
||||
private threadsViews(): CodexThreadsView[] {
|
||||
return this.options.app.workspace
|
||||
.getLeavesOfType(VIEW_TYPE_CODEX_THREADS)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@ import type { App, WorkspaceLeaf } from "obsidian";
|
|||
|
||||
import { VIEW_TYPE_CODEX_PANEL } from "../constants";
|
||||
import { hasPendingRequests, pendingRequestCounts } from "../domain/pending-requests/aggregate";
|
||||
import type { ChatWorkspacePanelSurface } from "../features/chat/host/contracts";
|
||||
import type { ChatWorkspacePanelSnapshot, ChatWorkspacePanelSurface } from "../features/chat/host/contracts";
|
||||
import { CodexChatView } from "../features/chat/host/view.obsidian";
|
||||
import type { ChatPanelSnapshot } from "../features/chat/panel/snapshot";
|
||||
|
||||
type ThreadPanelTarget =
|
||||
| {
|
||||
|
|
@ -42,7 +41,7 @@ interface WorkspacePanelReconcileOptions {
|
|||
|
||||
const ignoreWorkspacePanelLoadError = (): void => undefined;
|
||||
|
||||
export interface OpenCodexPanelSnapshot extends ChatPanelSnapshot {
|
||||
export interface WorkspacePanelSnapshot extends ChatWorkspacePanelSnapshot {
|
||||
lastFocused: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -127,7 +126,7 @@ export class WorkspacePanelCoordinator {
|
|||
return true;
|
||||
}
|
||||
|
||||
getOpenPanelSnapshots(): OpenCodexPanelSnapshot[] {
|
||||
getOpenPanelSnapshots(): WorkspacePanelSnapshot[] {
|
||||
const leaves = this.panelLeaves();
|
||||
this.ensureInitialFocusedPanel(leaves);
|
||||
return leaves.flatMap((leaf, index) => {
|
||||
|
|
@ -370,7 +369,7 @@ export class WorkspacePanelCoordinator {
|
|||
if (viewId) this.lastFocusedPanelViewId = viewId;
|
||||
}
|
||||
|
||||
private openPanelSnapshotWithFocus(snapshot: ChatPanelSnapshot): OpenCodexPanelSnapshot {
|
||||
private openPanelSnapshotWithFocus(snapshot: ChatWorkspacePanelSnapshot): WorkspacePanelSnapshot {
|
||||
return { ...snapshot, lastFocused: snapshot.viewId === this.lastFocusedPanelViewId };
|
||||
}
|
||||
|
||||
|
|
@ -417,11 +416,11 @@ export class WorkspacePanelCoordinator {
|
|||
}
|
||||
}
|
||||
|
||||
function isIdleEmptyPanelSnapshot(snapshot: ChatPanelSnapshot): boolean {
|
||||
function isIdleEmptyPanelSnapshot(snapshot: ChatWorkspacePanelSnapshot): boolean {
|
||||
return (
|
||||
snapshot.threadId === null &&
|
||||
snapshot.turnLifecycle.kind === "idle" &&
|
||||
!hasPendingRequests(pendingRequestCounts(snapshot)) &&
|
||||
!hasPendingRequests(snapshot.pendingRequests) &&
|
||||
!snapshot.hasComposerDraft
|
||||
);
|
||||
}
|
||||
|
|
@ -441,16 +440,14 @@ function restoredThreadId(leaf: WorkspaceLeaf): string | null {
|
|||
return typeof threadId === "string" && threadId.length > 0 ? threadId : null;
|
||||
}
|
||||
|
||||
function restoredPanelSnapshot(leaf: WorkspaceLeaf, index: number): OpenCodexPanelSnapshot | null {
|
||||
function restoredPanelSnapshot(leaf: WorkspaceLeaf, index: number): WorkspacePanelSnapshot | null {
|
||||
const threadId = restoredThreadId(leaf);
|
||||
if (!threadId) return null;
|
||||
return {
|
||||
viewId: `restored:${String(index)}:${threadId}`,
|
||||
threadId,
|
||||
turnLifecycle: { kind: "idle" },
|
||||
pendingApprovals: 0,
|
||||
pendingUserInputs: 0,
|
||||
pendingMcpElicitations: 0,
|
||||
pendingRequests: pendingRequestCounts({ pendingApprovals: 0, pendingUserInputs: 0, pendingMcpElicitations: 0 }),
|
||||
hasComposerDraft: false,
|
||||
connected: false,
|
||||
lastFocused: false,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { transitionRestoredThreadLifecycle } from "../../../../../src/features/chat/application/threads/restored-thread-lifecycle";
|
||||
import {
|
||||
parseRestoredThreadState,
|
||||
transitionRestoredThreadLifecycle,
|
||||
} from "../../../../../src/features/chat/application/threads/restored-thread-lifecycle";
|
||||
|
||||
describe("transitionRestoredThreadLifecycle", () => {
|
||||
it("clears restored-thread loading only for the active loading promise", () => {
|
||||
|
|
@ -23,4 +26,14 @@ describe("transitionRestoredThreadLifecycle", () => {
|
|||
loading: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("parses restored thread view state defensively", () => {
|
||||
expect(parseRestoredThreadState({ threadId: "thread", threadTitle: "Title" })).toEqual({
|
||||
threadId: "thread",
|
||||
title: "Title",
|
||||
explicitName: null,
|
||||
});
|
||||
expect(parseRestoredThreadState({ threadId: "" })).toBeNull();
|
||||
expect(parseRestoredThreadState(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
13
tests/features/chat/host/workspace-panel-snapshot.test.ts
Normal file
13
tests/features/chat/host/workspace-panel-snapshot.test.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { openPanelTurnLifecycle } from "../../../../src/features/chat/host/workspace-panel-snapshot";
|
||||
|
||||
describe("chat workspace panel snapshots", () => {
|
||||
it("projects open panel turn lifecycle without exposing full chat state", () => {
|
||||
expect(openPanelTurnLifecycle({ kind: "idle" })).toEqual({ kind: "idle" });
|
||||
expect(openPanelTurnLifecycle({ kind: "starting", pendingTurnStart: { anchorItemId: "local", promptSubmitHookItemIds: [] } })).toEqual({
|
||||
kind: "starting",
|
||||
});
|
||||
expect(openPanelTurnLifecycle({ kind: "running", turnId: "turn" })).toEqual({ kind: "running", turnId: "turn" });
|
||||
});
|
||||
});
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { openPanelTurnLifecycle, parseRestoredThreadState } from "../../../../src/features/chat/panel/snapshot";
|
||||
|
||||
describe("chat view snapshots", () => {
|
||||
it("projects open panel turn lifecycle without exposing full chat state", () => {
|
||||
expect(openPanelTurnLifecycle({ kind: "idle" })).toEqual({ kind: "idle" });
|
||||
expect(openPanelTurnLifecycle({ kind: "starting", pendingTurnStart: { anchorItemId: "local", promptSubmitHookItemIds: [] } })).toEqual({
|
||||
kind: "starting",
|
||||
});
|
||||
expect(openPanelTurnLifecycle({ kind: "running", turnId: "turn" })).toEqual({ kind: "running", turnId: "turn" });
|
||||
});
|
||||
|
||||
it("parses restored thread view state defensively", () => {
|
||||
expect(parseRestoredThreadState({ threadId: "thread", threadTitle: "Title" })).toEqual({
|
||||
threadId: "thread",
|
||||
title: "Title",
|
||||
explicitName: null,
|
||||
});
|
||||
expect(parseRestoredThreadState({ threadId: "" })).toBeNull();
|
||||
expect(parseRestoredThreadState(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -3,8 +3,7 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { Thread } from "../../../src/domain/threads/model";
|
||||
import { renderThreadsViewShell } from "../../../src/features/threads-view/shell.dom";
|
||||
import { type ThreadsRowModel, threadRows } from "../../../src/features/threads-view/state";
|
||||
import type { OpenCodexPanelSnapshot } from "../../../src/workspace/panel-coordinator";
|
||||
import { type ThreadsRowModel, type ThreadsViewPanelActivity, threadRows } from "../../../src/features/threads-view/state";
|
||||
import { changeInputValue, installObsidianDomShims } from "../../support/dom";
|
||||
|
||||
installObsidianDomShims();
|
||||
|
|
@ -14,29 +13,12 @@ function expectPresent<T>(value: T | null | undefined): T {
|
|||
return value;
|
||||
}
|
||||
|
||||
function openPanelSnapshot(
|
||||
overrides: Partial<{
|
||||
viewId: string;
|
||||
threadId: string | null;
|
||||
turnLifecycle: OpenCodexPanelSnapshot["turnLifecycle"];
|
||||
pendingApprovals: number;
|
||||
pendingUserInputs: number;
|
||||
pendingMcpElicitations: number;
|
||||
hasComposerDraft: boolean;
|
||||
connected: boolean;
|
||||
lastFocused: boolean;
|
||||
}> = {},
|
||||
): OpenCodexPanelSnapshot {
|
||||
function panelActivity(overrides: Partial<ThreadsViewPanelActivity> = {}): ThreadsViewPanelActivity {
|
||||
return {
|
||||
viewId: "view",
|
||||
threadId: "thread",
|
||||
lastFocused: false,
|
||||
turnLifecycle: { kind: "idle" },
|
||||
pendingApprovals: 0,
|
||||
pendingUserInputs: 0,
|
||||
pendingMcpElicitations: 0,
|
||||
hasComposerDraft: false,
|
||||
connected: true,
|
||||
selected: false,
|
||||
pending: false,
|
||||
running: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
|
@ -88,38 +70,19 @@ describe("threads view renderer decisions", () => {
|
|||
threadRows(
|
||||
[threadFixture({ id: "thread" })],
|
||||
[
|
||||
openPanelSnapshot({ viewId: "open", threadId: "thread" }),
|
||||
openPanelSnapshot({ viewId: "running", threadId: "thread", turnLifecycle: { kind: "running", turnId: "turn" } }),
|
||||
openPanelSnapshot({ viewId: "pending", threadId: "thread", pendingApprovals: 1 }),
|
||||
panelActivity({ threadId: "thread" }),
|
||||
panelActivity({ threadId: "thread", running: true }),
|
||||
panelActivity({ threadId: "thread", pending: true }),
|
||||
],
|
||||
new Map(),
|
||||
)[0]?.live,
|
||||
).toMatchObject({ status: "pending" });
|
||||
|
||||
expect(
|
||||
threadRows(
|
||||
[threadFixture({ id: "thread" })],
|
||||
[openPanelSnapshot({ viewId: "draft", threadId: "thread", hasComposerDraft: true })],
|
||||
new Map(),
|
||||
)[0]?.live,
|
||||
).toMatchObject({
|
||||
expect(threadRows([threadFixture({ id: "thread" })], [panelActivity({ threadId: "thread" })], new Map())[0]?.live).toMatchObject({
|
||||
status: "open",
|
||||
});
|
||||
expect(
|
||||
threadRows(
|
||||
[threadFixture({ id: "thread" })],
|
||||
[openPanelSnapshot({ viewId: "offline", threadId: "thread", connected: false })],
|
||||
new Map(),
|
||||
)[0]?.live,
|
||||
).toMatchObject({
|
||||
status: "open",
|
||||
});
|
||||
expect(
|
||||
threadRows(
|
||||
[threadFixture({ id: "thread" })],
|
||||
[openPanelSnapshot({ viewId: "none", threadId: null, turnLifecycle: { kind: "running", turnId: "turn" } })],
|
||||
new Map(),
|
||||
)[0]?.live,
|
||||
threadRows([threadFixture({ id: "thread" })], [panelActivity({ threadId: null, running: true })], new Map())[0]?.live,
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
|
|
@ -129,7 +92,7 @@ describe("threads view renderer decisions", () => {
|
|||
threadFixture({ id: "closed", preview: "Closed thread" }),
|
||||
threadFixture({ id: "focused", preview: "Focused thread", updatedAt: 2 }),
|
||||
],
|
||||
[openPanelSnapshot({ viewId: "view-focused", threadId: "focused", lastFocused: true })],
|
||||
[panelActivity({ threadId: "focused", selected: true })],
|
||||
new Map(),
|
||||
);
|
||||
|
||||
|
|
@ -142,7 +105,7 @@ describe("threads view renderer decisions", () => {
|
|||
const actions = threadsViewActions();
|
||||
const rows = threadRows(
|
||||
[threadFixture({ id: "closed", preview: "Closed thread" }), threadFixture({ id: "open", preview: "Open thread", updatedAt: 2 })],
|
||||
[openPanelSnapshot({ viewId: "view-open", threadId: "open", pendingApprovals: 1, lastFocused: true })],
|
||||
[panelActivity({ threadId: "open", pending: true, selected: true })],
|
||||
new Map(),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { Thread } from "../../../src/domain/threads/model";
|
||||
import { threadRows, transitionThreadsRenameState } from "../../../src/features/threads-view/state";
|
||||
import type { OpenCodexPanelSnapshot } from "../../../src/workspace/panel-coordinator";
|
||||
import { type ThreadsViewPanelActivity, threadRows, transitionThreadsRenameState } from "../../../src/features/threads-view/state";
|
||||
|
||||
describe("threads view rename state", () => {
|
||||
it("maps threads view auto-name events through the shared rename lifecycle", () => {
|
||||
|
|
@ -47,7 +46,7 @@ describe("threads view rename state", () => {
|
|||
});
|
||||
|
||||
it("treats pending MCP elicitations as pending live state", () => {
|
||||
const rows = threadRows([thread()], [openPanelSnapshot({ pendingMcpElicitations: 1 })], new Map());
|
||||
const rows = threadRows([thread()], [panelActivity({ pending: true })], new Map());
|
||||
|
||||
expect(rows[0]?.live).toMatchObject({ status: "pending" });
|
||||
});
|
||||
|
|
@ -65,17 +64,12 @@ function thread(overrides: Partial<Thread> = {}): Thread {
|
|||
};
|
||||
}
|
||||
|
||||
function openPanelSnapshot(overrides: Partial<OpenCodexPanelSnapshot> = {}): OpenCodexPanelSnapshot {
|
||||
function panelActivity(overrides: Partial<ThreadsViewPanelActivity> = {}): ThreadsViewPanelActivity {
|
||||
return {
|
||||
viewId: "view",
|
||||
threadId: "thread",
|
||||
turnLifecycle: { kind: "idle" },
|
||||
pendingApprovals: 0,
|
||||
pendingUserInputs: 0,
|
||||
pendingMcpElicitations: 0,
|
||||
hasComposerDraft: false,
|
||||
connected: true,
|
||||
lastFocused: false,
|
||||
selected: false,
|
||||
pending: false,
|
||||
running: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -539,7 +539,7 @@ function threadsHost(overrides: Record<string, unknown> = {}) {
|
|||
},
|
||||
openNewPanel: vi.fn().mockResolvedValue(undefined),
|
||||
openThreadInAvailableView: vi.fn().mockResolvedValue(undefined),
|
||||
getOpenPanelSnapshots: vi.fn(() => []),
|
||||
openPanelActivities: vi.fn(() => []),
|
||||
threadCatalog: {
|
||||
apply: vi.fn(),
|
||||
loadActive: vi.fn(async () => []),
|
||||
|
|
|
|||
|
|
@ -107,9 +107,7 @@ describe("CodexPanelPlugin workspace panel reconciliation", () => {
|
|||
{
|
||||
threadId: "thread-1",
|
||||
turnLifecycle: { kind: "idle" },
|
||||
pendingApprovals: 0,
|
||||
pendingUserInputs: 0,
|
||||
pendingMcpElicitations: 0,
|
||||
pendingRequests: { actionable: 0 },
|
||||
hasComposerDraft: false,
|
||||
connected: false,
|
||||
lastFocused: false,
|
||||
|
|
@ -122,16 +120,7 @@ describe("CodexPanelPlugin workspace panel reconciliation", () => {
|
|||
const openLeaf = leaf();
|
||||
openLeaf.view = chatView(CodexChatView, openLeaf);
|
||||
const openView = openLeaf.view as CodexChatView;
|
||||
vi.spyOn(openView.surface, "openPanelSnapshot").mockReturnValue({
|
||||
viewId: "open-view",
|
||||
threadId: "thread-1",
|
||||
turnLifecycle: { kind: "idle" },
|
||||
pendingApprovals: 0,
|
||||
pendingUserInputs: 0,
|
||||
pendingMcpElicitations: 0,
|
||||
hasComposerDraft: false,
|
||||
connected: true,
|
||||
});
|
||||
vi.spyOn(openView.surface, "openPanelSnapshot").mockReturnValue(panelSnapshot({ viewId: "open-view", threadId: "thread-1" }));
|
||||
vi.spyOn(openView.surface, "focusThread").mockResolvedValue(undefined);
|
||||
const emptyLeaf = leaf();
|
||||
emptyLeaf.view = chatView(CodexChatView, emptyLeaf);
|
||||
|
|
@ -150,29 +139,11 @@ describe("CodexPanelPlugin workspace panel reconciliation", () => {
|
|||
const busyLeaf = leaf();
|
||||
busyLeaf.view = chatView(CodexChatView, busyLeaf);
|
||||
const busyView = busyLeaf.view as CodexChatView;
|
||||
vi.spyOn(busyView.surface, "openPanelSnapshot").mockReturnValue({
|
||||
viewId: "busy-view",
|
||||
threadId: "other-thread",
|
||||
turnLifecycle: { kind: "idle" },
|
||||
pendingApprovals: 0,
|
||||
pendingUserInputs: 0,
|
||||
pendingMcpElicitations: 0,
|
||||
hasComposerDraft: false,
|
||||
connected: true,
|
||||
});
|
||||
vi.spyOn(busyView.surface, "openPanelSnapshot").mockReturnValue(panelSnapshot({ viewId: "busy-view", threadId: "other-thread" }));
|
||||
const emptyLeaf = leaf();
|
||||
emptyLeaf.view = chatView(CodexChatView, emptyLeaf);
|
||||
const emptyView = emptyLeaf.view as CodexChatView;
|
||||
vi.spyOn(emptyView.surface, "openPanelSnapshot").mockReturnValue({
|
||||
viewId: "empty-view",
|
||||
threadId: null,
|
||||
turnLifecycle: { kind: "idle" },
|
||||
pendingApprovals: 0,
|
||||
pendingUserInputs: 0,
|
||||
pendingMcpElicitations: 0,
|
||||
hasComposerDraft: false,
|
||||
connected: true,
|
||||
});
|
||||
vi.spyOn(emptyView.surface, "openPanelSnapshot").mockReturnValue(panelSnapshot({ viewId: "empty-view", threadId: null }));
|
||||
const openEmptyThread = vi.spyOn(emptyView.surface, "openThread").mockResolvedValue(undefined);
|
||||
const plugin = await pluginWithLeaves([busyLeaf, emptyLeaf]);
|
||||
|
||||
|
|
@ -360,9 +331,9 @@ describe("CodexPanelPlugin workspace panel reconciliation", () => {
|
|||
activeLeafHandler(secondLeaf);
|
||||
|
||||
expect(refreshThreadsViewLiveState).toHaveBeenCalledOnce();
|
||||
expect(pluginWithThreads.runtime.threadsHost().getOpenPanelSnapshots()).toMatchObject([
|
||||
{ viewId: "first", lastFocused: false },
|
||||
{ viewId: "second", lastFocused: true },
|
||||
expect(pluginWithThreads.runtime.threadsHost().openPanelActivities()).toMatchObject([
|
||||
{ threadId: "thread-1", selected: false },
|
||||
{ threadId: "thread-2", selected: true },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -898,18 +869,27 @@ function thread(id: string): Thread {
|
|||
};
|
||||
}
|
||||
|
||||
function panelSnapshot(
|
||||
overrides: Partial<ReturnType<CodexChatView["surface"]["openPanelSnapshot"]>> = {},
|
||||
): ReturnType<CodexChatView["surface"]["openPanelSnapshot"]> {
|
||||
function panelSnapshot(overrides: PanelSnapshotFixtureOverrides = {}): ReturnType<CodexChatView["surface"]["openPanelSnapshot"]> {
|
||||
const { pendingApprovals, pendingUserInputs, pendingMcpElicitations, ...snapshotOverrides } = overrides;
|
||||
const pendingRequests = snapshotOverrides.pendingRequests ?? {
|
||||
approvals: pendingApprovals ?? 0,
|
||||
userInputs: pendingUserInputs ?? 0,
|
||||
mcpElicitations: pendingMcpElicitations ?? 0,
|
||||
actionable: (pendingApprovals ?? 0) + (pendingUserInputs ?? 0) + (pendingMcpElicitations ?? 0),
|
||||
};
|
||||
return {
|
||||
viewId: "view",
|
||||
threadId: "thread",
|
||||
turnLifecycle: { kind: "idle" },
|
||||
pendingApprovals: 0,
|
||||
pendingUserInputs: 0,
|
||||
pendingMcpElicitations: 0,
|
||||
pendingRequests,
|
||||
hasComposerDraft: false,
|
||||
connected: true,
|
||||
...overrides,
|
||||
...snapshotOverrides,
|
||||
};
|
||||
}
|
||||
|
||||
type PanelSnapshotFixtureOverrides = Partial<ReturnType<CodexChatView["surface"]["openPanelSnapshot"]>> & {
|
||||
pendingApprovals?: number;
|
||||
pendingUserInputs?: number;
|
||||
pendingMcpElicitations?: number;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ const CHAT_APPLICATION_OUTER_LAYER_MESSAGE =
|
|||
const CHAT_APP_SERVER_OUTER_LAYER_MESSAGE = "Chat app-server adapters must not import chat host, panel, presentation, or UI layers.";
|
||||
const CHAT_WORKSPACE_BOUNDARY_MESSAGE =
|
||||
"Chat modules must not import workspace modules; pass workspace capabilities through chat host contracts.";
|
||||
const FEATURE_WORKSPACE_BOUNDARY_MESSAGE =
|
||||
"Feature modules must not import workspace modules; pass workspace capabilities through feature host contracts.";
|
||||
const WORKSPACE_CHAT_INTERNAL_MESSAGE = "Workspace modules may coordinate chat only through chat host contracts and Obsidian views.";
|
||||
const CHAT_PANEL_RUNTIME_BOUNDARY_MESSAGE = "Chat panel modules must not import app-server adapters or chat host internals.";
|
||||
const CHAT_PRESENTATION_OUTER_LAYER_MESSAGE =
|
||||
"Chat presentation modules must stay pure view-model projection; keep application, app-server, host, panel, and UI dependencies outward.";
|
||||
|
|
@ -402,6 +405,8 @@ export function timestamp(): number {
|
|||
"no-chat-application-outer-layer-imports.grit",
|
||||
"no-chat-app-server-outer-layer-imports.grit",
|
||||
"no-chat-workspace-boundary-imports.grit",
|
||||
"no-feature-workspace-boundary-imports.grit",
|
||||
"no-workspace-chat-internal-imports.grit",
|
||||
"no-chat-panel-runtime-boundary-imports.grit",
|
||||
"no-chat-presentation-outer-layer-imports.grit",
|
||||
"no-chat-ui-outer-layer-imports.grit",
|
||||
|
|
@ -411,11 +416,11 @@ export function timestamp(): number {
|
|||
`
|
||||
import type { Host } from "../host/contracts";
|
||||
import type { ChatThreadHistoryPage } from "../app-server/threads/projection";
|
||||
import type { PanelSnapshot } from "../panel/snapshot";
|
||||
import type { ToolbarPanelActions } from "../panel/toolbar-actions";
|
||||
import { statusText } from "../presentation/runtime/status";
|
||||
import { Toolbar } from "../ui/toolbar";
|
||||
|
||||
export type Escape = ChatThreadHistoryPage | Host | PanelSnapshot;
|
||||
export type Escape = ChatThreadHistoryPage | Host | ToolbarPanelActions;
|
||||
export const values = [statusText, Toolbar] satisfies unknown[];
|
||||
`.trimStart(),
|
||||
);
|
||||
|
|
@ -439,11 +444,11 @@ export type Escape = AppServerClient;
|
|||
path.join(cwd, "src/features/chat/app-server/outer.ts"),
|
||||
`
|
||||
import type { Host } from "../host/contracts";
|
||||
import type { PanelSnapshot } from "../panel/snapshot";
|
||||
import type { ToolbarPanelActions } from "../panel/toolbar-actions";
|
||||
import { statusText } from "../presentation/runtime/status";
|
||||
import { Toolbar } from "../ui/toolbar";
|
||||
|
||||
export type Escape = Host | PanelSnapshot;
|
||||
export type Escape = Host | ToolbarPanelActions;
|
||||
export const values = [statusText, Toolbar] satisfies unknown[];
|
||||
`.trimStart(),
|
||||
);
|
||||
|
|
@ -471,6 +476,40 @@ export type Escape = WorkspacePanelCoordinator;
|
|||
import type { ChatStateStore } from "../application/state/store";
|
||||
|
||||
export type Allowed = ChatStateStore;
|
||||
`.trimStart(),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(cwd, "src/features/threads-view/workspace-escape.ts"),
|
||||
`
|
||||
import type { WorkspacePanelCoordinator } from "../../workspace/panel-coordinator";
|
||||
|
||||
export type Escape = WorkspacePanelCoordinator;
|
||||
`.trimStart(),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(cwd, "src/features/threads-view/workspace-allowed.ts"),
|
||||
`
|
||||
import type { Thread } from "../../domain/threads/model";
|
||||
|
||||
export type Allowed = Thread;
|
||||
`.trimStart(),
|
||||
);
|
||||
await mkdir(path.join(cwd, "src/workspace"), { recursive: true });
|
||||
await writeFile(
|
||||
path.join(cwd, "src/workspace/chat-internal-escape.ts"),
|
||||
`
|
||||
import type { ToolbarPanelActions } from "../features/chat/panel/toolbar-actions";
|
||||
|
||||
export type Escape = ToolbarPanelActions;
|
||||
`.trimStart(),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(cwd, "src/workspace/chat-host-allowed.ts"),
|
||||
`
|
||||
import type { ChatWorkspacePanelSurface } from "../features/chat/host/contracts";
|
||||
import type { CodexChatView } from "../features/chat/host/view.obsidian";
|
||||
|
||||
export type Allowed = ChatWorkspacePanelSurface | CodexChatView;
|
||||
`.trimStart(),
|
||||
);
|
||||
await writeFile(
|
||||
|
|
@ -500,10 +539,10 @@ import type { AppServerClient } from "../../../app-server/connection/client";
|
|||
import type { ChatStateStore } from "../application/state/store";
|
||||
import type { ChatServerActionsHost } from "../app-server/actions/host";
|
||||
import type { Host } from "../host/contracts";
|
||||
import type { PanelSnapshot } from "../panel/snapshot";
|
||||
import type { ToolbarPanelActions } from "../panel/toolbar-actions";
|
||||
import { Toolbar } from "../ui/toolbar";
|
||||
|
||||
export type Escape = AppServerClient | ChatStateStore | ChatServerActionsHost | Host | PanelSnapshot;
|
||||
export type Escape = AppServerClient | ChatStateStore | ChatServerActionsHost | Host | ToolbarPanelActions;
|
||||
export const toolbar = Toolbar;
|
||||
`.trimStart(),
|
||||
);
|
||||
|
|
@ -523,9 +562,9 @@ import type { AppServerClient } from "../../../app-server/connection/client";
|
|||
import type { ChatStateStore } from "../application/state/store";
|
||||
import type { ChatServerActionsHost } from "../app-server/actions/host";
|
||||
import type { Host } from "../host/contracts";
|
||||
import type { PanelSnapshot } from "../panel/snapshot";
|
||||
import type { ToolbarPanelActions } from "../panel/toolbar-actions";
|
||||
|
||||
export type Escape = AppServerClient | ChatStateStore | ChatServerActionsHost | Host | PanelSnapshot;
|
||||
export type Escape = AppServerClient | ChatStateStore | ChatServerActionsHost | Host | ToolbarPanelActions;
|
||||
`.trimStart(),
|
||||
);
|
||||
await writeFile(
|
||||
|
|
@ -549,6 +588,10 @@ export const value = statusText;
|
|||
"src/features/chat/app-server/allowed.ts",
|
||||
"src/features/chat/host/workspace-escape.ts",
|
||||
"src/features/chat/host/workspace-allowed.ts",
|
||||
"src/features/threads-view/workspace-escape.ts",
|
||||
"src/features/threads-view/workspace-allowed.ts",
|
||||
"src/workspace/chat-internal-escape.ts",
|
||||
"src/workspace/chat-host-allowed.ts",
|
||||
"src/features/chat/panel/outer.tsx",
|
||||
"src/features/chat/panel/allowed.tsx",
|
||||
"src/features/chat/presentation/outer.ts",
|
||||
|
|
@ -570,6 +613,10 @@ export const value = statusText;
|
|||
expect(pluginDiagnostics(report, "src/features/chat/app-server/allowed.ts")).toEqual([]);
|
||||
expect(pluginMessages(report, "src/features/chat/host/workspace-escape.ts")).toEqual([CHAT_WORKSPACE_BOUNDARY_MESSAGE]);
|
||||
expect(pluginDiagnostics(report, "src/features/chat/host/workspace-allowed.ts")).toEqual([]);
|
||||
expect(pluginMessages(report, "src/features/threads-view/workspace-escape.ts")).toEqual([FEATURE_WORKSPACE_BOUNDARY_MESSAGE]);
|
||||
expect(pluginDiagnostics(report, "src/features/threads-view/workspace-allowed.ts")).toEqual([]);
|
||||
expect(pluginMessages(report, "src/workspace/chat-internal-escape.ts")).toEqual([WORKSPACE_CHAT_INTERNAL_MESSAGE]);
|
||||
expect(pluginDiagnostics(report, "src/workspace/chat-host-allowed.ts")).toEqual([]);
|
||||
expect(pluginMessages(report, "src/features/chat/panel/outer.tsx")).toEqual(
|
||||
Array.from({ length: 3 }, () => CHAT_PANEL_RUNTIME_BOUNDARY_MESSAGE),
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue