mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 17:30:31 +00:00
Functionize stateless chat controllers
This commit is contained in:
parent
985036e63e
commit
02df0cc4b7
20 changed files with 332 additions and 290 deletions
|
|
@ -12,15 +12,15 @@ import type { ThreadRenameController } from "../threads/thread-rename-controller
|
|||
import type { ToolbarPanelController } from "./toolbar-controller";
|
||||
import type { AppServerWarmupActions } from "../session/app-server-warmup-controller";
|
||||
import type { ChatConnectionController } from "../session/connection-controller";
|
||||
import type { ChatReconnectController } from "../session/reconnect-controller";
|
||||
import type { ChatReconnectActions } from "../session/reconnect-actions";
|
||||
import type { PendingRequestController } from "../requests/pending-request-controller";
|
||||
import { ServerRequestResponder } from "../requests/server-request-responder";
|
||||
import { createServerRequestActions } from "../requests/server-request-actions";
|
||||
import type { ComposerSubmissionController } from "../turns/composer-submission-controller";
|
||||
import type { RestoredThreadController } from "../threads/restored-thread-controller";
|
||||
import type { ThreadIdentityController } from "../threads/thread-identity-controller";
|
||||
import type { ThreadIdentityActions } from "../threads/thread-identity-actions";
|
||||
import type { ThreadResumeController } from "../threads/thread-resume-controller";
|
||||
import type { ThreadSelectionActions } from "../threads/thread-selection-controller";
|
||||
import type { ChatViewOpenCloseController } from "./view-open-close-controller";
|
||||
import type { ChatViewOpenCloseActions } from "./open-close-actions";
|
||||
import type { ChatViewRenderController } from "./view-render-controller";
|
||||
import type { ChatViewStateActions } from "./view-state-controller";
|
||||
import type { ChatMessageRenderer } from "../ui/message-stream";
|
||||
|
|
@ -34,7 +34,7 @@ export interface ChatViewControllers {
|
|||
connection: {
|
||||
manager: ConnectionManager;
|
||||
controller: ChatConnectionController;
|
||||
reconnect: ChatReconnectController;
|
||||
reconnect: ChatReconnectActions;
|
||||
warmup: AppServerWarmupActions;
|
||||
};
|
||||
inbound: {
|
||||
|
|
@ -50,7 +50,7 @@ export interface ChatViewControllers {
|
|||
resume: ThreadResumeController;
|
||||
actions: ChatThreadActionController;
|
||||
restored: RestoredThreadController;
|
||||
identity: ThreadIdentityController;
|
||||
identity: ThreadIdentityActions;
|
||||
rename: ThreadRenameController;
|
||||
selection: ThreadSelectionActions;
|
||||
};
|
||||
|
|
@ -71,7 +71,7 @@ export interface ChatViewControllers {
|
|||
render: {
|
||||
controller: ChatViewRenderController;
|
||||
messages: ChatMessageRenderer;
|
||||
openClose: ChatViewOpenCloseController;
|
||||
openClose: ChatViewOpenCloseActions;
|
||||
viewState: ChatViewStateActions;
|
||||
};
|
||||
}
|
||||
|
|
@ -99,7 +99,7 @@ export function createChatViewControllers(ports: ChatPanelContext): ChatViewCont
|
|||
connection,
|
||||
goals,
|
||||
});
|
||||
const serverRequestResponder = new ServerRequestResponder({
|
||||
const serverRequestResponder = createServerRequestActions({
|
||||
currentClient: ports.client.getClient,
|
||||
});
|
||||
const controller = createChatInboundController(ports, {
|
||||
|
|
|
|||
79
src/features/chat/panel/open-close-actions.ts
Normal file
79
src/features/chat/panel/open-close-actions.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import type { EventRef, WorkspaceLeaf } from "obsidian";
|
||||
|
||||
import { unmountChatPanelShell } from "../ui/shell";
|
||||
|
||||
export interface ChatViewOpenCloseActionsHost {
|
||||
setOpened: (opened: boolean) => void;
|
||||
setClosing: (closing: boolean) => void;
|
||||
registerEvent: (eventRef: EventRef) => void;
|
||||
registerComposerNoteIndexInvalidation: (register: (eventRef: EventRef) => void) => void;
|
||||
registerPointerDown: (handler: (event: PointerEvent) => void) => void;
|
||||
registerActiveLeafChange: (handler: (leaf: WorkspaceLeaf | null) => void) => void;
|
||||
isOwnLeaf: (leaf: WorkspaceLeaf | null) => boolean;
|
||||
scrollMessagesToBottomOnFocus: () => void;
|
||||
applyCachedSharedAppServerState: () => void;
|
||||
render: () => void;
|
||||
scheduleDeferredAppServerWarmup: () => void;
|
||||
scheduleDeferredRestoredThreadHydration: () => void;
|
||||
closeToolbarPanelOnOutsidePointer: (event: PointerEvent) => void;
|
||||
invalidateConnectionWork: () => void;
|
||||
invalidateResumeWork: () => void;
|
||||
clearDeferredTasks: () => void;
|
||||
panelRoot: () => HTMLElement | null;
|
||||
disposeMessages: () => void;
|
||||
disposeComposer: () => void;
|
||||
disconnect: () => void;
|
||||
clearClient: () => void;
|
||||
refreshLiveState: () => void;
|
||||
deferRefreshLiveState: () => void;
|
||||
}
|
||||
|
||||
export interface ChatViewOpenCloseActions {
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export function createChatViewOpenCloseActions(host: ChatViewOpenCloseActionsHost): ChatViewOpenCloseActions {
|
||||
return {
|
||||
open: () => {
|
||||
openChatView(host);
|
||||
},
|
||||
close: () => {
|
||||
closeChatView(host);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function openChatView(host: ChatViewOpenCloseActionsHost): void {
|
||||
host.setOpened(true);
|
||||
host.setClosing(false);
|
||||
host.registerComposerNoteIndexInvalidation((eventRef) => {
|
||||
host.registerEvent(eventRef);
|
||||
});
|
||||
host.registerPointerDown((event) => {
|
||||
host.closeToolbarPanelOnOutsidePointer(event);
|
||||
});
|
||||
host.registerActiveLeafChange((leaf) => {
|
||||
if (host.isOwnLeaf(leaf)) host.scrollMessagesToBottomOnFocus();
|
||||
});
|
||||
host.applyCachedSharedAppServerState();
|
||||
host.render();
|
||||
host.scheduleDeferredAppServerWarmup();
|
||||
host.scheduleDeferredRestoredThreadHydration();
|
||||
}
|
||||
|
||||
function closeChatView(host: ChatViewOpenCloseActionsHost): void {
|
||||
host.setOpened(false);
|
||||
host.setClosing(true);
|
||||
host.invalidateConnectionWork();
|
||||
host.invalidateResumeWork();
|
||||
host.clearDeferredTasks();
|
||||
const panelRoot = host.panelRoot();
|
||||
host.disposeMessages();
|
||||
host.disposeComposer();
|
||||
unmountChatPanelShell(panelRoot);
|
||||
host.disconnect();
|
||||
host.clearClient();
|
||||
host.refreshLiveState();
|
||||
host.deferRefreshLiveState();
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import type { ChatAppServerMetadataController } from "../app-server/metadata-con
|
|||
import type { ChatAppServerThreadController } from "../app-server/thread-controller";
|
||||
import type { ChatComposerController } from "../composer/controller";
|
||||
import { createAppServerWarmupActions } from "../session/app-server-warmup-controller";
|
||||
import { ChatViewOpenCloseController } from "./view-open-close-controller";
|
||||
import { createChatViewOpenCloseActions } from "./open-close-actions";
|
||||
import { ChatViewRenderController } from "./view-render-controller";
|
||||
import type { ChatMessageRenderer } from "../ui/message-stream";
|
||||
import { applyCachedSharedAppServerState } from "./cached-app-server-state";
|
||||
|
|
@ -60,7 +60,7 @@ export function createConnectionLifecycleControllerGroup(
|
|||
connected: () => refs.connection.isConnected(),
|
||||
ensureConnected: client.ensureConnected,
|
||||
}),
|
||||
openCloseController: new ChatViewOpenCloseController({
|
||||
openCloseController: createChatViewOpenCloseActions({
|
||||
setOpened: lifecycle.setOpened,
|
||||
setClosing: lifecycle.setClosing,
|
||||
registerEvent: obsidian.registerEvent,
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
import type { EventRef, WorkspaceLeaf } from "obsidian";
|
||||
|
||||
import { unmountChatPanelShell } from "../ui/shell";
|
||||
|
||||
export interface ChatViewOpenCloseControllerHost {
|
||||
setOpened: (opened: boolean) => void;
|
||||
setClosing: (closing: boolean) => void;
|
||||
registerEvent: (eventRef: EventRef) => void;
|
||||
registerComposerNoteIndexInvalidation: (register: (eventRef: EventRef) => void) => void;
|
||||
registerPointerDown: (handler: (event: PointerEvent) => void) => void;
|
||||
registerActiveLeafChange: (handler: (leaf: WorkspaceLeaf | null) => void) => void;
|
||||
isOwnLeaf: (leaf: WorkspaceLeaf | null) => boolean;
|
||||
scrollMessagesToBottomOnFocus: () => void;
|
||||
applyCachedSharedAppServerState: () => void;
|
||||
render: () => void;
|
||||
scheduleDeferredAppServerWarmup: () => void;
|
||||
scheduleDeferredRestoredThreadHydration: () => void;
|
||||
closeToolbarPanelOnOutsidePointer: (event: PointerEvent) => void;
|
||||
invalidateConnectionWork: () => void;
|
||||
invalidateResumeWork: () => void;
|
||||
clearDeferredTasks: () => void;
|
||||
panelRoot: () => HTMLElement | null;
|
||||
disposeMessages: () => void;
|
||||
disposeComposer: () => void;
|
||||
disconnect: () => void;
|
||||
clearClient: () => void;
|
||||
refreshLiveState: () => void;
|
||||
deferRefreshLiveState: () => void;
|
||||
}
|
||||
|
||||
export class ChatViewOpenCloseController {
|
||||
constructor(private readonly host: ChatViewOpenCloseControllerHost) {}
|
||||
|
||||
open(): void {
|
||||
this.host.setOpened(true);
|
||||
this.host.setClosing(false);
|
||||
this.host.registerComposerNoteIndexInvalidation((eventRef) => {
|
||||
this.host.registerEvent(eventRef);
|
||||
});
|
||||
this.host.registerPointerDown((event) => {
|
||||
this.host.closeToolbarPanelOnOutsidePointer(event);
|
||||
});
|
||||
this.host.registerActiveLeafChange((leaf) => {
|
||||
if (this.host.isOwnLeaf(leaf)) this.host.scrollMessagesToBottomOnFocus();
|
||||
});
|
||||
this.host.applyCachedSharedAppServerState();
|
||||
this.host.render();
|
||||
this.host.scheduleDeferredAppServerWarmup();
|
||||
this.host.scheduleDeferredRestoredThreadHydration();
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.host.setOpened(false);
|
||||
this.host.setClosing(true);
|
||||
this.host.invalidateConnectionWork();
|
||||
this.host.invalidateResumeWork();
|
||||
this.host.clearDeferredTasks();
|
||||
const panelRoot = this.host.panelRoot();
|
||||
this.host.disposeMessages();
|
||||
this.host.disposeComposer();
|
||||
unmountChatPanelShell(panelRoot);
|
||||
this.host.disconnect();
|
||||
this.host.clearClient();
|
||||
this.host.refreshLiveState();
|
||||
this.host.deferRefreshLiveState();
|
||||
}
|
||||
}
|
||||
40
src/features/chat/requests/server-request-actions.ts
Normal file
40
src/features/chat/requests/server-request-actions.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { AppServerClient } from "../../../app-server/client";
|
||||
|
||||
type RespondRequestId = Parameters<AppServerClient["respondToServerRequest"]>[0];
|
||||
type RejectRequestId = Parameters<AppServerClient["rejectServerRequest"]>[0];
|
||||
|
||||
export interface ServerRequestActionsHost {
|
||||
currentClient: () => AppServerClient | null;
|
||||
}
|
||||
|
||||
export interface ServerRequestActions {
|
||||
respond: (requestId: RespondRequestId, result: unknown) => boolean;
|
||||
reject: (requestId: RejectRequestId, code: number, message: string) => boolean;
|
||||
}
|
||||
|
||||
export function createServerRequestActions(host: ServerRequestActionsHost): ServerRequestActions {
|
||||
return {
|
||||
respond: (requestId, result) => respondToServerRequest(host, requestId, result),
|
||||
reject: (requestId, code, message) => rejectServerRequest(host, requestId, code, message),
|
||||
};
|
||||
}
|
||||
|
||||
function respondToServerRequest(host: ServerRequestActionsHost, requestId: RespondRequestId, result: unknown): boolean {
|
||||
try {
|
||||
const client = host.currentClient();
|
||||
client?.respondToServerRequest(requestId, result);
|
||||
return Boolean(client);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function rejectServerRequest(host: ServerRequestActionsHost, requestId: RejectRequestId, code: number, message: string): boolean {
|
||||
try {
|
||||
const client = host.currentClient();
|
||||
client?.rejectServerRequest(requestId, code, message);
|
||||
return Boolean(client);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import type { AppServerClient } from "../../../app-server/client";
|
||||
|
||||
type RespondRequestId = Parameters<AppServerClient["respondToServerRequest"]>[0];
|
||||
type RejectRequestId = Parameters<AppServerClient["rejectServerRequest"]>[0];
|
||||
|
||||
export interface ServerRequestResponderHost {
|
||||
currentClient: () => AppServerClient | null;
|
||||
}
|
||||
|
||||
export class ServerRequestResponder {
|
||||
constructor(private readonly host: ServerRequestResponderHost) {}
|
||||
|
||||
respond(requestId: RespondRequestId, result: unknown): boolean {
|
||||
try {
|
||||
const client = this.host.currentClient();
|
||||
client?.respondToServerRequest(requestId, result);
|
||||
return Boolean(client);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
reject(requestId: RejectRequestId, code: number, message: string): boolean {
|
||||
try {
|
||||
const client = this.host.currentClient();
|
||||
client?.rejectServerRequest(requestId, code, message);
|
||||
return Boolean(client);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import { ChatAppServerDiagnosticsController } from "../app-server/diagnostics-co
|
|||
import { ChatAppServerMetadataController } from "../app-server/metadata-controller";
|
||||
import { ChatAppServerThreadController } from "../app-server/thread-controller";
|
||||
import { ChatConnectionController } from "./connection-controller";
|
||||
import type { ServerRequestResponder } from "../requests/server-request-responder";
|
||||
import type { ServerRequestActions } from "../requests/server-request-actions";
|
||||
import type { ChatThreadGoalController } from "../threads/thread-goal-controller";
|
||||
import type { ThreadRenameController } from "../threads/thread-rename-controller";
|
||||
import { ChatInboundController } from "../inbound/controller";
|
||||
|
|
@ -59,7 +59,7 @@ export function createChatInboundController(
|
|||
appServerMetadata: ChatAppServerMetadataController;
|
||||
appServerDiagnostics: ChatAppServerDiagnosticsController;
|
||||
threadRename: ThreadRenameController;
|
||||
serverRequestResponder: ServerRequestResponder;
|
||||
serverRequestResponder: ServerRequestActions;
|
||||
},
|
||||
): ChatInboundController {
|
||||
const { plugin, thread, render } = context;
|
||||
|
|
|
|||
48
src/features/chat/session/reconnect-actions.ts
Normal file
48
src/features/chat/session/reconnect-actions.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { clearLocalTurnAction, closePanelsAction } from "../chat-state-actions";
|
||||
import { activeThreadId } from "../chat-state-selectors";
|
||||
import type { ChatStateStore } from "../chat-state";
|
||||
|
||||
export interface ChatReconnectActionsHost {
|
||||
stateStore: ChatStateStore;
|
||||
invalidateConnectionWork: () => void;
|
||||
invalidateResumeWork: () => void;
|
||||
clearDeferredDiagnostics: () => void;
|
||||
reconnect: () => void;
|
||||
clearClient: () => void;
|
||||
setStatus: (status: string) => void;
|
||||
render: () => void;
|
||||
ensureConnected: () => Promise<void>;
|
||||
resumeThread: (threadId: string) => Promise<void>;
|
||||
addSystemMessage: (text: string) => void;
|
||||
}
|
||||
|
||||
export interface ChatReconnectActions {
|
||||
reconnectPanel: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function createChatReconnectActions(host: ChatReconnectActionsHost): ChatReconnectActions {
|
||||
return {
|
||||
reconnectPanel: () => reconnectPanel(host),
|
||||
};
|
||||
}
|
||||
|
||||
async function reconnectPanel(host: ChatReconnectActionsHost): Promise<void> {
|
||||
const threadId = activeThreadId(host.stateStore.getState());
|
||||
host.stateStore.dispatch(closePanelsAction());
|
||||
host.invalidateConnectionWork();
|
||||
host.invalidateResumeWork();
|
||||
host.clearDeferredDiagnostics();
|
||||
host.reconnect();
|
||||
host.clearClient();
|
||||
host.stateStore.dispatch(clearLocalTurnAction());
|
||||
host.setStatus("Reconnecting...");
|
||||
host.render();
|
||||
|
||||
await host.ensureConnected();
|
||||
if (!threadId) return;
|
||||
try {
|
||||
await host.resumeThread(threadId);
|
||||
} catch (error) {
|
||||
host.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import { clearLocalTurnAction, closePanelsAction } from "../chat-state-actions";
|
||||
import { activeThreadId } from "../chat-state-selectors";
|
||||
import type { ChatStateStore } from "../chat-state";
|
||||
|
||||
export interface ChatReconnectControllerHost {
|
||||
stateStore: ChatStateStore;
|
||||
invalidateConnectionWork: () => void;
|
||||
invalidateResumeWork: () => void;
|
||||
clearDeferredDiagnostics: () => void;
|
||||
reconnect: () => void;
|
||||
clearClient: () => void;
|
||||
setStatus: (status: string) => void;
|
||||
render: () => void;
|
||||
ensureConnected: () => Promise<void>;
|
||||
resumeThread: (threadId: string) => Promise<void>;
|
||||
addSystemMessage: (text: string) => void;
|
||||
}
|
||||
|
||||
export class ChatReconnectController {
|
||||
constructor(private readonly host: ChatReconnectControllerHost) {}
|
||||
|
||||
async reconnectPanel(): Promise<void> {
|
||||
const threadId = activeThreadId(this.host.stateStore.getState());
|
||||
this.host.stateStore.dispatch(closePanelsAction());
|
||||
this.host.invalidateConnectionWork();
|
||||
this.host.invalidateResumeWork();
|
||||
this.host.clearDeferredDiagnostics();
|
||||
this.host.reconnect();
|
||||
this.host.clearClient();
|
||||
this.host.stateStore.dispatch(clearLocalTurnAction());
|
||||
this.host.setStatus("Reconnecting...");
|
||||
this.host.render();
|
||||
|
||||
await this.host.ensureConnected();
|
||||
if (!threadId) return;
|
||||
try {
|
||||
await this.host.resumeThread(threadId);
|
||||
} catch (error) {
|
||||
this.host.addSystemMessage(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,12 @@ import { ChatRuntimeSettingsController } from "../runtime/runtime-settings-contr
|
|||
import { ChatThreadActionController } from "./thread-actions-controller";
|
||||
import { ChatThreadGoalController } from "./thread-goal-controller";
|
||||
import { ThreadHistoryController } from "./thread-history-controller";
|
||||
import { ThreadIdentityController } from "./thread-identity-controller";
|
||||
import { createThreadIdentityActions } from "./thread-identity-actions";
|
||||
import { ThreadRenameController } from "./thread-rename-controller";
|
||||
import { ThreadResumeController } from "./thread-resume-controller";
|
||||
import { createThreadSelectionActions } from "./thread-selection-controller";
|
||||
import { RestoredThreadController } from "./restored-thread-controller";
|
||||
import { ChatReconnectController } from "../session/reconnect-controller";
|
||||
import { createChatReconnectActions } from "../session/reconnect-actions";
|
||||
import { createChatViewStateActions } from "../panel/view-state-controller";
|
||||
import { ToolbarPanelController } from "../panel/toolbar-controller";
|
||||
import type { ChatPanelContext } from "../panel/context";
|
||||
|
|
@ -71,7 +71,7 @@ export function createThreadControllerGroup(
|
|||
resumeThread: thread.resumeThread,
|
||||
addSystemMessage: status.addSystemMessage,
|
||||
});
|
||||
const reconnectActions = new ChatReconnectController({
|
||||
const reconnectActions = createChatReconnectActions({
|
||||
stateStore,
|
||||
invalidateConnectionWork: lifecycle.invalidateConnectionWork,
|
||||
invalidateResumeWork: lifecycle.invalidateResumeWork,
|
||||
|
|
@ -145,7 +145,7 @@ export function createThreadControllerGroup(
|
|||
return response?.dataBase64 ?? "";
|
||||
}),
|
||||
});
|
||||
const threadIdentity = new ThreadIdentityController({
|
||||
const threadIdentity = createThreadIdentityActions({
|
||||
stateStore,
|
||||
restoredThread,
|
||||
invalidateResumeWork: lifecycle.invalidateResumeWork,
|
||||
|
|
|
|||
75
src/features/chat/threads/thread-identity-actions.ts
Normal file
75
src/features/chat/threads/thread-identity-actions.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import type { RestoredThreadController } from "./restored-thread-controller";
|
||||
import { applyThreadListAction, clearActiveThreadAction } from "../chat-state-actions";
|
||||
import { activeThreadId, listedThreads } from "../chat-state-selectors";
|
||||
import type { ChatStateStore } from "../chat-state";
|
||||
|
||||
export interface ThreadIdentityActionsHost {
|
||||
stateStore: ChatStateStore;
|
||||
restoredThread: RestoredThreadController;
|
||||
invalidateResumeWork: () => void;
|
||||
clearDeferredRestoredThreadHydration: () => void;
|
||||
resetThreadTurnPresence: (hadTurns: boolean) => void;
|
||||
notifyActiveThreadIdentityChanged: () => void;
|
||||
refreshTabHeader: () => void;
|
||||
refreshLiveState: () => void;
|
||||
render: () => void;
|
||||
}
|
||||
|
||||
export interface ThreadIdentityActions {
|
||||
clearActiveThreadContext: () => void;
|
||||
notifyThreadArchived: (threadId: string) => void;
|
||||
notifyThreadRenamed: (threadId: string, name: string | null) => void;
|
||||
}
|
||||
|
||||
export function createThreadIdentityActions(host: ThreadIdentityActionsHost): ThreadIdentityActions {
|
||||
return {
|
||||
clearActiveThreadContext: () => {
|
||||
clearActiveThreadContext(host);
|
||||
},
|
||||
notifyThreadArchived: (threadId) => {
|
||||
notifyThreadArchived(host, threadId);
|
||||
},
|
||||
notifyThreadRenamed: (threadId, name) => {
|
||||
notifyThreadRenamed(host, threadId, name);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function clearActiveThreadContext(host: ThreadIdentityActionsHost): void {
|
||||
host.invalidateResumeWork();
|
||||
host.restoredThread.clear();
|
||||
host.clearDeferredRestoredThreadHydration();
|
||||
host.stateStore.dispatch(clearActiveThreadAction());
|
||||
host.resetThreadTurnPresence(false);
|
||||
host.notifyActiveThreadIdentityChanged();
|
||||
host.refreshLiveState();
|
||||
}
|
||||
|
||||
function notifyThreadArchived(host: ThreadIdentityActionsHost, threadId: string): void {
|
||||
if (activeThreadId(host.stateStore.getState()) !== threadId) return;
|
||||
clearActiveThreadContext(host);
|
||||
host.render();
|
||||
}
|
||||
|
||||
function notifyThreadRenamed(host: ThreadIdentityActionsHost, threadId: string, name: string | null): void {
|
||||
let changed = false;
|
||||
const renamedThreads = listedThreads(host.stateStore.getState()).map((thread) => {
|
||||
if (thread.id !== threadId) return thread;
|
||||
changed = true;
|
||||
return { ...thread, name };
|
||||
});
|
||||
host.stateStore.dispatch(applyThreadListAction(renamedThreads));
|
||||
const restoredThread = host.restoredThread.placeholder();
|
||||
if (restoredThread?.threadId === threadId && (restoredThread.title !== name || restoredThread.explicitName !== name)) {
|
||||
host.restoredThread.rename(threadId, name);
|
||||
changed = true;
|
||||
}
|
||||
const activeThreadChanged = activeThreadId(host.stateStore.getState()) === threadId || host.restoredThread.isPending(threadId);
|
||||
if (!changed && !activeThreadChanged) return;
|
||||
if (activeThreadChanged) {
|
||||
host.notifyActiveThreadIdentityChanged();
|
||||
} else {
|
||||
host.refreshTabHeader();
|
||||
}
|
||||
host.render();
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import type { RestoredThreadController } from "./restored-thread-controller";
|
||||
import { applyThreadListAction, clearActiveThreadAction } from "../chat-state-actions";
|
||||
import { activeThreadId, listedThreads } from "../chat-state-selectors";
|
||||
import type { ChatStateStore } from "../chat-state";
|
||||
|
||||
export interface ThreadIdentityControllerHost {
|
||||
stateStore: ChatStateStore;
|
||||
restoredThread: RestoredThreadController;
|
||||
invalidateResumeWork: () => void;
|
||||
clearDeferredRestoredThreadHydration: () => void;
|
||||
resetThreadTurnPresence: (hadTurns: boolean) => void;
|
||||
notifyActiveThreadIdentityChanged: () => void;
|
||||
refreshTabHeader: () => void;
|
||||
refreshLiveState: () => void;
|
||||
render: () => void;
|
||||
}
|
||||
|
||||
export class ThreadIdentityController {
|
||||
constructor(private readonly host: ThreadIdentityControllerHost) {}
|
||||
|
||||
clearActiveThreadContext(): void {
|
||||
this.host.invalidateResumeWork();
|
||||
this.host.restoredThread.clear();
|
||||
this.host.clearDeferredRestoredThreadHydration();
|
||||
this.host.stateStore.dispatch(clearActiveThreadAction());
|
||||
this.host.resetThreadTurnPresence(false);
|
||||
this.host.notifyActiveThreadIdentityChanged();
|
||||
this.host.refreshLiveState();
|
||||
}
|
||||
|
||||
notifyThreadArchived(threadId: string): void {
|
||||
if (activeThreadId(this.host.stateStore.getState()) !== threadId) return;
|
||||
this.clearActiveThreadContext();
|
||||
this.host.render();
|
||||
}
|
||||
|
||||
notifyThreadRenamed(threadId: string, name: string | null): void {
|
||||
let changed = false;
|
||||
const renamedThreads = listedThreads(this.host.stateStore.getState()).map((thread) => {
|
||||
if (thread.id !== threadId) return thread;
|
||||
changed = true;
|
||||
return { ...thread, name };
|
||||
});
|
||||
this.host.stateStore.dispatch(applyThreadListAction(renamedThreads));
|
||||
const restoredThread = this.host.restoredThread.placeholder();
|
||||
if (restoredThread?.threadId === threadId && (restoredThread.title !== name || restoredThread.explicitName !== name)) {
|
||||
this.host.restoredThread.rename(threadId, name);
|
||||
changed = true;
|
||||
}
|
||||
const activeThreadChanged =
|
||||
activeThreadId(this.host.stateStore.getState()) === threadId || this.host.restoredThread.isPending(threadId);
|
||||
if (!changed && !activeThreadChanged) return;
|
||||
if (activeThreadChanged) {
|
||||
this.host.notifyActiveThreadIdentityChanged();
|
||||
} else {
|
||||
this.host.refreshTabHeader();
|
||||
}
|
||||
this.host.render();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import type { ChatAppServerThreadController } from "../app-server/thread-controller";
|
||||
import { ChatComposerController } from "../composer/controller";
|
||||
import { activeTurnId } from "../chat-state";
|
||||
import type { ChatReconnectController } from "../session/reconnect-controller";
|
||||
import type { ChatReconnectActions } from "../session/reconnect-actions";
|
||||
import { PendingRequestController } from "../requests/pending-request-controller";
|
||||
import type { ChatRuntimeSettingsController } from "../runtime/runtime-settings-controller";
|
||||
import { ComposerSubmissionController } from "./composer-submission-controller";
|
||||
import { PlanImplementationController } from "./plan-implementation-controller";
|
||||
import { createPlanImplementationActions } from "./plan-implementation-actions";
|
||||
import { SlashCommandController } from "./slash-command-controller";
|
||||
import { TurnSubmissionController } from "./turn-submission-controller";
|
||||
import type { ChatThreadActionController } from "../threads/thread-actions-controller";
|
||||
|
|
@ -25,7 +25,7 @@ export function createTurnControllerGroup(
|
|||
runtimeSettings: ChatRuntimeSettingsController;
|
||||
threadActions: ChatThreadActionController;
|
||||
threadRename: ThreadRenameController;
|
||||
reconnectActions: ChatReconnectController;
|
||||
reconnectActions: ChatReconnectActions;
|
||||
goals: ChatThreadGoalController;
|
||||
history: ThreadHistoryController;
|
||||
},
|
||||
|
|
@ -139,7 +139,7 @@ export function createTurnControllerGroup(
|
|||
effortStatusLines: runtime.effortStatusLines,
|
||||
},
|
||||
});
|
||||
const planImplementation = new PlanImplementationController({
|
||||
const planImplementation = createPlanImplementationActions({
|
||||
stateStore,
|
||||
connection: {
|
||||
currentClient,
|
||||
|
|
|
|||
44
src/features/chat/turns/plan-implementation-actions.ts
Normal file
44
src/features/chat/turns/plan-implementation-actions.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import type { AppServerClient } from "../../../app-server/client";
|
||||
import { closePanelsAction, setRequestedCollaborationModeDefaultAction } from "../chat-state-actions";
|
||||
import { activeThreadId, canImplementPlan } from "../chat-state-selectors";
|
||||
import type { ChatStateStore } from "../chat-state";
|
||||
import type { DisplayItem } from "../display/types";
|
||||
|
||||
const IMPLEMENT_PLAN_PROMPT = "Please implement this plan.";
|
||||
|
||||
interface PlanImplementationConnectionPort {
|
||||
currentClient(): AppServerClient | null;
|
||||
ensureConnected(): Promise<void>;
|
||||
}
|
||||
|
||||
interface PlanImplementationSubmissionPort {
|
||||
sendTurnText(text: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface PlanImplementationActionsHost {
|
||||
stateStore: ChatStateStore;
|
||||
connection: PlanImplementationConnectionPort;
|
||||
submission: PlanImplementationSubmissionPort;
|
||||
}
|
||||
|
||||
export interface PlanImplementationActions {
|
||||
canImplement: (item: DisplayItem) => boolean;
|
||||
implement: (item: DisplayItem) => Promise<void>;
|
||||
}
|
||||
|
||||
export function createPlanImplementationActions(host: PlanImplementationActionsHost): PlanImplementationActions {
|
||||
return {
|
||||
canImplement: (item) => canImplementPlan(host.stateStore.getState(), item),
|
||||
implement: (item) => implementPlan(host, item),
|
||||
};
|
||||
}
|
||||
|
||||
async function implementPlan(host: PlanImplementationActionsHost, item: DisplayItem): Promise<void> {
|
||||
if (!canImplementPlan(host.stateStore.getState(), item)) return;
|
||||
await host.connection.ensureConnected();
|
||||
if (!host.connection.currentClient() || !activeThreadId(host.stateStore.getState())) return;
|
||||
|
||||
host.stateStore.dispatch(setRequestedCollaborationModeDefaultAction());
|
||||
host.stateStore.dispatch(closePanelsAction());
|
||||
await host.submission.sendTurnText(IMPLEMENT_PLAN_PROMPT);
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import type { AppServerClient } from "../../../app-server/client";
|
||||
import { closePanelsAction, setRequestedCollaborationModeDefaultAction } from "../chat-state-actions";
|
||||
import { activeThreadId, canImplementPlan } from "../chat-state-selectors";
|
||||
import type { ChatStateStore } from "../chat-state";
|
||||
import type { DisplayItem } from "../display/types";
|
||||
|
||||
const IMPLEMENT_PLAN_PROMPT = "Please implement this plan.";
|
||||
|
||||
interface PlanImplementationConnectionPort {
|
||||
currentClient(): AppServerClient | null;
|
||||
ensureConnected(): Promise<void>;
|
||||
}
|
||||
|
||||
interface PlanImplementationSubmissionPort {
|
||||
sendTurnText(text: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface PlanImplementationControllerHost {
|
||||
stateStore: ChatStateStore;
|
||||
connection: PlanImplementationConnectionPort;
|
||||
submission: PlanImplementationSubmissionPort;
|
||||
}
|
||||
|
||||
export class PlanImplementationController {
|
||||
constructor(private readonly host: PlanImplementationControllerHost) {}
|
||||
|
||||
canImplement(item: DisplayItem): boolean {
|
||||
return canImplementPlan(this.host.stateStore.getState(), item);
|
||||
}
|
||||
|
||||
async implement(item: DisplayItem): Promise<void> {
|
||||
if (!this.canImplement(item)) return;
|
||||
await this.host.connection.ensureConnected();
|
||||
if (!this.host.connection.currentClient() || !activeThreadId(this.host.stateStore.getState())) return;
|
||||
|
||||
this.host.stateStore.dispatch(setRequestedCollaborationModeDefaultAction());
|
||||
this.host.stateStore.dispatch(closePanelsAction());
|
||||
await this.host.submission.sendTurnText(IMPLEMENT_PLAN_PROMPT);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,19 +3,16 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { EventRef } from "obsidian";
|
||||
|
||||
import {
|
||||
ChatViewOpenCloseController,
|
||||
type ChatViewOpenCloseControllerHost,
|
||||
} from "../../../../src/features/chat/panel/view-open-close-controller";
|
||||
import { createChatViewOpenCloseActions, type ChatViewOpenCloseActionsHost } from "../../../../src/features/chat/panel/open-close-actions";
|
||||
import { unmountChatPanelShell } from "../../../../src/features/chat/ui/shell";
|
||||
|
||||
vi.mock("../../../../src/features/chat/ui/shell", () => ({
|
||||
unmountChatPanelShell: vi.fn(),
|
||||
}));
|
||||
|
||||
function createHost(overrides: Partial<ChatViewOpenCloseControllerHost> = {}) {
|
||||
function createHost(overrides: Partial<ChatViewOpenCloseActionsHost> = {}) {
|
||||
const root = document.createElement("div");
|
||||
const host: ChatViewOpenCloseControllerHost = {
|
||||
const host: ChatViewOpenCloseActionsHost = {
|
||||
setOpened: vi.fn(),
|
||||
setClosing: vi.fn(),
|
||||
registerEvent: vi.fn(),
|
||||
|
|
@ -43,10 +40,10 @@ function createHost(overrides: Partial<ChatViewOpenCloseControllerHost> = {}) {
|
|||
deferRefreshLiveState: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
return { controller: new ChatViewOpenCloseController(host), host, root };
|
||||
return { controller: createChatViewOpenCloseActions(host), host, root };
|
||||
}
|
||||
|
||||
describe("ChatViewOpenCloseController", () => {
|
||||
describe("createChatViewOpenCloseActions", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(unmountChatPanelShell).mockClear();
|
||||
});
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { AppServerClient } from "../../../../src/app-server/client";
|
||||
import { ServerRequestResponder } from "../../../../src/features/chat/requests/server-request-responder";
|
||||
import { createServerRequestActions } from "../../../../src/features/chat/requests/server-request-actions";
|
||||
|
||||
describe("ServerRequestResponder", () => {
|
||||
describe("createServerRequestActions", () => {
|
||||
it("responds through the current app-server client", () => {
|
||||
const respondToServerRequest = vi.fn();
|
||||
const responder = new ServerRequestResponder({
|
||||
const responder = createServerRequestActions({
|
||||
currentClient: () => ({ respondToServerRequest }) as unknown as AppServerClient,
|
||||
});
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ describe("ServerRequestResponder", () => {
|
|||
|
||||
it("rejects through the current app-server client", () => {
|
||||
const rejectServerRequest = vi.fn();
|
||||
const responder = new ServerRequestResponder({
|
||||
const responder = createServerRequestActions({
|
||||
currentClient: () => ({ rejectServerRequest }) as unknown as AppServerClient,
|
||||
});
|
||||
|
||||
|
|
@ -27,9 +27,9 @@ describe("ServerRequestResponder", () => {
|
|||
});
|
||||
|
||||
it("reports failure when there is no client or the client throws", () => {
|
||||
expect(new ServerRequestResponder({ currentClient: () => null }).respond(7, null)).toBe(false);
|
||||
expect(createServerRequestActions({ currentClient: () => null }).respond(7, null)).toBe(false);
|
||||
expect(
|
||||
new ServerRequestResponder({
|
||||
createServerRequestActions({
|
||||
currentClient: () =>
|
||||
({
|
||||
rejectServerRequest: () => {
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createChatState, createChatStateStore } from "../../../../src/features/chat/chat-state";
|
||||
import { ChatReconnectController, type ChatReconnectControllerHost } from "../../../../src/features/chat/session/reconnect-controller";
|
||||
import { createChatReconnectActions, type ChatReconnectActionsHost } from "../../../../src/features/chat/session/reconnect-actions";
|
||||
|
||||
function createHost(overrides: Partial<ChatReconnectControllerHost> = {}) {
|
||||
function createHost(overrides: Partial<ChatReconnectActionsHost> = {}) {
|
||||
const stateStore = createChatStateStore(createChatState());
|
||||
stateStore.dispatch({ type: "ui/panel-set", panel: "history" });
|
||||
stateStore.dispatch({
|
||||
|
|
@ -17,7 +17,7 @@ function createHost(overrides: Partial<ChatReconnectControllerHost> = {}) {
|
|||
approvalsReviewer: null,
|
||||
activePermissionProfile: null,
|
||||
});
|
||||
const host: ChatReconnectControllerHost = {
|
||||
const host: ChatReconnectActionsHost = {
|
||||
stateStore,
|
||||
invalidateConnectionWork: vi.fn(),
|
||||
invalidateResumeWork: vi.fn(),
|
||||
|
|
@ -34,10 +34,10 @@ function createHost(overrides: Partial<ChatReconnectControllerHost> = {}) {
|
|||
return { host, stateStore };
|
||||
}
|
||||
|
||||
describe("ChatReconnectController", () => {
|
||||
describe("createChatReconnectActions", () => {
|
||||
it("resets local connection state before reconnecting and resumes the active thread", async () => {
|
||||
const { host, stateStore } = createHost();
|
||||
const controller = new ChatReconnectController(host);
|
||||
const controller = createChatReconnectActions(host);
|
||||
|
||||
await controller.reconnectPanel();
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ describe("ChatReconnectController", () => {
|
|||
const { host } = createHost({
|
||||
resumeThread: vi.fn().mockRejectedValue(new Error("resume failed")),
|
||||
});
|
||||
const controller = new ChatReconnectController(host);
|
||||
const controller = createChatReconnectActions(host);
|
||||
|
||||
await controller.reconnectPanel();
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createChatState, createChatStateStore } from "../../../../src/features/chat/chat-state";
|
||||
import { ThreadIdentityController } from "../../../../src/features/chat/threads/thread-identity-controller";
|
||||
import { createThreadIdentityActions } from "../../../../src/features/chat/threads/thread-identity-actions";
|
||||
import type { RestoredThreadController } from "../../../../src/features/chat/threads/restored-thread-controller";
|
||||
import type { RestoredThreadPlaceholderState } from "../../../../src/features/chat/panel/lifecycle";
|
||||
import type { Thread } from "../../../../src/generated/app-server/v2/Thread";
|
||||
|
|
@ -52,10 +52,10 @@ function createController() {
|
|||
refreshLiveState: vi.fn(),
|
||||
render: vi.fn(),
|
||||
};
|
||||
return { controller: new ThreadIdentityController(host), host, restoredPlaceholder, restoredRename, stateStore };
|
||||
return { controller: createThreadIdentityActions(host), host, restoredPlaceholder, restoredRename, stateStore };
|
||||
}
|
||||
|
||||
describe("ThreadIdentityController", () => {
|
||||
describe("createThreadIdentityActions", () => {
|
||||
it("clears the active thread when it is archived", () => {
|
||||
const { controller, host, stateStore } = createController();
|
||||
stateStore.dispatch({
|
||||
|
|
@ -4,9 +4,9 @@ import type { AppServerClient } from "../../../../src/app-server/client";
|
|||
import { createChatState, createChatStateStore, type ChatStateStore } from "../../../../src/features/chat/chat-state";
|
||||
import { implementPlanCandidateFromState } from "../../../../src/features/chat/plan-implementation";
|
||||
import {
|
||||
PlanImplementationController,
|
||||
type PlanImplementationControllerHost,
|
||||
} from "../../../../src/features/chat/turns/plan-implementation-controller";
|
||||
createPlanImplementationActions,
|
||||
type PlanImplementationActionsHost,
|
||||
} from "../../../../src/features/chat/turns/plan-implementation-actions";
|
||||
import type { DisplayItem } from "../../../../src/features/chat/display/types";
|
||||
|
||||
const planItem = (id: string): DisplayItem => ({
|
||||
|
|
@ -38,7 +38,7 @@ function createController({ client = {} as AppServerClient } = {}) {
|
|||
const stateStore = createChatStateStore(createChatState());
|
||||
const ensureConnected = vi.fn().mockResolvedValue(undefined);
|
||||
const sendTurnText = vi.fn().mockResolvedValue(undefined);
|
||||
const host: PlanImplementationControllerHost = {
|
||||
const host: PlanImplementationActionsHost = {
|
||||
stateStore,
|
||||
connection: {
|
||||
currentClient: () => client,
|
||||
|
|
@ -48,10 +48,10 @@ function createController({ client = {} as AppServerClient } = {}) {
|
|||
sendTurnText,
|
||||
},
|
||||
};
|
||||
return { controller: new PlanImplementationController(host), ensureConnected, sendTurnText, stateStore };
|
||||
return { controller: createPlanImplementationActions(host), ensureConnected, sendTurnText, stateStore };
|
||||
}
|
||||
|
||||
describe("PlanImplementationController", () => {
|
||||
describe("createPlanImplementationActions", () => {
|
||||
it("finds the latest proposed plan only when the thread is implementable", () => {
|
||||
const stateStore = createChatStateStore(createChatState());
|
||||
const first = planItem("first");
|
||||
Loading…
Reference in a new issue