Inline chat panel effects

This commit is contained in:
murashit 2026-06-08 08:19:05 +09:00
parent 3ece50abd3
commit 5265ebd3a6
6 changed files with 308 additions and 404 deletions

View file

@ -6,9 +6,14 @@ import type { RuntimeSnapshot } from "../../../runtime/state";
import type { ChatState, ChatStateStore } from "../chat-state";
import type { CodexChatHost } from "../chat-host";
import type { ChatMessageScrollIntentController } from "../controllers/view/message-scroll-intent-controller";
import type { DisplayDetailSection } from "../display/types";
import type { ChatViewEffects } from "./effects";
import type { ChatConnectionWorkTracker, ChatResumeWorkTracker, ChatViewDeferredTasks } from "./lifecycle";
import type { DisplayDetailSection, DisplayItem } from "../display/types";
import type {
ChatConnectionWorkTracker,
ChatResumeWorkTracker,
ChatViewDeferredTasks,
ChatViewRenderScheduleOptions,
RestoredThreadState,
} from "./lifecycle";
import type { ComposerMetaViewModel } from "./model";
export interface ChatPanelContext {
@ -20,7 +25,10 @@ export interface ChatPanelContext {
render: ChatPanelRenderContext;
runtime: ChatPanelRuntimeContext;
thread: ChatPanelThreadContext;
effects: ChatViewEffects;
liveState: ChatPanelLiveStateContext;
scroll: ChatPanelScrollContext;
status: ChatPanelStatusContext;
composer: ChatPanelComposerContext;
}
interface ChatPanelObsidianContext {
@ -37,11 +45,14 @@ interface ChatPanelObsidianContext {
interface ChatPanelStateContext {
stateStore: ChatStateStore;
getState: () => ChatState;
systemItem: (text: string) => DisplayItem;
}
interface ChatPanelClientContext {
getClient: () => AppServerClient | null;
setClient: (client: AppServerClient | null) => void;
clear: () => void;
ensureConnected: () => Promise<void>;
}
interface ChatPanelLifecycleContext {
@ -53,6 +64,13 @@ interface ChatPanelLifecycleContext {
setOpened: (opened: boolean) => void;
getClosing: () => boolean;
setClosing: (closing: boolean) => void;
invalidateConnectionWork: () => void;
invalidateResumeWork: () => void;
scheduleDeferredDiagnostics: () => void;
clearDeferredDiagnostics: () => void;
scheduleDeferredRestoredThreadHydration: () => void;
clearDeferredRestoredThreadHydration: () => void;
scheduleDeferredAppServerWarmup: () => void;
}
interface ChatPanelRenderContext {
@ -66,6 +84,9 @@ interface ChatPanelRenderContext {
composerPlaceholder: () => string;
composerMetaViewModel: () => ComposerMetaViewModel;
closeToolbarPanelOnOutsidePointer: (event: PointerEvent) => void;
now: () => void;
shellSlots: () => void;
schedule: (options?: ChatViewRenderScheduleOptions) => void;
}
interface ChatPanelRuntimeContext {
@ -87,4 +108,31 @@ interface ChatPanelThreadContext {
refreshSkills: (forceReload?: boolean) => Promise<void>;
publishAppServerMetadataSnapshot: () => void;
loadSharedThreadList: () => Promise<void>;
notifyIdentityChanged: () => void;
resetTurnPresence: (hadTurns: boolean) => void;
restorePlaceholder: (restoredThread: RestoredThreadState) => void;
clearRestoredLifecycle: () => void;
refreshTabHeader: () => void;
}
interface ChatPanelLiveStateContext {
refresh: () => void;
deferRefresh: () => void;
}
interface ChatPanelScrollContext {
forceBottom: () => void;
correctAfterLayoutChange: () => void;
preservePosition: () => void;
bottomOnFocus: () => void;
}
interface ChatPanelStatusContext {
set: (status: string) => void;
addSystemMessage: (text: string) => void;
addStructuredSystemMessage: (text: string, details: DisplayDetailSection[]) => void;
}
interface ChatPanelComposerContext {
setText: (text: string) => void;
}

View file

@ -132,17 +132,17 @@ export function createChatViewControllers(ports: ChatPanelContext): ChatViewCont
connection.setHandlers({
onNotification: (notification) => {
controller.handleNotification(notification);
context.effects.liveState.refresh();
context.effects.render.schedule();
context.liveState.refresh();
context.render.schedule();
},
onServerRequest: (request) => {
controller.handleServerRequest(request);
context.effects.liveState.refresh();
context.effects.render.now();
context.liveState.refresh();
context.render.now();
},
onLog: (message) => {
controller.handleAppServerLog(message);
context.effects.render.now();
context.render.now();
},
onExit: () => {
connectionController.handleExit();
@ -215,7 +215,7 @@ export function createChatViewControllers(ports: ChatPanelContext): ChatViewCont
}
function createControllerContext(ports: ChatPanelContext) {
const { obsidian, plugin, state, client, lifecycle, render, runtime, thread, effects } = ports;
const { obsidian, plugin, state, client, lifecycle, render, runtime, thread, liveState, scroll, status, composer } = ports;
const { app, owner, viewId } = obsidian;
const { stateStore } = state;
@ -229,7 +229,10 @@ function createControllerContext(ports: ChatPanelContext) {
render,
runtime,
thread,
effects,
liveState,
scroll,
status,
composer,
app,
owner,
viewId,
@ -284,8 +287,24 @@ function createSubmissionControllerGroup(
history: ThreadHistoryController;
},
) {
const { app, owner, plugin, state, stateStore, viewId, render, runtime, thread, effects, lifecycle, currentClient, submissionState } =
context;
const {
app,
owner,
plugin,
state,
stateStore,
viewId,
render,
runtime,
thread,
liveState,
scroll,
status,
lifecycle,
currentClient,
submissionState,
client,
} = context;
const { messageScrollIntent } = lifecycle;
const composerController = new ChatComposerController({
@ -302,18 +321,18 @@ function createSubmissionControllerGroup(
togglePlan: () => void refs.runtimeSettings.toggleCollaborationMode(),
toggleAutoReview: () => void refs.runtimeSettings.toggleAutoReview(),
toggleFast: () => void refs.runtimeSettings.toggleFastMode(),
renderIfDetached: effects.render.now,
onDraftChange: effects.liveState.refresh,
renderIfDetached: render.now,
onDraftChange: liveState.refresh,
onComposerResize: () => {
effects.scroll.correctAfterLayoutChange();
scroll.correctAfterLayoutChange();
},
});
const pendingRequests = new PendingRequestController({
state: createPendingRequestStatePort(stateStore),
controller: refs.controller,
composerHasFocus: () => composerController.hasFocus(),
refreshLiveState: effects.liveState.refresh,
render: effects.render.now,
refreshLiveState: liveState.refresh,
render: render.now,
});
const turnSubmission = new TurnSubmissionController({
@ -327,8 +346,8 @@ function createSubmissionControllerGroup(
},
thread: {
startThread: (preview) => refs.appServerThreads.startThread(preview),
notifyActiveThreadIdentityChanged: effects.thread.notifyIdentityChanged,
resetThreadTurnPresence: effects.thread.resetTurnPresence,
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
resetThreadTurnPresence: thread.resetTurnPresence,
},
runtime: {
applyPendingThreadSettings: () => refs.runtimeSettings.applyPendingThreadSettings(),
@ -340,13 +359,13 @@ function createSubmissionControllerGroup(
},
},
view: {
forceMessagesToBottom: effects.scroll.forceBottom,
render: effects.render.now,
scheduleRender: effects.render.schedule,
forceMessagesToBottom: scroll.forceBottom,
render: render.now,
scheduleRender: render.schedule,
},
status: {
setStatus: effects.status.set,
addSystemMessage: effects.status.addSystemMessage,
setStatus: status.set,
addSystemMessage: status.addSystemMessage,
},
});
const slashCommands = new SlashCommandController({
@ -381,9 +400,9 @@ function createSubmissionControllerGroup(
clear: (threadId) => refs.goals.clear(threadId),
},
status: {
addSystemMessage: effects.status.addSystemMessage,
addStructuredSystemMessage: effects.status.addStructuredSystemMessage,
setStatus: effects.status.set,
addSystemMessage: status.addSystemMessage,
addStructuredSystemMessage: status.addStructuredSystemMessage,
setStatus: status.set,
statusSummaryLines: runtime.statusSummaryLines,
connectionDiagnosticDetails: runtime.connectionDiagnosticDetails,
mcpStatusLines: runtime.mcpStatusLines,
@ -395,7 +414,7 @@ function createSubmissionControllerGroup(
state: submissionState,
connection: {
currentClient,
ensureConnected: effects.client.ensureConnected,
ensureConnected: client.ensureConnected,
},
submission: {
sendTurnText: (text) => turnSubmission.sendTurnText(text),
@ -437,11 +456,11 @@ function createSubmissionControllerGroup(
turnSubmission,
connection: {
currentClient,
ensureConnected: effects.client.ensureConnected,
ensureConnected: client.ensureConnected,
},
status: {
setStatus: effects.status.set,
addSystemMessage: effects.status.addSystemMessage,
setStatus: status.set,
addSystemMessage: status.addSystemMessage,
},
});
composerController.setActionHandlers({
@ -469,7 +488,7 @@ function createConnectionLifecycleControllerGroup(
appServerMetadata: ChatAppServerMetadataController;
},
) {
const { ports, obsidian, lifecycle, render, effects } = context;
const { ports, obsidian, lifecycle, render, liveState, scroll, client } = context;
const { deferredTasks } = lifecycle;
return {
@ -478,7 +497,7 @@ function createConnectionLifecycleControllerGroup(
opened: lifecycle.getOpened,
closing: lifecycle.getClosing,
connected: () => refs.connection.isConnected(),
ensureConnected: effects.client.ensureConnected,
ensureConnected: client.ensureConnected,
}),
openCloseController: new ChatViewOpenCloseController({
setOpened: lifecycle.setOpened,
@ -490,16 +509,16 @@ function createConnectionLifecycleControllerGroup(
registerPointerDown: obsidian.registerPointerDown,
registerActiveLeafChange: obsidian.registerActiveLeafChange,
isOwnLeaf: obsidian.isOwnLeaf,
scrollMessagesToBottomOnFocus: effects.scroll.bottomOnFocus,
scrollMessagesToBottomOnFocus: scroll.bottomOnFocus,
applyCachedSharedAppServerState: () => {
applyCachedSharedAppServerState(ports, refs.appServerThreads, refs.appServerMetadata);
},
render: effects.render.now,
scheduleDeferredAppServerWarmup: effects.lifecycle.scheduleDeferredAppServerWarmup,
scheduleDeferredRestoredThreadHydration: effects.lifecycle.scheduleDeferredRestoredThreadHydration,
render: render.now,
scheduleDeferredAppServerWarmup: lifecycle.scheduleDeferredAppServerWarmup,
scheduleDeferredRestoredThreadHydration: lifecycle.scheduleDeferredRestoredThreadHydration,
closeToolbarPanelOnOutsidePointer: render.closeToolbarPanelOnOutsidePointer,
invalidateConnectionWork: effects.lifecycle.invalidateConnectionWork,
invalidateResumeWork: effects.lifecycle.invalidateResumeWork,
invalidateConnectionWork: lifecycle.invalidateConnectionWork,
invalidateResumeWork: lifecycle.invalidateResumeWork,
clearDeferredTasks: () => {
deferredTasks.clearAll();
},
@ -513,9 +532,9 @@ function createConnectionLifecycleControllerGroup(
disconnect: () => {
refs.connection.disconnect();
},
clearClient: effects.client.clear,
refreshLiveState: effects.liveState.refresh,
deferRefreshLiveState: effects.liveState.deferRefresh,
clearClient: client.clear,
refreshLiveState: liveState.refresh,
deferRefreshLiveState: liveState.deferRefresh,
}),
};
}
@ -527,7 +546,7 @@ function createAppServerControllerGroup(
goals: ChatThreadGoalController;
},
) {
const { plugin, runtime, effects, stateStore } = context;
const { plugin, runtime, scroll, stateStore } = context;
const appServerBaseHost = {
stateStore,
vaultPath: plugin.vaultPath,
@ -549,7 +568,7 @@ function createAppServerControllerGroup(
const appServerThreads = new ChatAppServerThreadController({
...appServerBaseHost,
runtimeSnapshot: runtime.runtimeSnapshot,
forceMessagesToBottom: effects.scroll.forceBottom,
forceMessagesToBottom: scroll.forceBottom,
publishThreadList: (threads) => {
plugin.applyThreadListSnapshot(threads);
},
@ -570,7 +589,7 @@ function createInboundController(
serverRequestResponder: ServerRequestResponder;
},
) {
const { plugin, thread, effects, stateStore } = context;
const { plugin, thread, render, stateStore } = context;
return new ChatInboundController(stateStore, {
refreshThreads: () => {
@ -588,7 +607,7 @@ function createInboundController(
notifyThreadRenamed: plugin.notifyThreadRenamed.bind(plugin),
recordMcpStartupStatus: (name, status, message) => {
refs.appServerDiagnostics.recordMcpStartupStatus(name, status, message);
effects.render.schedule();
render.schedule();
},
respondToServerRequest: (requestId, result) => refs.serverRequestResponder.respond(requestId, result),
rejectServerRequest: (requestId, code, message) => refs.serverRequestResponder.reject(requestId, code, message),
@ -603,7 +622,7 @@ function createConnectionControllerGroup(
appServerDiagnostics: ChatAppServerDiagnosticsController;
},
) {
const { plugin, client, thread, effects, lifecycle, connectionState } = context;
const { plugin, client, thread, status, liveState, render, lifecycle, connectionState } = context;
const { connectionWork } = lifecycle;
return {
@ -619,18 +638,18 @@ function createConnectionControllerGroup(
refreshPublishedCapabilityDiagnostics: () => refs.appServerDiagnostics.refreshPublishedCapabilityDiagnostics(),
},
setClient: client.setClient,
invalidateResumeWork: effects.lifecycle.invalidateResumeWork,
invalidateResumeWork: lifecycle.invalidateResumeWork,
loadSharedThreadList: thread.loadSharedThreadList,
scheduleDeferredDiagnostics: effects.lifecycle.scheduleDeferredDiagnostics,
clearDeferredDiagnostics: effects.lifecycle.clearDeferredDiagnostics,
refreshTabHeader: effects.thread.refreshTabHeader,
resetThreadTurnPresence: effects.thread.resetTurnPresence,
setStatus: effects.status.set,
addSystemMessage: effects.status.addSystemMessage,
scheduleDeferredDiagnostics: lifecycle.scheduleDeferredDiagnostics,
clearDeferredDiagnostics: lifecycle.clearDeferredDiagnostics,
refreshTabHeader: thread.refreshTabHeader,
resetThreadTurnPresence: thread.resetTurnPresence,
setStatus: status.set,
addSystemMessage: status.addSystemMessage,
configuredCommand: () => plugin.settings.codexPath,
refreshLiveState: effects.liveState.refresh,
render: effects.render.now,
scheduleRender: effects.render.schedule,
refreshLiveState: liveState.refresh,
render: render.now,
scheduleRender: render.schedule,
notifyConnectionFailed: () => {
new Notice("Codex app-server connection failed.");
},
@ -644,37 +663,54 @@ function createThreadToolbarControllerGroup(
connection: ConnectionManager;
},
) {
const { obsidian, plugin, runtime, thread, effects, lifecycle, stateStore, currentClient, connectionState, panelState, threadState } =
context;
const {
obsidian,
plugin,
state,
runtime,
thread,
status,
liveState,
scroll,
render,
client,
composer,
lifecycle,
stateStore,
currentClient,
connectionState,
panelState,
threadState,
} = context;
const { deferredTasks, resumeWork } = lifecycle;
const history = new ThreadHistoryController({
stateStore,
currentClient,
render: effects.render.now,
addSystemMessage: effects.status.addSystemMessage,
forceMessagesToBottom: effects.scroll.forceBottom,
keepCurrentScrollPosition: effects.scroll.preservePosition,
setThreadTurnPresence: effects.thread.resetTurnPresence,
render: render.now,
addSystemMessage: status.addSystemMessage,
forceMessagesToBottom: scroll.forceBottom,
keepCurrentScrollPosition: scroll.preservePosition,
setThreadTurnPresence: thread.resetTurnPresence,
});
const threadActions = new ChatThreadActionController({
stateStore,
vaultPath: plugin.vaultPath,
settings: () => plugin.settings,
archiveAdapter: obsidian.archiveAdapter,
ensureConnected: effects.client.ensureConnected,
ensureConnected: client.ensureConnected,
currentClient,
history,
addSystemMessage: effects.status.addSystemMessage,
setStatus: effects.status.set,
setComposerText: effects.composer.setText,
addSystemMessage: status.addSystemMessage,
setStatus: status.set,
setComposerText: composer.setText,
openThreadInNewView: (threadId) => plugin.openThreadInNewView(threadId),
openThreadInCurrentPanel: thread.selectThread,
notifyThreadArchived: plugin.notifyThreadArchived.bind(plugin),
notifyThreadRenamed: (threadId, name) => {
plugin.notifyThreadRenamed(threadId, name);
},
notifyActiveThreadIdentityChanged: effects.thread.notifyIdentityChanged,
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
refreshThreads: thread.refreshThreads,
refreshSharedThreadListFromOpenSurface: () => {
plugin.refreshSharedThreadListFromOpenSurface();
@ -683,7 +719,7 @@ function createThreadToolbarControllerGroup(
const toolbarPanels = new ToolbarPanelController({
stateStore,
threadActions,
scheduleRender: effects.render.schedule,
scheduleRender: render.schedule,
});
const threadSelection = new ThreadSelectionController({
panelState,
@ -693,59 +729,59 @@ function createThreadToolbarControllerGroup(
},
focusThreadInOpenView: (threadId) => plugin.focusThreadInOpenView(threadId),
resumeThread: thread.resumeThread,
addSystemMessage: effects.status.addSystemMessage,
addSystemMessage: status.addSystemMessage,
});
const reconnectActions = new ChatReconnectController({
connectionState,
panelState,
threadState,
invalidateConnectionWork: effects.lifecycle.invalidateConnectionWork,
invalidateResumeWork: effects.lifecycle.invalidateResumeWork,
clearDeferredDiagnostics: effects.lifecycle.clearDeferredDiagnostics,
invalidateConnectionWork: lifecycle.invalidateConnectionWork,
invalidateResumeWork: lifecycle.invalidateResumeWork,
clearDeferredDiagnostics: lifecycle.clearDeferredDiagnostics,
reconnect: () => {
refs.connection.reconnect();
},
clearClient: effects.client.clear,
setStatus: effects.status.set,
render: effects.render.now,
ensureConnected: effects.client.ensureConnected,
clearClient: client.clear,
setStatus: status.set,
render: render.now,
ensureConnected: client.ensureConnected,
resumeThread: thread.resumeThread,
addSystemMessage: effects.status.addSystemMessage,
addSystemMessage: status.addSystemMessage,
});
const runtimeSettings = new ChatRuntimeSettingsController({
stateStore,
currentClient,
runtimeSnapshot: runtime.runtimeSnapshot,
collaborationModeLabel: runtime.collaborationModeLabel,
addSystemMessage: effects.status.addSystemMessage,
addSystemMessage: status.addSystemMessage,
});
const goals = new ChatThreadGoalController({
stateStore,
currentClient,
ensureConnected: effects.client.ensureConnected,
addSystemMessage: effects.status.addSystemMessage,
ensureConnected: client.ensureConnected,
addSystemMessage: status.addSystemMessage,
addGoalEvent: (item) => {
stateStore.dispatch({ type: "transcript/item-upserted", item });
},
render: effects.render.now,
refreshLiveState: effects.liveState.refresh,
render: render.now,
refreshLiveState: liveState.refresh,
});
const restoredThread = new RestoredThreadController({
deferredTasks,
opened: lifecycle.getOpened,
resumeThread: thread.resumeThread,
invalidateResumeWork: effects.lifecycle.invalidateResumeWork,
invalidateResumeWork: lifecycle.invalidateResumeWork,
state: threadState,
systemItem: effects.state.systemItem,
setStatus: effects.status.set,
refreshTabHeader: effects.thread.refreshTabHeader,
systemItem: state.systemItem,
setStatus: status.set,
refreshTabHeader: thread.refreshTabHeader,
});
const viewStateController = new ChatViewStateController({
invalidateResumeWork: effects.lifecycle.invalidateResumeWork,
clearRestoredThreadLifecycle: effects.thread.clearRestoredLifecycle,
clearDeferredRestoredThreadHydration: effects.lifecycle.clearDeferredRestoredThreadHydration,
scheduleDeferredAppServerWarmup: effects.lifecycle.scheduleDeferredAppServerWarmup,
restoreThreadPlaceholder: effects.thread.restorePlaceholder,
invalidateResumeWork: lifecycle.invalidateResumeWork,
clearRestoredThreadLifecycle: thread.clearRestoredLifecycle,
clearDeferredRestoredThreadHydration: lifecycle.clearDeferredRestoredThreadHydration,
scheduleDeferredAppServerWarmup: lifecycle.scheduleDeferredAppServerWarmup,
restoreThreadPlaceholder: thread.restorePlaceholder,
});
const threadResume = new ThreadResumeController({
state: threadState,
@ -754,16 +790,16 @@ function createThreadToolbarControllerGroup(
history,
restoredThread,
currentClient,
ensureConnected: effects.client.ensureConnected,
ensureConnected: client.ensureConnected,
closing: lifecycle.getClosing,
systemItem: effects.state.systemItem,
resetThreadTurnPresence: effects.thread.resetTurnPresence,
clearDeferredRestoredThreadHydration: effects.lifecycle.clearDeferredRestoredThreadHydration,
notifyActiveThreadIdentityChanged: effects.thread.notifyIdentityChanged,
addSystemMessage: effects.status.addSystemMessage,
forceMessagesToBottom: effects.scroll.forceBottom,
render: effects.render.now,
refreshLiveState: effects.liveState.refresh,
systemItem: state.systemItem,
resetThreadTurnPresence: thread.resetTurnPresence,
clearDeferredRestoredThreadHydration: lifecycle.clearDeferredRestoredThreadHydration,
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
addSystemMessage: status.addSystemMessage,
forceMessagesToBottom: scroll.forceBottom,
render: render.now,
refreshLiveState: liveState.refresh,
syncThreadGoal: (threadId) => goals.syncThreadGoal(threadId),
recoverTokenUsageFromRollout: (path) =>
recoverRolloutTokenUsage(path, async (filePath, options) => {
@ -774,23 +810,23 @@ function createThreadToolbarControllerGroup(
const threadIdentity = new ThreadIdentityController({
state: threadState,
restoredThread,
invalidateResumeWork: effects.lifecycle.invalidateResumeWork,
clearDeferredRestoredThreadHydration: effects.lifecycle.clearDeferredRestoredThreadHydration,
resetThreadTurnPresence: effects.thread.resetTurnPresence,
notifyActiveThreadIdentityChanged: effects.thread.notifyIdentityChanged,
refreshTabHeader: effects.thread.refreshTabHeader,
refreshLiveState: effects.liveState.refresh,
render: effects.render.now,
invalidateResumeWork: lifecycle.invalidateResumeWork,
clearDeferredRestoredThreadHydration: lifecycle.clearDeferredRestoredThreadHydration,
resetThreadTurnPresence: thread.resetTurnPresence,
notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged,
refreshTabHeader: thread.refreshTabHeader,
refreshLiveState: liveState.refresh,
render: render.now,
});
const threadRename = new ThreadRenameController({
stateStore,
vaultPath: plugin.vaultPath,
settings: () => plugin.settings,
ensureConnected: effects.client.ensureConnected,
ensureConnected: client.ensureConnected,
currentClient: () => refs.connection.currentClient(),
refreshThreads: thread.refreshThreads,
render: effects.render.shellSlots,
addSystemMessage: effects.status.addSystemMessage,
render: render.shellSlots,
addSystemMessage: status.addSystemMessage,
notifyThreadRenamed: plugin.notifyThreadRenamed.bind(plugin),
});

View file

@ -1,139 +0,0 @@
import type { ChatMessageScrollIntentController } from "../controllers/view/message-scroll-intent-controller";
import type { ChatAction } from "../chat-state";
import type { CodexChatHost } from "../chat-host";
import type { DisplayDetailSection, DisplayItem } from "../display/types";
import { createSystemItem } from "../display/system";
import type { ChatViewEffects } from "./effects";
import type { ChatViewRenderScheduleOptions, RestoredThreadState } from "./lifecycle";
export interface ChatViewEffectHandlersOptions {
plugin: CodexChatHost;
viewWindow: () => Window;
renderCommands: {
render: (options?: ChatViewRenderScheduleOptions) => void;
renderShellSlots: () => void;
forceMessagesToBottom: () => void;
correctMessagesAfterLayoutChange: () => void;
};
statusCommands: {
addSystemMessage: (text: string) => void;
addStructuredSystemMessage: (text: string, details: DisplayDetailSection[]) => void;
};
threadCommands: {
resetThreadTurnPresence: (hadTurns: boolean) => void;
restorePlaceholder: (restoredThread: RestoredThreadState) => void;
clearRestoredLifecycle: () => void;
};
connectionCommands: {
invalidate: () => void;
ensureConnected: () => Promise<void>;
};
composerCommands: {
setText: (text: string) => void;
};
messageScrollIntent: ChatMessageScrollIntentController;
scheduleRender: (options?: ChatViewRenderScheduleOptions) => void;
notifyActiveThreadIdentityChanged: () => void;
refreshTabHeader: () => void;
invalidateResumeWork: () => void;
scheduleDeferredDiagnostics: () => void;
clearDeferredDiagnostics: () => void;
scheduleDeferredRestoredThreadHydration: () => void;
clearDeferredRestoredThreadHydration: () => void;
scheduleDeferredAppServerWarmup: () => void;
dispatch: (action: ChatAction) => void;
clearClient: () => void;
}
export function createChatViewEffectHandlers(options: ChatViewEffectHandlersOptions): ChatViewEffects {
return {
render: {
now: () => {
options.renderCommands.render();
},
shellSlots: () => {
options.renderCommands.renderShellSlots();
},
schedule: (renderOptions) => {
options.scheduleRender(renderOptions);
},
},
liveState: {
refresh: () => {
options.plugin.refreshThreadsViewLiveState();
},
deferRefresh: () => {
options.viewWindow().setTimeout(() => {
options.plugin.refreshThreadsViewLiveState();
}, 0);
},
},
scroll: {
forceBottom: () => {
options.messageScrollIntent.forceBottom();
options.renderCommands.forceMessagesToBottom();
},
correctAfterLayoutChange: () => {
options.renderCommands.correctMessagesAfterLayoutChange();
},
preservePosition: () => {
options.messageScrollIntent.preservePosition();
},
bottomOnFocus: () => {
options.messageScrollIntent.scrollToBottomOnFocus();
},
},
status: {
set: (status) => {
options.dispatch({ type: "connection/status-set", status });
},
addSystemMessage: (text) => {
options.statusCommands.addSystemMessage(text);
},
addStructuredSystemMessage: (text, details) => {
options.statusCommands.addStructuredSystemMessage(text, details);
},
},
thread: {
notifyIdentityChanged: options.notifyActiveThreadIdentityChanged,
resetTurnPresence: (hadTurns) => {
options.threadCommands.resetThreadTurnPresence(hadTurns);
},
restorePlaceholder: (restoredThread) => {
options.threadCommands.restorePlaceholder(restoredThread);
},
clearRestoredLifecycle: () => {
options.threadCommands.clearRestoredLifecycle();
},
refreshTabHeader: options.refreshTabHeader,
},
lifecycle: {
invalidateConnectionWork: () => {
options.connectionCommands.invalidate();
},
invalidateResumeWork: options.invalidateResumeWork,
scheduleDeferredDiagnostics: options.scheduleDeferredDiagnostics,
clearDeferredDiagnostics: options.clearDeferredDiagnostics,
scheduleDeferredRestoredThreadHydration: options.scheduleDeferredRestoredThreadHydration,
clearDeferredRestoredThreadHydration: options.clearDeferredRestoredThreadHydration,
scheduleDeferredAppServerWarmup: options.scheduleDeferredAppServerWarmup,
},
state: {
dispatch: options.dispatch,
systemItem,
},
client: {
clear: options.clearClient,
ensureConnected: options.connectionCommands.ensureConnected,
},
composer: {
setText: (text) => {
options.composerCommands.setText(text);
},
},
};
}
function systemItem(text: string): DisplayItem {
return createSystemItem(`system-${String(Date.now())}-${Math.random().toString(36).slice(2)}`, text);
}

View file

@ -1,53 +0,0 @@
import type { ChatAction } from "../chat-state";
import type { DisplayDetailSection, DisplayItem } from "../display/types";
import type { ChatViewRenderScheduleOptions, RestoredThreadState } from "./lifecycle";
export interface ChatViewEffects {
render: {
now: () => void;
shellSlots: () => void;
schedule: (options?: ChatViewRenderScheduleOptions) => void;
};
liveState: {
refresh: () => void;
deferRefresh: () => void;
};
scroll: {
forceBottom: () => void;
correctAfterLayoutChange: () => void;
preservePosition: () => void;
bottomOnFocus: () => void;
};
status: {
set: (status: string) => void;
addSystemMessage: (text: string) => void;
addStructuredSystemMessage: (text: string, details: DisplayDetailSection[]) => void;
};
thread: {
notifyIdentityChanged: () => void;
resetTurnPresence: (hadTurns: boolean) => void;
restorePlaceholder: (restoredThread: RestoredThreadState) => void;
clearRestoredLifecycle: () => void;
refreshTabHeader: () => void;
};
lifecycle: {
invalidateConnectionWork: () => void;
invalidateResumeWork: () => void;
scheduleDeferredDiagnostics: () => void;
clearDeferredDiagnostics: () => void;
scheduleDeferredRestoredThreadHydration: () => void;
clearDeferredRestoredThreadHydration: () => void;
scheduleDeferredAppServerWarmup: () => void;
};
state: {
dispatch: (action: ChatAction) => void;
systemItem: (text: string) => DisplayItem;
};
client: {
clear: () => void;
ensureConnected: () => Promise<void>;
};
composer: {
setText: (text: string) => void;
};
}

View file

@ -3,7 +3,6 @@ import type { RuntimeSnapshot } from "../../../../runtime/state";
import type { CodexChatHost } from "../../chat-host";
import type { ChatAction, ChatState } from "../../chat-state";
import type { ToolbarThreadRow } from "../../toolbar-model";
import type { ChatViewEffects } from "../effects";
import type { ChatViewRenderScheduleOptions } from "../lifecycle";
import type { RestoredThreadTitleSnapshot } from "../model";
import type { ChatViewSlotRendererPorts } from "./types";
@ -64,7 +63,9 @@ export interface ChatViewSlotRendererPortsOptions {
goalCommands: ChatSlotGoalCommands;
appServerCommands: ChatSlotAppServerCommands;
renderCommands: ChatSlotRenderCommands;
effects: ChatViewEffects;
status: {
addSystemMessage: (text: string) => void;
};
dispatch: (action: ChatAction) => void;
startNewThread: () => Promise<void>;
setRequestedModelFromUi: (model: string | null) => Promise<void>;
@ -156,7 +157,7 @@ export function createChatViewSlotRendererPorts(options: ChatViewSlotRendererPor
async function compactConversation(options: ChatViewSlotRendererPortsOptions): Promise<void> {
const threadId = options.state().activeThread.id;
if (!threadId) {
options.effects.status.addSystemMessage("No active thread to compact.");
options.status.addSystemMessage("No active thread to compact.");
return;
}
await options.threadCommands.compactThread(threadId);
@ -180,7 +181,7 @@ async function saveGoalObjective(options: ChatViewSlotRendererPortsOptions, obje
const response = await options.appServerCommands.startThread(objective, { syncGoal: false });
threadId = response?.thread.id ?? null;
} catch (error) {
options.effects.status.addSystemMessage(error instanceof Error ? error.message : String(error));
options.status.addSystemMessage(error instanceof Error ? error.message : String(error));
return;
}
}

View file

@ -2,7 +2,7 @@ import { ItemView, type ViewStateResult, type WorkspaceLeaf } from "obsidian";
import type { AppServerClient } from "../../app-server/client";
import { VIEW_TYPE_CODEX_PANEL } from "../../constants";
import type { DisplayDetailSection } from "./display/types";
import type { DisplayDetailSection, DisplayItem } from "./display/types";
import type { ReasoningEffort } from "../../generated/app-server/ReasoningEffort";
import type { Model } from "../../generated/app-server/v2/Model";
import type { Thread } from "../../generated/app-server/v2/Thread";
@ -12,6 +12,7 @@ import { chatTurnBusy, createChatStateStore, type ChatState, type ChatAction } f
import type { OpenCodexPanelSnapshot } from "../../runtime/open-panel-snapshot";
import type { SharedAppServerMetadata } from "../../runtime/shared-app-server-state";
import type { CodexChatHost } from "./chat-host";
import { createSystemItem } from "./display/system";
import {
activeThreadTitle as buildActiveThreadTitle,
chatViewDisplayTitle,
@ -29,11 +30,9 @@ import {
type ChatViewRenderScheduleOptions,
} from "./panel/lifecycle";
import { ChatMessageScrollIntentController } from "./controllers/view/message-scroll-intent-controller";
import type { ChatViewEffects } from "./panel/effects";
import type { ChatPanelContext } from "./panel/context";
import { createChatViewControllers, type ChatViewControllers } from "./panel/controllers";
import { createPanelUiStatePort } from "./controllers/state-ports";
import { createChatViewEffectHandlers } from "./panel/effect-handlers";
import { createChatViewSlotRendererPorts } from "./panel/slots/ports";
import { ChatViewSlotRenderers } from "./panel/slots/renderers";
@ -45,7 +44,6 @@ export class CodexChatView extends ItemView {
private readonly chatState = createChatStateStore();
private readonly viewId = `codex-panel-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
private readonly deferredTasks: ChatViewDeferredTasks;
private readonly effects: ChatViewEffects;
private readonly messageScrollIntent: ChatMessageScrollIntentController;
private readonly slotRenderers: ChatViewSlotRenderers;
private readonly connectionWork = new ChatConnectionWorkTracker();
@ -68,7 +66,6 @@ export class CodexChatView extends ItemView {
this.controllers.render.controller.render();
},
});
this.effects = this.createEffectHandlers();
this.controllers = createChatViewControllers(this.createControllerPorts());
this.slotRenderers = new ChatViewSlotRenderers(this.createSlotRendererPorts());
}
@ -95,12 +92,17 @@ export class CodexChatView extends ItemView {
state: {
stateStore: this.chatState,
getState: () => this.state,
systemItem: (text) => this.systemItem(text),
},
client: {
getClient: () => this.client,
setClient: (client) => {
this.client = client;
},
clear: () => {
this.client = null;
},
ensureConnected: () => this.controllers.connection.controller.ensureConnected(),
},
lifecycle: {
deferredTasks: this.deferredTasks,
@ -115,6 +117,27 @@ export class CodexChatView extends ItemView {
setClosing: (closing) => {
this.closing = closing;
},
invalidateConnectionWork: () => {
this.controllers.connection.controller.invalidate();
},
invalidateResumeWork: () => {
this.invalidateResumeWork();
},
scheduleDeferredDiagnostics: () => {
this.scheduleDeferredDiagnostics();
},
clearDeferredDiagnostics: () => {
this.clearDeferredDiagnostics();
},
scheduleDeferredRestoredThreadHydration: () => {
this.scheduleDeferredRestoredThreadHydration();
},
clearDeferredRestoredThreadHydration: () => {
this.clearDeferredRestoredThreadHydration();
},
scheduleDeferredAppServerWarmup: () => {
this.scheduleDeferredAppServerWarmup();
},
},
render: {
panelRoot: () => this.panelRoot(),
@ -137,6 +160,15 @@ export class CodexChatView extends ItemView {
closeToolbarPanelOnOutsidePointer: (event) => {
this.closeToolbarPanelOnOutsidePointer(event);
},
now: () => {
this.controllers.render.controller.render();
},
shellSlots: () => {
this.controllers.render.controller.renderShellSlots();
},
schedule: (options) => {
this.scheduleRender(options);
},
},
runtime: {
runtimeSnapshot: () => this.runtimeSnapshot(),
@ -158,8 +190,65 @@ export class CodexChatView extends ItemView {
this.controllers.appServer.metadata.publishAppServerMetadataSnapshot();
},
loadSharedThreadList: () => this.loadSharedThreadList(),
notifyIdentityChanged: () => {
this.notifyActiveThreadIdentityChanged();
},
resetTurnPresence: (hadTurns) => {
this.controllers.thread.rename.resetThreadTurnPresence(hadTurns);
},
restorePlaceholder: (restoredThread) => {
this.controllers.thread.restored.restore(restoredThread);
},
clearRestoredLifecycle: () => {
this.controllers.thread.restored.clear();
},
refreshTabHeader: () => {
this.refreshTabHeader();
},
},
liveState: {
refresh: () => {
this.plugin.refreshThreadsViewLiveState();
},
deferRefresh: () => {
this.containerEl.win.setTimeout(() => {
this.plugin.refreshThreadsViewLiveState();
}, 0);
},
},
scroll: {
forceBottom: () => {
this.messageScrollIntent.forceBottom();
this.controllers.render.messages.forceMessagesToBottom();
},
correctAfterLayoutChange: () => {
this.controllers.render.messages.correctMessagesAfterLayoutChange();
},
preservePosition: () => {
this.messageScrollIntent.preservePosition();
},
bottomOnFocus: () => {
this.messageScrollIntent.scrollToBottomOnFocus();
},
},
status: {
set: (status) => {
this.dispatch({ type: "connection/status-set", status });
},
addSystemMessage: (text) => {
this.controllers.inbound.controller.addSystemMessage(text);
this.controllers.render.controller.render();
},
addStructuredSystemMessage: (text, details) => {
this.controllers.inbound.controller.addStructuredSystemMessage(text, details);
this.controllers.render.controller.render();
},
},
composer: {
setText: (text) => {
this.controllers.composer.controller.setDraft(text, { focus: true, renderIfDetached: true });
},
},
effects: this.effects,
};
}
@ -230,7 +319,12 @@ export class CodexChatView extends ItemView {
this.controllers.composer.controller.render(parent);
},
},
effects: this.effects,
status: {
addSystemMessage: (text) => {
this.controllers.inbound.controller.addSystemMessage(text);
this.controllers.render.controller.render();
},
},
dispatch: (action) => {
this.dispatch(action);
},
@ -240,93 +334,6 @@ export class CodexChatView extends ItemView {
});
}
private createEffectHandlers(): ChatViewEffects {
return createChatViewEffectHandlers({
plugin: this.plugin,
viewWindow: () => this.containerEl.win,
renderCommands: {
render: (options) => {
this.controllers.render.controller.render(options);
},
renderShellSlots: () => {
this.controllers.render.controller.renderShellSlots();
},
forceMessagesToBottom: () => {
this.controllers.render.messages.forceMessagesToBottom();
},
correctMessagesAfterLayoutChange: () => {
this.controllers.render.messages.correctMessagesAfterLayoutChange();
},
},
statusCommands: {
addSystemMessage: (text) => {
this.controllers.inbound.controller.addSystemMessage(text);
this.controllers.render.controller.render();
},
addStructuredSystemMessage: (text, details) => {
this.controllers.inbound.controller.addStructuredSystemMessage(text, details);
this.controllers.render.controller.render();
},
},
threadCommands: {
resetThreadTurnPresence: (hadTurns) => {
this.controllers.thread.rename.resetThreadTurnPresence(hadTurns);
},
restorePlaceholder: (restoredThread) => {
this.controllers.thread.restored.restore(restoredThread);
},
clearRestoredLifecycle: () => {
this.controllers.thread.restored.clear();
},
},
connectionCommands: {
invalidate: () => {
this.controllers.connection.controller.invalidate();
},
ensureConnected: () => this.controllers.connection.controller.ensureConnected(),
},
composerCommands: {
setText: (text) => {
this.controllers.composer.controller.setDraft(text, { focus: true, renderIfDetached: true });
},
},
messageScrollIntent: this.messageScrollIntent,
scheduleRender: (options) => {
this.scheduleRender(options);
},
notifyActiveThreadIdentityChanged: () => {
this.notifyActiveThreadIdentityChanged();
},
refreshTabHeader: () => {
this.refreshTabHeader();
},
invalidateResumeWork: () => {
this.invalidateResumeWork();
},
scheduleDeferredDiagnostics: () => {
this.scheduleDeferredDiagnostics();
},
clearDeferredDiagnostics: () => {
this.clearDeferredDiagnostics();
},
scheduleDeferredRestoredThreadHydration: () => {
this.scheduleDeferredRestoredThreadHydration();
},
clearDeferredRestoredThreadHydration: () => {
this.clearDeferredRestoredThreadHydration();
},
scheduleDeferredAppServerWarmup: () => {
this.scheduleDeferredAppServerWarmup();
},
dispatch: (action) => {
this.dispatch(action);
},
clearClient: () => {
this.client = null;
},
});
}
private get state(): ChatState {
return this.chatState.getState();
}
@ -339,6 +346,10 @@ export class CodexChatView extends ItemView {
this.chatState.dispatch(action);
}
private systemItem(text: string): DisplayItem {
return createSystemItem(`system-${String(Date.now())}-${Math.random().toString(36).slice(2)}`, text);
}
override getViewType(): string {
return VIEW_TYPE_CODEX_PANEL;
}
@ -451,7 +462,7 @@ export class CodexChatView extends ItemView {
this.controllers.thread.identity.clearActiveThreadContext();
this.chatState.dispatch({ type: "ui/panel-set", panel: null });
this.effects.status.set("New chat.");
this.dispatch({ type: "connection/status-set", status: "New chat." });
this.messageScrollIntent.forceBottom();
this.controllers.render.controller.render();
this.focusComposer();