mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
refactor(chat): derive Threads activity from panel state
This commit is contained in:
parent
cf0548a0d0
commit
f93570848f
26 changed files with 201 additions and 226 deletions
|
|
@ -11,24 +11,10 @@ interface PendingRequestQueueSource {
|
|||
readonly pendingMcpElicitations: readonly unknown[];
|
||||
}
|
||||
|
||||
interface PendingRequestCountSource {
|
||||
readonly pendingApprovals: number;
|
||||
readonly pendingUserInputs: number;
|
||||
readonly pendingMcpElicitations: number;
|
||||
}
|
||||
|
||||
export function pendingRequestCountsFromQueues(source: PendingRequestQueueSource): PendingRequestCounts {
|
||||
return pendingRequestCounts({
|
||||
pendingApprovals: source.approvals.length,
|
||||
pendingUserInputs: source.pendingUserInputs.length,
|
||||
pendingMcpElicitations: source.pendingMcpElicitations.length,
|
||||
});
|
||||
}
|
||||
|
||||
export function pendingRequestCounts(source: PendingRequestCountSource): PendingRequestCounts {
|
||||
const approvals = source.pendingApprovals;
|
||||
const userInputs = source.pendingUserInputs;
|
||||
const mcpElicitations = source.pendingMcpElicitations;
|
||||
const approvals = source.approvals.length;
|
||||
const userInputs = source.pendingUserInputs.length;
|
||||
const mcpElicitations = source.pendingMcpElicitations.length;
|
||||
return {
|
||||
approvals,
|
||||
userInputs,
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ export interface ChatConnectionActionsHost {
|
|||
setStatus: (statusText: string, phase?: ChatConnectionPhase) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
configuredCommand: () => string;
|
||||
refreshLiveState: () => void;
|
||||
isStaleConnectionError: (error: unknown) => boolean;
|
||||
isStaleResourceContextError: (error: unknown) => boolean;
|
||||
notifyConnectionFailed: () => void;
|
||||
|
|
@ -43,7 +42,7 @@ export interface ChatConnectionActionsHost {
|
|||
|
||||
type ChatConnectionExitHost = Pick<
|
||||
ChatConnectionActionsHost,
|
||||
"invalidateThreadWork" | "setStatus" | "stateStore" | "resetThreadTurnPresence" | "refreshLiveState"
|
||||
"invalidateThreadWork" | "setStatus" | "stateStore" | "resetThreadTurnPresence"
|
||||
>;
|
||||
|
||||
function handleChatConnectionExit(host: ChatConnectionExitHost): void {
|
||||
|
|
@ -51,7 +50,6 @@ function handleChatConnectionExit(host: ChatConnectionExitHost): void {
|
|||
host.setStatus(STATUS_CONNECTION_STOPPED, { kind: "disconnected", message: STATUS_CONNECTION_STOPPED });
|
||||
host.stateStore.dispatch({ type: "connection/scoped-cleared" });
|
||||
host.resetThreadTurnPresence(false);
|
||||
host.refreshLiveState();
|
||||
}
|
||||
|
||||
export interface ChatConnectionActions {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ export interface PendingRequestActionsHost {
|
|||
responder: PendingRequestResponder;
|
||||
composerHasFocus: () => boolean;
|
||||
focusComposer: () => void;
|
||||
refreshLiveState: () => void;
|
||||
}
|
||||
|
||||
export interface PendingRequestActions {
|
||||
|
|
@ -121,6 +120,5 @@ function pendingUserInput(host: PendingRequestActionsHost, requestId: PendingReq
|
|||
}
|
||||
|
||||
function commitRequestAction(host: PendingRequestActionsHost): void {
|
||||
host.refreshLiveState();
|
||||
host.focusComposer();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ export interface ThreadGoalSyncHost {
|
|||
localItemIds: LocalIdSource;
|
||||
addSystemMessage: (text: string) => void;
|
||||
addGoalEvent: (item: GoalThreadStreamItem) => void;
|
||||
refreshLiveState: () => void;
|
||||
}
|
||||
|
||||
export interface GoalActionsHost extends ThreadGoalSyncHost {
|
||||
|
|
@ -253,7 +252,6 @@ function applyGoalIfActive(
|
|||
const item = options.reportChange ? goalChangeItem(host.localItemIds.next("goal"), activeThread.goal, goal) : null;
|
||||
host.stateStore.dispatch({ type: "active-thread/goal-set", goal });
|
||||
if (item) host.addGoalEvent(item);
|
||||
host.refreshLiveState();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ export interface ResumeActionsHost {
|
|||
notifyActiveThreadIdentityChanged: () => void;
|
||||
recordResumedThread: (thread: Thread) => void;
|
||||
addSystemMessage: (text: string) => void;
|
||||
refreshLiveState: () => void;
|
||||
syncThreadGoal: (threadId: string) => Promise<void>;
|
||||
recoverTokenUsageFromRollout?: (path: string) => Promise<ThreadTokenUsage | null>;
|
||||
}
|
||||
|
|
@ -80,7 +79,6 @@ async function resumeThread(
|
|||
if (renderFallbackMessage) {
|
||||
host.addSystemMessage(`Resumed thread ${effect.value.activation.thread.id}`);
|
||||
}
|
||||
host.refreshLiveState();
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (isStaleResume(host, resume, currentPanelTarget)) return false;
|
||||
|
|
@ -123,7 +121,6 @@ function recoverResumedThreadTokenUsage(
|
|||
const activeThread = activeThreadState(state);
|
||||
if (!activeThread || activeThread.id !== threadId || activeThread.tokenUsage !== null) return;
|
||||
host.stateStore.dispatch({ type: "active-thread/token-usage-set", tokenUsage });
|
||||
host.refreshLiveState();
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,9 +58,6 @@ export function createChatComposerController(
|
|||
togglePlan: () => void input.runtimeSettings.toggleCollaborationMode(),
|
||||
toggleAutoReview: () => void input.runtimeSettings.toggleAutoReview(),
|
||||
toggleFast: () => void input.runtimeSettings.toggleFastMode(),
|
||||
onDraftChange: () => {
|
||||
environment.plugin.workspace.refreshThreadsViewLiveState();
|
||||
},
|
||||
onHeightChange: () => undefined,
|
||||
canFocus: environment.obsidian.isForeground,
|
||||
onAttachmentError: (message) => {
|
||||
|
|
|
|||
|
|
@ -38,9 +38,7 @@ interface ChatPanelConnectionBundleHost {
|
|||
canConnect: () => boolean;
|
||||
deferredTasks: ChatViewDeferredTasks;
|
||||
invalidateThreadWork: () => void;
|
||||
deferLiveStateRefresh: () => void;
|
||||
refreshTabHeader: () => void;
|
||||
refreshLiveState: () => void;
|
||||
}
|
||||
|
||||
export interface ChatPanelConnectionBundle {
|
||||
|
|
@ -173,9 +171,6 @@ export function createConnectionBundle(
|
|||
resetThreadTurnPresence: (hadTurns: boolean) => {
|
||||
autoTitleCoordinator.resetThreadTurnPresence(hadTurns);
|
||||
},
|
||||
refreshLiveState: () => {
|
||||
host.refreshLiveState();
|
||||
},
|
||||
};
|
||||
const connectionActions = createChatConnectionActions({
|
||||
...connectionExitHost,
|
||||
|
|
@ -189,12 +184,10 @@ export function createConnectionBundle(
|
|||
vaultPath: sourceContext.cwd,
|
||||
generation: sourceContext.generation,
|
||||
});
|
||||
host.deferLiveStateRefresh();
|
||||
},
|
||||
onServerRequest: (request, responder) => {
|
||||
serverRequestResponders.remember(request.id, responder);
|
||||
inboundHandler.handleServerRequest(request);
|
||||
host.deferLiveStateRefresh();
|
||||
},
|
||||
onLog: (message) => {
|
||||
inboundHandler.handleAppServerLog(message);
|
||||
|
|
@ -235,9 +228,6 @@ export function createConnectionBundle(
|
|||
setStatus: status.set,
|
||||
addSystemMessage: status.addSystemMessage,
|
||||
configuredCommand: () => environment.plugin.settingsRef.settings.codexPath(),
|
||||
refreshLiveState: () => {
|
||||
host.refreshLiveState();
|
||||
},
|
||||
isStaleConnectionError: (error) => error instanceof StaleConnectionError,
|
||||
isStaleResourceContextError: isStaleAppServerResourceContextError,
|
||||
notifyConnectionFailed: () => {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ interface ChatPanelThreadFoundationInput {
|
|||
appServer: ChatCurrentAppServerGateway;
|
||||
localItemIds: LocalIdSource;
|
||||
status: ChatPanelThreadStatus;
|
||||
refreshLiveState: () => void;
|
||||
}
|
||||
|
||||
interface ChatPanelThreadFoundation {
|
||||
|
|
@ -85,7 +84,6 @@ interface ChatPanelThreadLifecycleInput {
|
|||
status: ChatPanelThreadStatus;
|
||||
threadStart: ThreadStartActions;
|
||||
foundation: ChatPanelThreadFoundation;
|
||||
refreshLiveState: () => void;
|
||||
notifyActiveThreadIdentityChanged: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +113,7 @@ interface ChatPanelThreadActionBundle {
|
|||
}
|
||||
|
||||
export function createThreadFoundation(host: ChatPanelThreadHost, input: ChatPanelThreadFoundationInput): ChatPanelThreadFoundation {
|
||||
const { appServer, localItemIds, status, refreshLiveState } = input;
|
||||
const { appServer, localItemIds, status } = input;
|
||||
const { environment, stateStore } = host;
|
||||
const threadOperationsTransport = createThreadOperationsTransport(appServer.clientAccess);
|
||||
const threadTitleTransport = createThreadTitleTransport({
|
||||
|
|
@ -200,7 +198,6 @@ export function createThreadFoundation(host: ChatPanelThreadHost, input: ChatPan
|
|||
addGoalEvent: (item) => {
|
||||
stateStore.dispatch({ type: "thread-stream/item-upserted", item });
|
||||
},
|
||||
refreshLiveState,
|
||||
},
|
||||
goalOperations,
|
||||
);
|
||||
|
|
@ -220,8 +217,7 @@ export function createThreadLifecycleBundle(
|
|||
host: ChatPanelThreadHost,
|
||||
input: ChatPanelThreadLifecycleInput,
|
||||
): ChatPanelThreadLifecycleBundle {
|
||||
const { appServer, localItemIds, ensureConnected, status, threadStart, foundation, refreshLiveState, notifyActiveThreadIdentityChanged } =
|
||||
input;
|
||||
const { appServer, localItemIds, ensureConnected, status, threadStart, foundation, notifyActiveThreadIdentityChanged } = input;
|
||||
let sessionLifecycle: ChatPanelThreadLifecycle | null = null;
|
||||
const goals = createGoalActions(
|
||||
{
|
||||
|
|
@ -241,7 +237,6 @@ export function createThreadLifecycleBundle(
|
|||
addGoalEvent: (item) => {
|
||||
host.stateStore.dispatch({ type: "thread-stream/item-upserted", item });
|
||||
},
|
||||
refreshLiveState,
|
||||
},
|
||||
foundation.goalOperations,
|
||||
);
|
||||
|
|
@ -261,7 +256,6 @@ export function createThreadLifecycleBundle(
|
|||
invalidateThreadWork: () => {
|
||||
foundation.invalidateThreadWork();
|
||||
},
|
||||
refreshLiveState,
|
||||
notifyActiveThreadIdentityChanged,
|
||||
});
|
||||
sessionLifecycle = lifecycle;
|
||||
|
|
@ -336,20 +330,10 @@ function createSessionThreadLifecycle(
|
|||
autoTitleCoordinator: AutoTitleCoordinator;
|
||||
history: HistoryController;
|
||||
invalidateThreadWork: () => void;
|
||||
refreshLiveState: () => void;
|
||||
notifyActiveThreadIdentityChanged: () => void;
|
||||
},
|
||||
): ChatPanelThreadLifecycle {
|
||||
const {
|
||||
appServer,
|
||||
status,
|
||||
goals,
|
||||
autoTitleCoordinator,
|
||||
history,
|
||||
invalidateThreadWork,
|
||||
refreshLiveState,
|
||||
notifyActiveThreadIdentityChanged,
|
||||
} = input;
|
||||
const { appServer, status, goals, autoTitleCoordinator, history, invalidateThreadWork, notifyActiveThreadIdentityChanged } = input;
|
||||
const restoration = new RestorationController({
|
||||
stateStore: host.stateStore,
|
||||
});
|
||||
|
|
@ -368,7 +352,6 @@ function createSessionThreadLifecycle(
|
|||
host.environment.plugin.threadCatalog.apply({ type: "thread-upserted", thread });
|
||||
},
|
||||
addSystemMessage: status.addSystemMessage,
|
||||
refreshLiveState,
|
||||
syncThreadGoal: (threadId) => goals.syncThreadGoal(threadId),
|
||||
recoverTokenUsageFromRollout: (path) =>
|
||||
recoverRolloutTokenUsage(path, (filePath, options) => appServer.readFileBase64(filePath, options)),
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ interface ChatPanelTurnInput {
|
|||
reconnect: () => Promise<void>;
|
||||
runtimeProjection: ChatPanelRuntimeProjection;
|
||||
refreshDiagnostics: () => Promise<void>;
|
||||
refreshLiveState: () => void;
|
||||
notifyActiveThreadIdentityChanged: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -75,7 +74,6 @@ export function createTurnBundle(host: ChatPanelTurnHost, input: ChatPanelTurnIn
|
|||
reconnect,
|
||||
runtimeProjection,
|
||||
refreshDiagnostics,
|
||||
refreshLiveState,
|
||||
notifyActiveThreadIdentityChanged,
|
||||
} = input;
|
||||
const pendingRequests = createPendingRequestActions({
|
||||
|
|
@ -85,7 +83,6 @@ export function createTurnBundle(host: ChatPanelTurnHost, input: ChatPanelTurnIn
|
|||
focusComposer: () => {
|
||||
composerController.focusComposer();
|
||||
},
|
||||
refreshLiveState,
|
||||
});
|
||||
const threadReferenceResolver = appServer.threadReferences({
|
||||
prepareInput: (text, snapshot) => composerController.preparedInput(text, snapshot),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import type { AppServerContextLease, AppServerQueryContextIdentity } from "../..
|
|||
import type { ObservedResultListener } from "../../../app-server/query/observed-result";
|
||||
import type { ModelMetadata, ReasoningEffort } from "../../../domain/catalog/metadata";
|
||||
import type { SendShortcut } from "../../../domain/input/send-shortcut";
|
||||
import type { PendingRequestCounts } from "../../../domain/pending-requests/aggregate";
|
||||
import type { SharedServerMetadata, SharedServerMetadataResource } from "../../../domain/server/metadata";
|
||||
import type { ArchiveExportSettings } from "../../../domain/threads/archive-markdown";
|
||||
import type { KeyedOperationQueue } from "../../../shared/runtime/keyed-operation-queue";
|
||||
|
|
@ -51,7 +50,7 @@ interface WorkspacePanels {
|
|||
openThreadInNewView(threadId: string): Promise<void>;
|
||||
focusThreadInOpenView(threadId: string): Promise<boolean>;
|
||||
openTurnDiff(state: TurnDiffViewState): Promise<void>;
|
||||
refreshThreadsViewLiveState(): void;
|
||||
notifyPanelActivityChanged(): void;
|
||||
openSideChat(sourceThreadId: string, sourceThreadTitle: string | null): Promise<void>;
|
||||
}
|
||||
|
||||
|
|
@ -102,13 +101,11 @@ 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;
|
||||
turnBusy: boolean;
|
||||
pending: boolean;
|
||||
hasComposerDraft: boolean;
|
||||
connected: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,8 +57,6 @@ interface ChatPanelSessionRuntimeParts {
|
|||
};
|
||||
runtime: {
|
||||
sharedState: ChatPanelSharedStateBinding;
|
||||
refreshLiveState(): void;
|
||||
deferLiveStateRefresh(): void;
|
||||
};
|
||||
disposeOwnedResources: () => void;
|
||||
}
|
||||
|
|
@ -77,7 +75,6 @@ interface ChatPanelSessionRuntimeHost {
|
|||
threadStreamScrollBinding: ChatThreadStreamScrollBinding;
|
||||
getClosing: () => boolean;
|
||||
reconnect?: () => Promise<void>;
|
||||
viewWindow: () => Window;
|
||||
}
|
||||
|
||||
export class ChatPanelSessionRuntime {
|
||||
|
|
@ -109,8 +106,6 @@ export class ChatPanelSessionRuntime {
|
|||
this.disposeOwnedResources();
|
||||
unmount();
|
||||
this.connection.manager.disconnect();
|
||||
this.runtime.refreshLiveState();
|
||||
this.runtime.deferLiveStateRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -129,23 +124,15 @@ function composeChatPanelSessionRuntime(host: ChatPanelSessionRuntimeHost): Chat
|
|||
const refreshTabHeader = () => {
|
||||
host.environment.view.refreshTabHeader();
|
||||
};
|
||||
const refreshLiveState = () => {
|
||||
host.environment.plugin.workspace.refreshThreadsViewLiveState();
|
||||
};
|
||||
const deferLiveStateRefresh = () => {
|
||||
host.viewWindow().setTimeout(refreshLiveState, 0);
|
||||
};
|
||||
const notifyActiveThreadIdentityChanged = () => {
|
||||
refreshTabHeader();
|
||||
host.environment.obsidian.requestWorkspaceLayoutSave();
|
||||
refreshLiveState();
|
||||
};
|
||||
|
||||
const threadFoundation = createThreadFoundation(host, {
|
||||
appServer: currentAppServer,
|
||||
localItemIds,
|
||||
status,
|
||||
refreshLiveState,
|
||||
});
|
||||
const invalidateThreadWork = (): void => {
|
||||
threadFoundation.invalidateThreadWork();
|
||||
|
|
@ -157,9 +144,7 @@ function composeChatPanelSessionRuntime(host: ChatPanelSessionRuntimeHost): Chat
|
|||
canConnect: () => !host.getClosing(),
|
||||
deferredTasks: host.deferredTasks,
|
||||
invalidateThreadWork,
|
||||
deferLiveStateRefresh,
|
||||
refreshTabHeader,
|
||||
refreshLiveState,
|
||||
},
|
||||
{
|
||||
connection,
|
||||
|
|
@ -207,7 +192,6 @@ function composeChatPanelSessionRuntime(host: ChatPanelSessionRuntimeHost): Chat
|
|||
status,
|
||||
threadStart,
|
||||
foundation: threadFoundation,
|
||||
refreshLiveState,
|
||||
notifyActiveThreadIdentityChanged,
|
||||
});
|
||||
const composerController = createChatComposerController(host, {
|
||||
|
|
@ -296,7 +280,6 @@ function composeChatPanelSessionRuntime(host: ChatPanelSessionRuntimeHost): Chat
|
|||
reconnect: reconnectForUser,
|
||||
runtimeProjection: runtime.projection,
|
||||
refreshDiagnostics: () => connectionActions.refreshDiagnostics(),
|
||||
refreshLiveState,
|
||||
notifyActiveThreadIdentityChanged,
|
||||
});
|
||||
const shell = createShellBundle(host, {
|
||||
|
|
@ -358,8 +341,6 @@ function composeChatPanelSessionRuntime(host: ChatPanelSessionRuntimeHost): Chat
|
|||
},
|
||||
runtime: {
|
||||
sharedState,
|
||||
refreshLiveState,
|
||||
deferLiveStateRefresh,
|
||||
},
|
||||
disposeOwnedResources: () => {
|
||||
connectionBundle.invalidateConnectionScope();
|
||||
|
|
|
|||
|
|
@ -4,14 +4,15 @@ import {
|
|||
appServerQueryContextIdentity,
|
||||
appServerQueryContextIdentityMatches,
|
||||
} from "../../../app-server/query/keys";
|
||||
import { pendingRequestCountsFromQueues } from "../../../domain/pending-requests/aggregate";
|
||||
import { hasPendingRequests, pendingRequestCountsFromQueues } from "../../../domain/pending-requests/aggregate";
|
||||
import { threadMeaningfulTitle, threadWindowTitle } from "../../../domain/threads/title";
|
||||
import { activeThreadState, awaitingResumeThreadState, type ChatState, panelThreadId } from "../application/state/root-reducer";
|
||||
import { type ChatStateStore, createChatStateStore } from "../application/state/store";
|
||||
import { ChatResumeWorkTracker } from "../application/threads/resume-work";
|
||||
import { chatTurnBusy } from "../application/turns/turn-state";
|
||||
import { renderChatPanelShell, unmountChatPanelShell } from "../panel/shell.dom";
|
||||
import { type ChatThreadStreamScrollBinding, createChatThreadStreamScrollBinding } from "../panel/thread-stream-scroll-binding";
|
||||
import type { ChatPanelEnvironment, ChatPanelHandle, ChatWorkspacePanelSnapshot, ChatWorkspacePanelTurnLifecycle } from "./contracts";
|
||||
import type { ChatPanelEnvironment, ChatPanelHandle, ChatWorkspacePanelSnapshot } from "./contracts";
|
||||
import { type ChatViewDeferredTasks, createChatViewDeferredTasks } from "./session/deferred-work";
|
||||
import { ChatPanelSessionRuntime } from "./session-runtime";
|
||||
import { parseChatPanelViewState } from "./view-state";
|
||||
|
|
@ -29,6 +30,8 @@ export class ChatPanelSession implements ChatPanelHandle {
|
|||
private pendingAppServerContextReplacement: { panelThreadId: string | null; generation: number } | null = null;
|
||||
private opened = false;
|
||||
private closing = false;
|
||||
private observedPanelActivity: PanelActivity | null = null;
|
||||
private unsubscribePanelActivity: (() => void) | null = null;
|
||||
|
||||
constructor(private readonly environment: ChatPanelEnvironment) {
|
||||
this.observedAppServerContext = this.currentAppServerContext();
|
||||
|
|
@ -166,12 +169,11 @@ export class ChatPanelSession implements ChatPanelHandle {
|
|||
}
|
||||
|
||||
openPanelSnapshot(): ChatWorkspacePanelSnapshot {
|
||||
const pendingRequests = pendingRequestCountsFromQueues(this.state.requests);
|
||||
const activity = panelActivity(this.state);
|
||||
return {
|
||||
viewId: this.environment.obsidian.viewId,
|
||||
threadId: this.closing ? null : this.panelThreadId(),
|
||||
turnLifecycle: openPanelTurnLifecycle(this.state.turn.lifecycle),
|
||||
pendingRequests,
|
||||
...activity,
|
||||
threadId: this.closing ? null : activity.threadId,
|
||||
hasComposerDraft: this.state.composer.draft.trim().length > 0,
|
||||
connected: this.runtime.connection.manager.isConnected(),
|
||||
};
|
||||
|
|
@ -221,6 +223,7 @@ export class ChatPanelSession implements ChatPanelHandle {
|
|||
open(): void {
|
||||
this.opened = true;
|
||||
this.closing = false;
|
||||
this.observePanelActivity();
|
||||
this.environment.obsidian.registerPointerDown((event) => {
|
||||
this.closeToolbarPanelOnOutsidePointer(event);
|
||||
});
|
||||
|
|
@ -232,10 +235,18 @@ export class ChatPanelSession implements ChatPanelHandle {
|
|||
async close(): Promise<void> {
|
||||
this.opened = false;
|
||||
this.closing = true;
|
||||
this.unsubscribePanelActivity?.();
|
||||
this.unsubscribePanelActivity = null;
|
||||
this.observedPanelActivity = null;
|
||||
const viewWindow = this.viewWindow();
|
||||
const panelRoot = this.environment.view.panelRoot();
|
||||
await this.runtime.dispose(() => {
|
||||
unmountChatPanelShell(panelRoot);
|
||||
});
|
||||
this.notifyPanelActivityChanged();
|
||||
viewWindow.setTimeout(() => {
|
||||
this.notifyPanelActivityChanged();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
setComposerText(text: string): void {
|
||||
|
|
@ -296,6 +307,22 @@ export class ChatPanelSession implements ChatPanelHandle {
|
|||
this.runtime.shell.closeToolbarPanelOnOutsidePointer(event);
|
||||
}
|
||||
|
||||
private observePanelActivity(): void {
|
||||
this.unsubscribePanelActivity?.();
|
||||
this.observedPanelActivity = panelActivity(this.state);
|
||||
this.unsubscribePanelActivity = this.stateStore.subscribe(() => {
|
||||
const next = panelActivity(this.state);
|
||||
if (panelActivityEquals(this.observedPanelActivity, next)) return;
|
||||
this.observedPanelActivity = next;
|
||||
this.notifyPanelActivityChanged();
|
||||
});
|
||||
this.notifyPanelActivityChanged();
|
||||
}
|
||||
|
||||
private notifyPanelActivityChanged(): void {
|
||||
this.environment.plugin.workspace.notifyPanelActivityChanged();
|
||||
}
|
||||
|
||||
private activeThreadTitle(): string | null {
|
||||
const activeThread = activeThreadState(this.state);
|
||||
if (!activeThread) return null;
|
||||
|
|
@ -334,13 +361,24 @@ export class ChatPanelSession implements ChatPanelHandle {
|
|||
threadStreamScrollBinding: this.threadStreamScrollBinding,
|
||||
getClosing: () => this.closing,
|
||||
reconnect: () => this.reconnect(),
|
||||
viewWindow: () => this.viewWindow(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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" };
|
||||
interface PanelActivity {
|
||||
readonly threadId: string | null;
|
||||
readonly turnBusy: boolean;
|
||||
readonly pending: boolean;
|
||||
}
|
||||
|
||||
function panelActivity(state: ChatState): PanelActivity {
|
||||
return {
|
||||
threadId: panelThreadId(state),
|
||||
turnBusy: chatTurnBusy(state),
|
||||
pending: hasPendingRequests(pendingRequestCountsFromQueues(state.requests)),
|
||||
};
|
||||
}
|
||||
|
||||
function panelActivityEquals(left: PanelActivity | null, right: PanelActivity): boolean {
|
||||
return left !== null && left.threadId === right.threadId && left.turnBusy === right.turnBusy && left.pending === right.pending;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@ export interface ChatComposerControllerOptions {
|
|||
togglePlan: () => void;
|
||||
toggleAutoReview: () => void;
|
||||
toggleFast: () => void;
|
||||
onDraftChange: () => void;
|
||||
onHeightChange: () => void;
|
||||
canFocus: () => boolean;
|
||||
onAttachmentError?: (message: string) => void;
|
||||
|
|
@ -145,7 +144,6 @@ export class ChatComposerController {
|
|||
draft: text,
|
||||
...(options.clearSuggestions === undefined ? {} : { clearSuggestions: options.clearSuggestions }),
|
||||
});
|
||||
this.options.onDraftChange();
|
||||
if (options.focus && this.options.canFocus()) focusComposer(this.composer);
|
||||
}
|
||||
|
||||
|
|
@ -270,7 +268,6 @@ export class ChatComposerController {
|
|||
selected: suggestionState.selected,
|
||||
dismissedSignature: suggestionState.dismissedSignature,
|
||||
});
|
||||
this.options.onDraftChange();
|
||||
}
|
||||
|
||||
private inputSuggestionState(): {
|
||||
|
|
@ -335,7 +332,6 @@ export class ChatComposerController {
|
|||
|
||||
this.pendingSelection = collapsedComposerSelection(insertion.value, insertion.cursor);
|
||||
this.dispatch({ type: "composer/draft-set", draft: insertion.value, clearSuggestions: true });
|
||||
this.options.onDraftChange();
|
||||
applyComposerInsertionToElement(this.composer, insertion.cursor);
|
||||
}
|
||||
|
||||
|
|
@ -398,7 +394,6 @@ export class ChatComposerController {
|
|||
};
|
||||
this.pendingSelection = collapsedComposerSelection(insertion.value, insertion.cursor);
|
||||
this.dispatch({ type: "composer/attachment-save-started", saveId: id, draft: insertion.value });
|
||||
this.options.onDraftChange();
|
||||
applyComposerInsertionToElement(this.composer, insertion.cursor);
|
||||
return pending;
|
||||
}
|
||||
|
|
@ -425,7 +420,6 @@ export class ChatComposerController {
|
|||
this.pruneActiveNoteContextSnapshots(replacement.value);
|
||||
this.pruneSelectionContextSnapshots(replacement.value);
|
||||
this.dispatch({ type: "composer/attachment-save-settled", saveId: pending.id, draft: replacement.value });
|
||||
this.options.onDraftChange();
|
||||
}
|
||||
|
||||
private settlePendingAttachmentSave(pending: PendingAttachmentSave): void {
|
||||
|
|
@ -434,7 +428,6 @@ export class ChatComposerController {
|
|||
const replacement = replacePendingAttachmentWithOriginalText(state.composer.draft, pending);
|
||||
this.pendingSelection = adjustedComposerSelection(composerSelectionSource(this.composer), state.composer.draft, replacement);
|
||||
this.dispatch({ type: "composer/attachment-save-settled", saveId: pending.id, draft: replacement.value });
|
||||
this.options.onDraftChange();
|
||||
}
|
||||
|
||||
private dismissSuggestions(): void {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
} from "./app-server/query/keys";
|
||||
import { AppServerResourceStore, StaleAppServerResourceContextError } from "./app-server/query/resource-store";
|
||||
import { VIEW_TYPE_CODEX_THREADS, VIEW_TYPE_CODEX_TURN_DIFF } from "./constants";
|
||||
import { hasPendingRequests } from "./domain/pending-requests/aggregate";
|
||||
import { createThreadGoalOperationCoordinator } from "./features/chat/application/threads/goal-actions";
|
||||
import type {
|
||||
ChatPanelClientSurface,
|
||||
|
|
@ -152,7 +151,7 @@ export class CodexPanelRuntime implements AppServerClientAccess {
|
|||
focusThreadInOpenView: (threadId) => this.panels.focusThreadInOpenView(threadId),
|
||||
openTurnDiff: (state) => this.openTurnDiff(state),
|
||||
openSideChat: (sourceThreadId, sourceThreadTitle) => this.panels.openSideChat(sourceThreadId, sourceThreadTitle),
|
||||
refreshThreadsViewLiveState: () => {
|
||||
notifyPanelActivityChanged: () => {
|
||||
this.refreshThreadsViewLiveState();
|
||||
},
|
||||
},
|
||||
|
|
@ -324,8 +323,8 @@ export class CodexPanelRuntime implements AppServerClientAccess {
|
|||
return this.panels.getOpenPanelSnapshots().map((snapshot) => ({
|
||||
threadId: snapshot.threadId,
|
||||
selected: snapshot.lastFocused,
|
||||
pending: hasPendingRequests(snapshot.pendingRequests),
|
||||
running: snapshot.turnLifecycle.kind !== "idle",
|
||||
pending: snapshot.pending,
|
||||
running: snapshot.turnBusy,
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import type { App, WorkspaceLeaf } from "obsidian";
|
||||
|
||||
import { VIEW_TYPE_CODEX_PANEL } from "../constants";
|
||||
import { hasPendingRequests, pendingRequestCounts } from "../domain/pending-requests/aggregate";
|
||||
import type { ChatWorkspacePanelSnapshot, ChatWorkspacePanelSurface } from "../features/chat/host/contracts";
|
||||
import { CodexChatView } from "../features/chat/host/view.obsidian";
|
||||
import { parseChatPanelViewState } from "../features/chat/host/view-state";
|
||||
|
|
@ -661,12 +660,7 @@ export class WorkspacePanelCoordinator {
|
|||
}
|
||||
|
||||
function isIdleEmptyPanelSnapshot(snapshot: ChatWorkspacePanelSnapshot): boolean {
|
||||
return (
|
||||
snapshot.threadId === null &&
|
||||
snapshot.turnLifecycle.kind === "idle" &&
|
||||
!hasPendingRequests(snapshot.pendingRequests) &&
|
||||
!snapshot.hasComposerDraft
|
||||
);
|
||||
return snapshot.threadId === null && !snapshot.turnBusy && !snapshot.pending && !snapshot.hasComposerDraft;
|
||||
}
|
||||
|
||||
function focusedPanelViewId(leaf: WorkspaceLeaf | null): string | null {
|
||||
|
|
@ -688,8 +682,8 @@ function restoredPanelSnapshot(leaf: WorkspaceLeaf, index: number): WorkspacePan
|
|||
return {
|
||||
viewId: `restored:${String(index)}:${threadId}`,
|
||||
threadId,
|
||||
turnLifecycle: { kind: "idle" },
|
||||
pendingRequests: pendingRequestCounts({ pendingApprovals: 0, pendingUserInputs: 0, pendingMcpElicitations: 0 }),
|
||||
turnBusy: false,
|
||||
pending: false,
|
||||
hasComposerDraft: false,
|
||||
connected: false,
|
||||
lastFocused: false,
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ function createActionsHarness({ connected = false, canConnect = true } = {}) {
|
|||
setStatus: vi.fn(),
|
||||
addSystemMessage: vi.fn(),
|
||||
configuredCommand: () => "codex",
|
||||
refreshLiveState: vi.fn(),
|
||||
isStaleConnectionError: () => false,
|
||||
isStaleResourceContextError: () => false,
|
||||
notifyConnectionFailed: vi.fn(),
|
||||
|
|
@ -228,7 +227,6 @@ describe("ChatConnectionActions", () => {
|
|||
message: "Codex app-server stopped.",
|
||||
});
|
||||
expect(host.resetThreadTurnPresence).toHaveBeenCalledWith(false);
|
||||
expect(host.refreshLiveState).toHaveBeenCalledOnce();
|
||||
expect(stateStore.getState()).toMatchObject({
|
||||
threadList: {
|
||||
listedThreads: [{ id: "thread-1", title: "Thread 1" }],
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ function expectPresent<T>(value: T | null | undefined): T {
|
|||
}
|
||||
|
||||
describe("PendingRequestActions", () => {
|
||||
it("resolves user input from immutable draft state and refreshes the host", () => {
|
||||
const { stateStore, responder, refreshLiveState, focusComposer, pendingRequests } = actionsHarness();
|
||||
it("resolves user input from immutable draft state and restores composer focus", () => {
|
||||
const { stateStore, responder, focusComposer, pendingRequests } = actionsHarness();
|
||||
const input = expectPresent(toPendingUserInput(userInputRequest()));
|
||||
stateStore.dispatch({ type: "request/user-input-queued", input });
|
||||
stateStore.dispatch({ type: "request/user-input-draft-set", key: "7:direction", value: "Left" });
|
||||
|
|
@ -21,52 +21,45 @@ describe("PendingRequestActions", () => {
|
|||
pendingRequests.actions().resolveUserInput(input.requestId);
|
||||
|
||||
expect(responder.resolveUserInput).toHaveBeenCalledWith(input.requestId, { direction: "Left" });
|
||||
expect(refreshLiveState).toHaveBeenCalledOnce();
|
||||
expect(focusComposer).toHaveBeenCalledOnce();
|
||||
expect(expectPresent(refreshLiveState.mock.invocationCallOrder[0])).toBeLessThan(
|
||||
expectPresent(focusComposer.mock.invocationCallOrder[0]),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["allows", "accept"],
|
||||
["denies", "decline"],
|
||||
] satisfies [string, ApprovalAction][])("%s a queued approval and commits the action", (_label, action) => {
|
||||
const { stateStore, responder, refreshLiveState, focusComposer, pendingRequests } = actionsHarness();
|
||||
const { stateStore, responder, focusComposer, pendingRequests } = actionsHarness();
|
||||
stateStore.dispatch({ type: "request/approval-queued", approval: approvalRequest() });
|
||||
|
||||
pendingRequests.actions().resolveApproval(1, action);
|
||||
|
||||
expect(responder.resolveApproval).toHaveBeenCalledWith(1, action);
|
||||
expect(refreshLiveState).toHaveBeenCalledOnce();
|
||||
expect(focusComposer).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("cancels queued user input and commits the action", () => {
|
||||
const { stateStore, responder, refreshLiveState, focusComposer, pendingRequests } = actionsHarness();
|
||||
const { stateStore, responder, focusComposer, pendingRequests } = actionsHarness();
|
||||
const input = expectPresent(toPendingUserInput(userInputRequest()));
|
||||
stateStore.dispatch({ type: "request/user-input-queued", input });
|
||||
|
||||
pendingRequests.actions().cancelUserInput(input.requestId);
|
||||
|
||||
expect(responder.cancelUserInput).toHaveBeenCalledWith(input.requestId);
|
||||
expect(refreshLiveState).toHaveBeenCalledOnce();
|
||||
expect(focusComposer).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("resolves a queued MCP elicitation and commits the action", () => {
|
||||
const { stateStore, responder, refreshLiveState, focusComposer, pendingRequests } = actionsHarness();
|
||||
const { stateStore, responder, focusComposer, pendingRequests } = actionsHarness();
|
||||
stateStore.dispatch({ type: "request/mcp-elicitation-queued", elicitation: mcpElicitationRequest() });
|
||||
|
||||
pendingRequests.actions().resolveMcpElicitation(9, "accept");
|
||||
|
||||
expect(responder.resolveMcpElicitation).toHaveBeenCalledWith(9, "accept");
|
||||
expect(refreshLiveState).toHaveBeenCalledOnce();
|
||||
expect(focusComposer).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("ignores actions for requests that disappeared before the action ran", () => {
|
||||
const { stateStore, responder, refreshLiveState, focusComposer, pendingRequests } = actionsHarness();
|
||||
const { stateStore, responder, focusComposer, pendingRequests } = actionsHarness();
|
||||
const input = expectPresent(toPendingUserInput(userInputRequest()));
|
||||
stateStore.dispatch({ type: "request/approval-queued", approval: approvalRequest() });
|
||||
stateStore.dispatch({ type: "request/user-input-queued", input });
|
||||
|
|
@ -84,7 +77,6 @@ describe("PendingRequestActions", () => {
|
|||
expect(responder.resolveUserInput).not.toHaveBeenCalled();
|
||||
expect(responder.cancelUserInput).not.toHaveBeenCalled();
|
||||
expect(responder.resolveMcpElicitation).not.toHaveBeenCalled();
|
||||
expect(refreshLiveState).not.toHaveBeenCalled();
|
||||
expect(focusComposer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
@ -147,15 +139,13 @@ function actionsHarness(composerHasFocus = vi.fn(() => false)) {
|
|||
resolveMcpElicitation: vi.fn(),
|
||||
};
|
||||
const focusComposer = vi.fn();
|
||||
const refreshLiveState = vi.fn();
|
||||
const pendingRequests = createPendingRequestActions({
|
||||
stateStore,
|
||||
responder,
|
||||
composerHasFocus,
|
||||
focusComposer,
|
||||
refreshLiveState,
|
||||
});
|
||||
return { stateStore, responder, focusComposer, refreshLiveState, pendingRequests };
|
||||
return { stateStore, responder, focusComposer, pendingRequests };
|
||||
}
|
||||
|
||||
function approvalRequest(): PendingApproval {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ describe("createGoalActions", () => {
|
|||
const stateStore = createChatStateStore(state);
|
||||
const currentGoal = goal();
|
||||
const goalTransport = goalTransportFixture({ readThreadGoal: vi.fn().mockResolvedValue(currentGoal) });
|
||||
const refreshLiveState = vi.fn();
|
||||
const actions = createGoalActions({
|
||||
stateStore,
|
||||
goalTransport,
|
||||
|
|
@ -28,13 +27,11 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-activated", threadId: "thread" }),
|
||||
addSystemMessage: vi.fn(),
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState,
|
||||
});
|
||||
|
||||
await actions.syncThreadGoal("thread");
|
||||
|
||||
expect(activeThreadState(stateStore.getState())?.goal).toEqual(currentGoal);
|
||||
expect(refreshLiveState).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not publish an old goal read after a shared mutation completes", async () => {
|
||||
|
|
@ -52,7 +49,6 @@ describe("createGoalActions", () => {
|
|||
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
|
||||
addSystemMessage: vi.fn(),
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
};
|
||||
const goalOperations = createThreadGoalOperationCoordinator();
|
||||
const sync = createThreadGoalSyncActions(host, goalOperations);
|
||||
|
|
@ -89,7 +85,6 @@ describe("createGoalActions", () => {
|
|||
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
|
||||
addSystemMessage,
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
};
|
||||
const goalOperations = createThreadGoalOperationCoordinator();
|
||||
const sync = createThreadGoalSyncActions(host, goalOperations);
|
||||
|
|
@ -126,7 +121,6 @@ describe("createGoalActions", () => {
|
|||
localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }),
|
||||
addSystemMessage: vi.fn(),
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
};
|
||||
const sync = createThreadGoalSyncActions(host, goalOperations);
|
||||
const actions = createGoalActions(
|
||||
|
|
@ -159,7 +153,6 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-activated", threadId: "thread" }),
|
||||
addSystemMessage,
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await actions.syncThreadGoal("thread");
|
||||
|
|
@ -190,7 +183,6 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-activated", threadId: "thread" }),
|
||||
addSystemMessage,
|
||||
addGoalEvent,
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await actions.setObjective("thread", " Updated ", 250);
|
||||
|
|
@ -226,7 +218,6 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-activated", threadId: "thread" }),
|
||||
addSystemMessage: vi.fn(),
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
const objectiveUpdate = actions.setObjective("thread", "Updated", null);
|
||||
|
|
@ -266,7 +257,6 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-activated", threadId: "thread-a" }),
|
||||
addSystemMessage: vi.fn(),
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
const oldUpdate = actions.setObjective("thread-a", "Old", null);
|
||||
|
|
@ -304,7 +294,6 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-activated", threadId: "thread" }),
|
||||
addSystemMessage: vi.fn(),
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
},
|
||||
goalOperations,
|
||||
);
|
||||
|
|
@ -343,7 +332,6 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-activated", threadId: "side" }),
|
||||
addSystemMessage,
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await expect(actions.setObjective("side", "Ship", null)).resolves.toBe(false);
|
||||
|
|
@ -369,7 +357,6 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-activated", threadId: "thread" }),
|
||||
addSystemMessage,
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
const pending = actions.setStatus("thread", "paused");
|
||||
|
|
@ -396,7 +383,6 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-activated", threadId: "thread" }),
|
||||
addSystemMessage,
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
const pending = actions.clear("thread");
|
||||
|
|
@ -421,7 +407,6 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-activated", threadId: "thread" }),
|
||||
addSystemMessage: vi.fn(),
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
const pending = actions.clear("thread");
|
||||
|
|
@ -445,7 +430,6 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-activated", threadId: "thread" }),
|
||||
addSystemMessage,
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
const pending = actions.setObjective("thread", "Finish", null);
|
||||
|
|
@ -472,7 +456,6 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-activated", threadId: "thread" }),
|
||||
addSystemMessage,
|
||||
addGoalEvent,
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await actions.setObjective("thread", "Finish", null);
|
||||
|
|
@ -528,7 +511,6 @@ describe("createGoalActions", () => {
|
|||
startThread,
|
||||
addSystemMessage: vi.fn(),
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await expect(actions.saveObjective(" Plan release ", null)).resolves.toBe(true);
|
||||
|
|
@ -549,7 +531,6 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-not-activated", threadId: "thread-new" }),
|
||||
addSystemMessage,
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await expect(actions.saveObjective("Plan release", null)).resolves.toBe(false);
|
||||
|
|
@ -572,7 +553,6 @@ describe("createGoalActions", () => {
|
|||
startThread,
|
||||
addSystemMessage: vi.fn(),
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
const pending = actions.saveObjective("Plan release", null);
|
||||
|
|
@ -626,7 +606,6 @@ describe("createGoalActions", () => {
|
|||
ensureRestoredThreadLoaded,
|
||||
addSystemMessage: vi.fn(),
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await expect(actions.saveObjective("Resume work", null)).resolves.toBe(true);
|
||||
|
|
@ -652,7 +631,6 @@ describe("createGoalActions", () => {
|
|||
startThread,
|
||||
addSystemMessage,
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await expect(actions.saveObjective(" ", null)).resolves.toBe(false);
|
||||
|
|
@ -678,7 +656,6 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-activated", threadId: "thread" }),
|
||||
addSystemMessage,
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await actions.setObjective("thread", "Finish", null);
|
||||
|
|
@ -705,7 +682,6 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-activated", threadId: "thread" }),
|
||||
addSystemMessage,
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await actions.setObjective("thread", "Finish", null);
|
||||
|
|
@ -730,7 +706,6 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-activated", threadId: "thread" }),
|
||||
addSystemMessage: vi.fn(),
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await expect(actions.setObjective("thread", "Finish", null)).resolves.toBe(false);
|
||||
|
|
@ -754,7 +729,6 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-activated", threadId: "thread" }),
|
||||
addSystemMessage: vi.fn(),
|
||||
addGoalEvent,
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await actions.setObjective("thread", "Updated", null);
|
||||
|
|
@ -778,7 +752,6 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-activated", threadId: "thread" }),
|
||||
addSystemMessage,
|
||||
addGoalEvent,
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await actions.setStatus("thread", "active");
|
||||
|
|
@ -801,7 +774,6 @@ describe("createGoalActions", () => {
|
|||
startThread: vi.fn().mockResolvedValue({ kind: "created-activated", threadId: "thread" }),
|
||||
addSystemMessage,
|
||||
addGoalEvent: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
});
|
||||
|
||||
await actions.syncThreadGoal("thread");
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ function createActions(response: ThreadResumeSnapshot | null = activation("threa
|
|||
notifyActiveThreadIdentityChanged: vi.fn(),
|
||||
recordResumedThread: vi.fn(),
|
||||
addSystemMessage: vi.fn(),
|
||||
refreshLiveState: vi.fn(),
|
||||
syncThreadGoal: vi.fn().mockResolvedValue(undefined),
|
||||
...overrides,
|
||||
resumeTransport: overrides.resumeTransport ?? { ensureConnected: vi.fn().mockResolvedValue(true), resumeThread },
|
||||
|
|
@ -148,16 +147,6 @@ describe("ResumeActions", () => {
|
|||
expect(activeThreadId(stateStore.getState())).toBe("second");
|
||||
});
|
||||
|
||||
it("refreshes live state after resumed history and goal sync finish", async () => {
|
||||
const { actions, host } = createActions();
|
||||
|
||||
await actions.resumeThread("thread");
|
||||
|
||||
expect(vi.mocked(host.refreshLiveState).mock.invocationCallOrder.at(-1)).toBeGreaterThan(
|
||||
vi.mocked(host.syncThreadGoal).mock.invocationCallOrder[0] ?? 0,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not switch threads while a different turn is busy", async () => {
|
||||
const { actions, host, resumeThread, stateStore } = createActions();
|
||||
stateStore.dispatch({
|
||||
|
|
|
|||
|
|
@ -100,16 +100,10 @@ describe("ChatPanelSessionRuntime actions", () => {
|
|||
expect(stateStore.getState().threadList.listedThreads).toEqual([]);
|
||||
});
|
||||
|
||||
it("fans out active-thread identity changes after starting a new thread", async () => {
|
||||
const refreshLiveState = vi.fn();
|
||||
it("refreshes persisted view identity after starting a new thread", async () => {
|
||||
const refreshTabHeader = vi.fn();
|
||||
const { runtime, stateStore } = sessionRuntimeFixture({
|
||||
environment: {
|
||||
plugin: {
|
||||
workspace: {
|
||||
refreshThreadsViewLiveState: refreshLiveState,
|
||||
},
|
||||
},
|
||||
view: {
|
||||
refreshTabHeader,
|
||||
},
|
||||
|
|
@ -133,7 +127,6 @@ describe("ChatPanelSessionRuntime actions", () => {
|
|||
await runtime.actions.startNewThread();
|
||||
|
||||
expect(refreshTabHeader).toHaveBeenCalledOnce();
|
||||
expect(refreshLiveState).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("wires reconnect cleanup through the runtime toolbar action", async () => {
|
||||
|
|
@ -169,11 +162,9 @@ describe("ChatPanelSessionRuntime actions", () => {
|
|||
vi.useFakeTimers();
|
||||
const unsubscribeThreads = vi.fn();
|
||||
const unsubscribeMetadata = vi.fn();
|
||||
const refreshLiveState = vi.fn();
|
||||
const { runtime, stateStore, deferredTasks, threadStreamScrollBinding } = sessionRuntimeFixture({
|
||||
environment: {
|
||||
plugin: {
|
||||
workspace: { refreshThreadsViewLiveState: refreshLiveState },
|
||||
threadCatalog: { observeActive: vi.fn(() => unsubscribeThreads) },
|
||||
appServerQueries: {
|
||||
observeAppServerMetadataResources: vi.fn(() => unsubscribeMetadata),
|
||||
|
|
@ -203,12 +194,10 @@ describe("ChatPanelSessionRuntime actions", () => {
|
|||
expect(dispatchScrollCommand).not.toHaveBeenCalled();
|
||||
expect(unmount).toHaveBeenCalledOnce();
|
||||
expect(disconnect).toHaveBeenCalledOnce();
|
||||
expect(refreshLiveState).toHaveBeenCalledOnce();
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
expect(diagnostics).not.toHaveBeenCalled();
|
||||
expect(warmup).not.toHaveBeenCalled();
|
||||
expect(refreshLiveState).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
function sessionRuntimeFixture(options: { environment?: PartialChatPanelEnvironment } = {}): {
|
||||
|
|
@ -230,7 +219,6 @@ describe("ChatPanelSessionRuntime actions", () => {
|
|||
resumeWork,
|
||||
threadStreamScrollBinding,
|
||||
getClosing: () => false,
|
||||
viewWindow: () => window,
|
||||
});
|
||||
return { runtime, stateStore, resumeWork, deferredTasks, threadStreamScrollBinding };
|
||||
}
|
||||
|
|
@ -298,7 +286,7 @@ describe("ChatPanelSessionRuntime actions", () => {
|
|||
openThreadInNewView: vi.fn().mockResolvedValue(undefined),
|
||||
focusThreadInOpenView: vi.fn().mockResolvedValue(false),
|
||||
openTurnDiff: vi.fn().mockResolvedValue(undefined),
|
||||
refreshThreadsViewLiveState: vi.fn(),
|
||||
notifyPanelActivityChanged: vi.fn(),
|
||||
...overrides.plugin?.workspace,
|
||||
openSideChat: overrides.plugin?.workspace?.openSideChat ?? vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -94,7 +94,6 @@ function turnBundleFixture(options: { stateStore?: ReturnType<typeof createChatS
|
|||
reconnect: vi.fn(),
|
||||
runtimeProjection,
|
||||
refreshDiagnostics,
|
||||
refreshLiveState: vi.fn(),
|
||||
notifyActiveThreadIdentityChanged: vi.fn(),
|
||||
} as never,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ServerNotification } from "../../../../src/app-server/connection/rpc-messages";
|
||||
import type { ServerNotification, ServerRequest } from "../../../../src/app-server/connection/rpc-messages";
|
||||
import { modelMetadataFromCatalogModels } from "../../../../src/app-server/protocol/catalog";
|
||||
import type { ThreadRecord } from "../../../../src/app-server/protocol/thread";
|
||||
import type { ObservedResult } from "../../../../src/app-server/query/observed-result";
|
||||
|
|
@ -38,6 +38,9 @@ const connectionMock = vi.hoisted(() => {
|
|||
connectCalls: 0,
|
||||
connected: false,
|
||||
onNotification: null as ((notification: ServerNotification) => void) | null,
|
||||
onServerRequest: null as
|
||||
| ((request: ServerRequest, responder: { respond(result: unknown): void; reject(code: number, message: string): void }) => void)
|
||||
| null,
|
||||
onExit: null as (() => void) | null,
|
||||
};
|
||||
|
||||
|
|
@ -48,6 +51,7 @@ const connectionMock = vi.hoisted(() => {
|
|||
state.connectCalls = 0;
|
||||
state.connected = false;
|
||||
state.onNotification = null;
|
||||
state.onServerRequest = null;
|
||||
state.onExit = null;
|
||||
},
|
||||
};
|
||||
|
|
@ -120,6 +124,7 @@ vi.mock("../../../../src/app-server/connection/connection-manager", () => {
|
|||
private publishHandlers(handlers: NonNullable<ConnectionManager["handlers"]>): void {
|
||||
const context = { codexPath: this.codexPath(), cwd: this.cwd };
|
||||
connectionMock.state.onNotification = (notification) => handlers.onNotification(notification, context);
|
||||
connectionMock.state.onServerRequest = (request, responder) => handlers.onServerRequest(request, responder);
|
||||
connectionMock.state.onExit = () => handlers.onExit(context);
|
||||
}
|
||||
}
|
||||
|
|
@ -621,6 +626,39 @@ describe("CodexChatView connection lifecycle", () => {
|
|||
expect(requestMethods(client)).not.toContain("thread/list");
|
||||
});
|
||||
|
||||
it("notifies Threads immediately and after the workspace settles when the panel closes", async () => {
|
||||
const notifyPanelActivityChanged = vi.fn();
|
||||
connectionMock.state.client = connectedClient();
|
||||
const view = await chatView({ host: chatHost({ notifyPanelActivityChanged }) });
|
||||
|
||||
await view.onOpen();
|
||||
await view.surface.openThread("thread-1");
|
||||
notifyPanelActivityChanged.mockClear();
|
||||
await view.onClose();
|
||||
|
||||
expect(notifyPanelActivityChanged).toHaveBeenCalledOnce();
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
||||
expect(notifyPanelActivityChanged).toHaveBeenCalledTimes(2);
|
||||
|
||||
connectionMock.state.onNotification?.({
|
||||
method: "turn/started",
|
||||
params: {
|
||||
threadId: "thread-1",
|
||||
turn: {
|
||||
id: "late-turn",
|
||||
status: "inProgress",
|
||||
startedAt: 1,
|
||||
completedAt: null,
|
||||
durationMs: null,
|
||||
error: null,
|
||||
itemsView: "full",
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
} satisfies Extract<ServerNotification, { method: "turn/started" }>);
|
||||
expect(notifyPanelActivityChanged).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("ignores stale connection work after the app-server exits during metadata loading", async () => {
|
||||
const config = deferred<unknown>();
|
||||
const client = connectedClient({
|
||||
|
|
@ -733,7 +771,7 @@ describe("CodexChatView connection lifecycle", () => {
|
|||
expect(view.getState()).toEqual({ version: 1, threadId: "thread-2", threadTitle: "Restored thread 2" });
|
||||
expect(view.surface.openPanelSnapshot()).toMatchObject({
|
||||
threadId: "thread-2",
|
||||
turnLifecycle: { kind: "idle" },
|
||||
turnBusy: false,
|
||||
hasComposerDraft: false,
|
||||
});
|
||||
expect(composerElement(view).value).toBe("");
|
||||
|
|
@ -866,7 +904,7 @@ describe("CodexChatView connection lifecycle", () => {
|
|||
clientUserMessageId: expect.stringMatching(/^local-user-\d+-[A-Za-z0-9_-]+-[a-z0-9]+$/),
|
||||
});
|
||||
});
|
||||
expect(view.surface.openPanelSnapshot()).toMatchObject({ turnLifecycle: { kind: "starting" } });
|
||||
expect(view.surface.openPanelSnapshot()).toMatchObject({ turnBusy: true });
|
||||
expect(view.getState()).toEqual({ version: 1, threadId: "thread-1", threadTitle: "Restored thread" });
|
||||
connectionMock.state.onNotification?.({
|
||||
method: "turn/started",
|
||||
|
|
@ -884,7 +922,83 @@ describe("CodexChatView connection lifecycle", () => {
|
|||
},
|
||||
},
|
||||
} satisfies Extract<ServerNotification, { method: "turn/started" }>);
|
||||
expect(view.surface.openPanelSnapshot()).toMatchObject({ turnLifecycle: { kind: "running", turnId: "turn-1" } });
|
||||
expect(view.surface.openPanelSnapshot()).toMatchObject({ turnBusy: true });
|
||||
});
|
||||
|
||||
it("notifies Threads only when panel activity changes", async () => {
|
||||
const notifyPanelActivityChanged = vi.fn();
|
||||
const client = connectedClient();
|
||||
connectionMock.state.client = client;
|
||||
const view = await chatView({ host: chatHost({ notifyPanelActivityChanged }) });
|
||||
|
||||
await view.onOpen();
|
||||
expect(notifyPanelActivityChanged).toHaveBeenCalledOnce();
|
||||
|
||||
await view.surface.openThread("thread-1");
|
||||
notifyPanelActivityChanged.mockClear();
|
||||
|
||||
view.surface.setComposerText("hello");
|
||||
expect(notifyPanelActivityChanged).not.toHaveBeenCalled();
|
||||
|
||||
await submitComposerByEnter(view);
|
||||
expect(notifyPanelActivityChanged).toHaveBeenCalledOnce();
|
||||
expect(view.surface.openPanelSnapshot()).toMatchObject({ threadId: "thread-1", turnBusy: true, pending: false });
|
||||
|
||||
notifyPanelActivityChanged.mockClear();
|
||||
connectionMock.state.onNotification?.({
|
||||
method: "turn/started",
|
||||
params: {
|
||||
threadId: "thread-1",
|
||||
turn: {
|
||||
id: "turn-1",
|
||||
status: "inProgress",
|
||||
startedAt: 1,
|
||||
completedAt: null,
|
||||
durationMs: null,
|
||||
error: null,
|
||||
itemsView: "full",
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
} satisfies Extract<ServerNotification, { method: "turn/started" }>);
|
||||
expect(notifyPanelActivityChanged).not.toHaveBeenCalled();
|
||||
|
||||
connectionMock.state.onServerRequest?.(
|
||||
{
|
||||
id: 7,
|
||||
method: "item/tool/requestUserInput",
|
||||
params: {
|
||||
threadId: "thread-1",
|
||||
turnId: "turn-1",
|
||||
itemId: "input-1",
|
||||
questions: [{ id: "note", header: "Note", question: "What now?", isOther: false, isSecret: false, options: null }],
|
||||
autoResolutionMs: null,
|
||||
},
|
||||
},
|
||||
{ respond: vi.fn(), reject: vi.fn() },
|
||||
);
|
||||
expect(notifyPanelActivityChanged).toHaveBeenCalledOnce();
|
||||
expect(view.surface.openPanelSnapshot()).toMatchObject({ threadId: "thread-1", turnBusy: true, pending: true });
|
||||
|
||||
notifyPanelActivityChanged.mockClear();
|
||||
connectionMock.state.onNotification?.({
|
||||
method: "turn/completed",
|
||||
params: {
|
||||
threadId: "thread-1",
|
||||
turn: {
|
||||
id: "turn-1",
|
||||
status: "completed",
|
||||
startedAt: 1,
|
||||
completedAt: 2,
|
||||
durationMs: 1,
|
||||
error: null,
|
||||
itemsView: "full",
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
} satisfies Extract<ServerNotification, { method: "turn/completed" }>);
|
||||
expect(notifyPanelActivityChanged).toHaveBeenCalledOnce();
|
||||
expect(view.surface.openPanelSnapshot()).toMatchObject({ threadId: "thread-1", turnBusy: false, pending: true });
|
||||
});
|
||||
|
||||
it("requests a workspace layout save after resuming a thread", async () => {
|
||||
|
|
@ -978,7 +1092,7 @@ describe("CodexChatView connection lifecycle", () => {
|
|||
await view.surface.openThread("other");
|
||||
|
||||
expect(requestMethods(client).filter((method) => method === "thread/unsubscribe")).toEqual([]);
|
||||
expect(view.surface.openPanelSnapshot()).toMatchObject({ threadId: "child", turnLifecycle: { kind: "running", turnId: "turn-child" } });
|
||||
expect(view.surface.openPanelSnapshot()).toMatchObject({ threadId: "child", turnBusy: true });
|
||||
});
|
||||
|
||||
it("resets to an unstarted empty chat without starting a thread", async () => {
|
||||
|
|
@ -992,7 +1106,7 @@ describe("CodexChatView connection lifecycle", () => {
|
|||
|
||||
expect(requestMethods(client)).not.toContain("thread/start");
|
||||
expect(view.getState()).toEqual({ version: 1 });
|
||||
expect(view.surface.openPanelSnapshot()).toMatchObject({ threadId: null, turnLifecycle: { kind: "idle" }, hasComposerDraft: false });
|
||||
expect(view.surface.openPanelSnapshot()).toMatchObject({ threadId: null, turnBusy: false, hasComposerDraft: false });
|
||||
expect(requestSaveLayout).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
|
|
@ -1002,7 +1116,7 @@ describe("CodexChatView connection lifecycle", () => {
|
|||
|
||||
expect(view.getState()).toEqual({ version: 1 });
|
||||
expect(view.getDisplayText()).not.toBe("Side chat");
|
||||
expect(view.surface.openPanelSnapshot()).toMatchObject({ threadId: null, turnLifecycle: { kind: "idle" }, hasComposerDraft: false });
|
||||
expect(view.surface.openPanelSnapshot()).toMatchObject({ threadId: null, turnBusy: false, hasComposerDraft: false });
|
||||
expect(view.containerEl.textContent).not.toContain("This side conversation is no longer available.");
|
||||
});
|
||||
|
||||
|
|
@ -1456,7 +1570,7 @@ interface ChatHostFixtureOverrides {
|
|||
openThreadInNewView?: CodexChatHost["workspace"]["openThreadInNewView"];
|
||||
focusThreadInOpenView?: CodexChatHost["workspace"]["focusThreadInOpenView"];
|
||||
openTurnDiff?: CodexChatHost["workspace"]["openTurnDiff"];
|
||||
refreshThreadsViewLiveState?: CodexChatHost["workspace"]["refreshThreadsViewLiveState"];
|
||||
notifyPanelActivityChanged?: CodexChatHost["workspace"]["notifyPanelActivityChanged"];
|
||||
openSideChat?: CodexChatHost["workspace"]["openSideChat"];
|
||||
applyThreadCatalogEvent?: CodexChatHost["threadCatalog"]["apply"];
|
||||
refreshActive?: CodexChatHost["threadCatalog"]["refreshActive"];
|
||||
|
|
@ -1605,7 +1719,7 @@ function chatHost(overrides: ChatHostFixtureOverrides = {}): TestCodexChatHost {
|
|||
openThreadInNewView: overrides.openThreadInNewView ?? vi.fn(),
|
||||
focusThreadInOpenView: overrides.focusThreadInOpenView ?? vi.fn().mockResolvedValue(false),
|
||||
openTurnDiff: overrides.openTurnDiff ?? vi.fn(),
|
||||
refreshThreadsViewLiveState: overrides.refreshThreadsViewLiveState ?? vi.fn(),
|
||||
notifyPanelActivityChanged: overrides.notifyPanelActivityChanged ?? vi.fn(),
|
||||
openSideChat: overrides.openSideChat ?? vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
appServerQueries: {
|
||||
|
|
|
|||
|
|
@ -101,7 +101,6 @@ function composerControllerFixture(
|
|||
togglePlan: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
toggleFast: vi.fn(),
|
||||
onDraftChange: vi.fn(),
|
||||
onHeightChange: vi.fn(),
|
||||
canFocus: () => true,
|
||||
...options.controller,
|
||||
|
|
@ -248,7 +247,6 @@ describe("ChatComposerController", () => {
|
|||
togglePlan: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
toggleFast: vi.fn(),
|
||||
onDraftChange: vi.fn(),
|
||||
canFocus: () => true,
|
||||
onHeightChange: vi.fn(),
|
||||
});
|
||||
|
|
@ -413,7 +411,6 @@ describe("ChatComposerController", () => {
|
|||
togglePlan: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
toggleFast: vi.fn(),
|
||||
onDraftChange: vi.fn(),
|
||||
canFocus: () => true,
|
||||
onHeightChange: vi.fn(),
|
||||
});
|
||||
|
|
@ -474,7 +471,6 @@ describe("ChatComposerController", () => {
|
|||
togglePlan: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
toggleFast: vi.fn(),
|
||||
onDraftChange: vi.fn(),
|
||||
canFocus: () => true,
|
||||
onHeightChange: vi.fn(),
|
||||
});
|
||||
|
|
@ -627,7 +623,6 @@ describe("ChatComposerController", () => {
|
|||
togglePlan: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
toggleFast: vi.fn(),
|
||||
onDraftChange: vi.fn(),
|
||||
canFocus: () => true,
|
||||
onHeightChange: vi.fn(),
|
||||
});
|
||||
|
|
@ -689,7 +684,6 @@ describe("ChatComposerController", () => {
|
|||
togglePlan: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
toggleFast: vi.fn(),
|
||||
onDraftChange: vi.fn(),
|
||||
canFocus: () => true,
|
||||
onHeightChange: vi.fn(),
|
||||
});
|
||||
|
|
@ -750,7 +744,6 @@ describe("ChatComposerController", () => {
|
|||
togglePlan: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
toggleFast: vi.fn(),
|
||||
onDraftChange: vi.fn(),
|
||||
canFocus: () => true,
|
||||
onHeightChange: vi.fn(),
|
||||
});
|
||||
|
|
@ -972,7 +965,6 @@ describe("ChatComposerController", () => {
|
|||
togglePlan: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
toggleFast: vi.fn(),
|
||||
onDraftChange: vi.fn(),
|
||||
canFocus: () => true,
|
||||
onHeightChange: vi.fn(),
|
||||
});
|
||||
|
|
@ -1032,7 +1024,6 @@ describe("ChatComposerController", () => {
|
|||
togglePlan: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
toggleFast: vi.fn(),
|
||||
onDraftChange: vi.fn(),
|
||||
canFocus: () => true,
|
||||
onHeightChange: vi.fn(),
|
||||
});
|
||||
|
|
@ -1078,7 +1069,6 @@ describe("ChatComposerController", () => {
|
|||
togglePlan: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
toggleFast: vi.fn(),
|
||||
onDraftChange: vi.fn(),
|
||||
canFocus: () => true,
|
||||
onHeightChange: vi.fn(),
|
||||
});
|
||||
|
|
@ -1130,7 +1120,6 @@ describe("ChatComposerController", () => {
|
|||
togglePlan: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
toggleFast: vi.fn(),
|
||||
onDraftChange: vi.fn(),
|
||||
canFocus: () => true,
|
||||
onHeightChange: vi.fn(),
|
||||
});
|
||||
|
|
@ -1189,7 +1178,6 @@ describe("ChatComposerController", () => {
|
|||
togglePlan: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
toggleFast: vi.fn(),
|
||||
onDraftChange: vi.fn(),
|
||||
canFocus: () => true,
|
||||
onHeightChange: vi.fn(),
|
||||
});
|
||||
|
|
@ -1266,7 +1254,6 @@ describe("ChatComposerController", () => {
|
|||
togglePlan: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
toggleFast: vi.fn(),
|
||||
onDraftChange: vi.fn(),
|
||||
canFocus: () => true,
|
||||
onHeightChange: vi.fn(),
|
||||
});
|
||||
|
|
@ -1315,7 +1302,6 @@ describe("ChatComposerController", () => {
|
|||
togglePlan: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
toggleFast: vi.fn(),
|
||||
onDraftChange: vi.fn(),
|
||||
canFocus: () => true,
|
||||
onHeightChange: vi.fn(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ describe("ChatPanelShell", () => {
|
|||
togglePlan: vi.fn(),
|
||||
toggleAutoReview: vi.fn(),
|
||||
toggleFast: vi.fn(),
|
||||
onDraftChange: vi.fn(),
|
||||
canFocus: () => true,
|
||||
onHeightChange: vi.fn(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ function chatHostFixture(): CodexChatHost {
|
|||
openThreadInNewView: vi.fn(),
|
||||
focusThreadInOpenView: vi.fn(),
|
||||
openTurnDiff: vi.fn(),
|
||||
refreshThreadsViewLiveState: vi.fn(),
|
||||
notifyPanelActivityChanged: vi.fn(),
|
||||
openSideChat: vi.fn(),
|
||||
},
|
||||
appServerQueries: {
|
||||
|
|
@ -181,17 +181,11 @@ export function thread(id: string): Thread {
|
|||
|
||||
export 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" },
|
||||
pendingRequests,
|
||||
turnBusy: false,
|
||||
pending: (pendingApprovals ?? 0) + (pendingUserInputs ?? 0) + (pendingMcpElicitations ?? 0) > 0,
|
||||
hasComposerDraft: false,
|
||||
connected: true,
|
||||
...snapshotOverrides,
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ describe("WorkspacePanelCoordinator", () => {
|
|||
const coordinator = panels(await pluginWithLeaves([restoredLeaf]));
|
||||
|
||||
expect(coordinator.getOpenPanelSnapshots()).toMatchObject([
|
||||
{ threadId: "thread-1", connected: false, lastFocused: false, pendingRequests: { actionable: 0 } },
|
||||
{ threadId: "thread-1", connected: false, lastFocused: false, turnBusy: false, pending: false },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue