mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
Manage window migrated event in agent view (#2590)
This commit is contained in:
parent
8ba39517d8
commit
4844e60e4f
3 changed files with 77 additions and 64 deletions
|
|
@ -3,14 +3,13 @@ import { attachChatViewLayoutObservers } from "@/components/chat-components/atta
|
|||
import { CHAT_AGENT_VIEWTYPE, COPILOT_AGENT_ICON_ID } from "@/constants";
|
||||
import { ChatViewEventTarget, EventTargetContext } from "@/context";
|
||||
import CopilotPlugin from "@/main";
|
||||
import { createPluginRoot } from "@/utils/react/createPluginRoot";
|
||||
import { mountPluginViewRoot, type PluginViewRootHandle } from "@/utils/react/mountPluginViewRoot";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { ItemView, WorkspaceLeaf } from "obsidian";
|
||||
import * as React from "react";
|
||||
import { Root } from "react-dom/client";
|
||||
|
||||
export default class CopilotAgentView extends ItemView {
|
||||
private root: Root | null = null;
|
||||
private viewRoot: PluginViewRootHandle | null = null;
|
||||
private handleSaveAsNote: (() => Promise<void>) | null = null;
|
||||
private disposeLayoutObservers: (() => void) | null = null;
|
||||
eventTarget: ChatViewEventTarget;
|
||||
|
|
@ -42,8 +41,7 @@ export default class CopilotAgentView extends ItemView {
|
|||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
this.root = createPluginRoot(this.containerEl.children[1], this.app);
|
||||
this.renderChat();
|
||||
this.viewRoot = mountPluginViewRoot(this.containerEl, this.app, () => this.renderTree());
|
||||
|
||||
const observers = attachChatViewLayoutObservers(this.containerEl);
|
||||
this.disposeLayoutObservers = observers.dispose;
|
||||
|
|
@ -55,10 +53,8 @@ export default class CopilotAgentView extends ItemView {
|
|||
);
|
||||
}
|
||||
|
||||
private renderChat(): void {
|
||||
if (!this.root) return;
|
||||
|
||||
this.root.render(
|
||||
private renderTree(): React.ReactNode {
|
||||
return (
|
||||
<EventTargetContext.Provider value={this.eventTarget}>
|
||||
<Tooltip.Provider delayDuration={0}>
|
||||
<AgentModeChat
|
||||
|
|
@ -80,16 +76,14 @@ export default class CopilotAgentView extends ItemView {
|
|||
}
|
||||
|
||||
updateView(): void {
|
||||
this.renderChat();
|
||||
this.viewRoot?.rerender();
|
||||
}
|
||||
|
||||
async onClose(): Promise<void> {
|
||||
this.disposeLayoutObservers?.();
|
||||
this.disposeLayoutObservers = null;
|
||||
|
||||
if (this.root) {
|
||||
this.root.unmount();
|
||||
this.root = null;
|
||||
}
|
||||
this.viewRoot?.unmount();
|
||||
this.viewRoot = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ import { CHAT_VIEWTYPE } from "@/constants";
|
|||
import { ChatViewEventTarget, EventTargetContext } from "@/context";
|
||||
import CopilotPlugin from "@/main";
|
||||
import { FileParserManager } from "@/tools/FileParserManager";
|
||||
import { createPluginRoot } from "@/utils/react/createPluginRoot";
|
||||
import { mountPluginViewRoot, type PluginViewRootHandle } from "@/utils/react/mountPluginViewRoot";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { ItemView, Platform, WorkspaceLeaf } from "obsidian";
|
||||
import * as React from "react";
|
||||
import { Root } from "react-dom/client";
|
||||
|
||||
export default class CopilotView extends ItemView {
|
||||
private get chainManager(): ChainManager {
|
||||
|
|
@ -16,12 +15,11 @@ export default class CopilotView extends ItemView {
|
|||
}
|
||||
|
||||
private fileParserManager: FileParserManager;
|
||||
private root: Root | null = null;
|
||||
private viewRoot: PluginViewRootHandle | null = null;
|
||||
private handleSaveAsNote: (() => Promise<void>) | null = null;
|
||||
private keyboardObserver: MutationObserver | null = null;
|
||||
private drawerHideObserver: MutationObserver | null = null;
|
||||
private lastDrawerEl: HTMLElement | null = null;
|
||||
private windowMigrationDestroy: (() => void) | null = null;
|
||||
eventTarget: ChatViewEventTarget;
|
||||
|
||||
constructor(
|
||||
|
|
@ -54,31 +52,10 @@ export default class CopilotView extends ItemView {
|
|||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
this.root = createPluginRoot(this.containerEl.children[1], this.app);
|
||||
const handleSaveAsNote = (saveFunction: () => Promise<void>) => {
|
||||
this.handleSaveAsNote = saveFunction;
|
||||
};
|
||||
const updateUserMessageHistory = (newMessage: string) => {
|
||||
this.plugin.updateUserMessageHistory(newMessage);
|
||||
};
|
||||
|
||||
this.renderView(handleSaveAsNote, updateUserMessageHistory);
|
||||
this.viewRoot = mountPluginViewRoot(this.containerEl, this.app, () => this.renderTree());
|
||||
this.setupMobileKeyboardObserver();
|
||||
this.setupDrawerHideObserver();
|
||||
|
||||
// Reason: When the leaf is dragged to (or back from) an Obsidian popout, the
|
||||
// containerEl is reparented into a different window's document but onOpen
|
||||
// does not re-fire. Lexical's editor._window stays bound to the original
|
||||
// window, so input events fired in the popout never reach the editor.
|
||||
// Tearing down and recreating the React root forces Lexical to re-register
|
||||
// its root element under the new window — typing works again.
|
||||
this.windowMigrationDestroy = this.containerEl.onWindowMigrated(() => {
|
||||
if (!this.root) return;
|
||||
this.root.unmount();
|
||||
this.root = createPluginRoot(this.containerEl.children[1], this.app);
|
||||
this.renderView(handleSaveAsNote, updateUserMessageHistory);
|
||||
});
|
||||
|
||||
// Reason: The view can move between containers (e.g. editor tab → drawer)
|
||||
// without onOpen firing again. Re-bind the drawer observer on layout changes
|
||||
// so it always watches the correct drawer element.
|
||||
|
|
@ -173,21 +150,24 @@ export default class CopilotView extends ItemView {
|
|||
});
|
||||
}
|
||||
|
||||
private renderView(
|
||||
handleSaveAsNote: (saveFunction: () => Promise<void>) => void,
|
||||
updateUserMessageHistory: (newMessage: string) => void
|
||||
): void {
|
||||
if (!this.root) return;
|
||||
private setSaveHandler = (saveFunction: () => Promise<void>): void => {
|
||||
this.handleSaveAsNote = saveFunction;
|
||||
};
|
||||
|
||||
this.root.render(
|
||||
private handleUpdateUserMessageHistory = (newMessage: string): void => {
|
||||
this.plugin.updateUserMessageHistory(newMessage);
|
||||
};
|
||||
|
||||
private renderTree(): React.ReactNode {
|
||||
return (
|
||||
<EventTargetContext.Provider value={this.eventTarget}>
|
||||
<Tooltip.Provider delayDuration={0}>
|
||||
<Chat
|
||||
chainManager={this.chainManager}
|
||||
updateUserMessageHistory={updateUserMessageHistory}
|
||||
updateUserMessageHistory={this.handleUpdateUserMessageHistory}
|
||||
fileParserManager={this.fileParserManager}
|
||||
plugin={this.plugin}
|
||||
onSaveChat={handleSaveAsNote}
|
||||
onSaveChat={this.setSaveHandler}
|
||||
chatUIState={this.plugin.chatUIState}
|
||||
/>
|
||||
</Tooltip.Provider>
|
||||
|
|
@ -202,16 +182,9 @@ export default class CopilotView extends ItemView {
|
|||
}
|
||||
|
||||
updateView(): void {
|
||||
// Note: The new architecture handles message loading through ChatManager
|
||||
// The messages will be loaded when the Chat component initializes
|
||||
const handleSaveAsNote = (saveFunction: () => Promise<void>) => {
|
||||
this.handleSaveAsNote = saveFunction;
|
||||
};
|
||||
const updateUserMessageHistory = (newMessage: string) => {
|
||||
this.plugin.updateUserMessageHistory(newMessage);
|
||||
};
|
||||
|
||||
this.renderView(handleSaveAsNote, updateUserMessageHistory);
|
||||
// The new architecture loads messages through ChatManager when the Chat
|
||||
// component initializes; this just re-renders the existing tree.
|
||||
this.viewRoot?.rerender();
|
||||
}
|
||||
|
||||
async onClose(): Promise<void> {
|
||||
|
|
@ -219,17 +192,13 @@ export default class CopilotView extends ItemView {
|
|||
this.keyboardObserver = null;
|
||||
this.drawerHideObserver?.disconnect();
|
||||
this.drawerHideObserver = null;
|
||||
this.windowMigrationDestroy?.();
|
||||
this.windowMigrationDestroy = null;
|
||||
// Reason: Clean up the class on the tracked drawer element when the view is closed.
|
||||
// Use lastDrawerEl instead of querying closest(), because the view may have already
|
||||
// been detached from the drawer DOM by the time onClose fires.
|
||||
this.lastDrawerEl?.classList.remove("copilot-keyboard-open");
|
||||
this.lastDrawerEl = null;
|
||||
|
||||
if (this.root) {
|
||||
this.root.unmount();
|
||||
this.root = null;
|
||||
}
|
||||
this.viewRoot?.unmount();
|
||||
this.viewRoot = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
50
src/utils/react/mountPluginViewRoot.ts
Normal file
50
src/utils/react/mountPluginViewRoot.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { createPluginRoot } from "@/utils/react/createPluginRoot";
|
||||
import { App } from "obsidian";
|
||||
import { ReactNode } from "react";
|
||||
import { Root } from "react-dom/client";
|
||||
|
||||
export interface PluginViewRootHandle {
|
||||
/** Re-render the current tree (e.g. after external state changes). */
|
||||
rerender(): void;
|
||||
/** Unmount the root and detach the popout-migration listener. */
|
||||
unmount(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount a React root into an Obsidian ItemView's content element and keep it
|
||||
* working when the leaf moves between windows.
|
||||
*
|
||||
* Reason: when a leaf is dragged to (or back from) an Obsidian popout, its
|
||||
* containerEl is reparented into a different window's document but the view's
|
||||
* onOpen does not re-fire. Anything bound to the original window — notably the
|
||||
* Lexical editor inside ChatInput, whose `_window` stays latched — stops
|
||||
* receiving input. Tearing down and recreating the root forces React (and
|
||||
* Lexical) to re-register under the new window.
|
||||
*
|
||||
* `render` is invoked on every (re)mount, so it must return a fresh element
|
||||
* tree each call rather than a cached node.
|
||||
*/
|
||||
export function mountPluginViewRoot(
|
||||
containerEl: HTMLElement,
|
||||
app: App,
|
||||
render: () => ReactNode
|
||||
): PluginViewRootHandle {
|
||||
let root: Root = createPluginRoot(containerEl.children[1], app);
|
||||
root.render(render());
|
||||
|
||||
const detachMigration = containerEl.onWindowMigrated(() => {
|
||||
root.unmount();
|
||||
root = createPluginRoot(containerEl.children[1], app);
|
||||
root.render(render());
|
||||
});
|
||||
|
||||
return {
|
||||
rerender() {
|
||||
root.render(render());
|
||||
},
|
||||
unmount() {
|
||||
detachMigration();
|
||||
root.unmount();
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue