From 982a2462f4b043eeaa23bcda1d26c767caccfee4 Mon Sep 17 00:00:00 2001 From: Zero Liu Date: Tue, 7 Jul 2026 10:00:26 +0800 Subject: [PATCH] Revert "feat(agent-home): surface Relevant Notes as a home-shelf tab (#2639)" (#2667) This reverts commit 73664992296fc714f85391497c82200a1f4811ba. --- jest.config.js | 4 -- src/agentMode/ui/AgentHome.tsx | 43 ++----------- src/agentMode/ui/AgentHomeShelf.test.tsx | 25 -------- src/agentMode/ui/AgentHomeShelf.tsx | 3 +- src/agentMode/ui/AgentHomeTab.tsx | 4 +- src/agentMode/ui/CopilotAgentView.tsx | 3 - src/agentMode/ui/RelevantNotesShelfPanel.tsx | 66 -------------------- src/agentMode/ui/homeShelfPrefs.ts | 53 ---------------- src/agentMode/ui/useRelevantNotesPaneOpen.ts | 19 ------ src/components/CopilotView.tsx | 3 - src/components/RelevantNotesView.tsx | 19 +++--- src/main.ts | 16 +++++ src/utils/registerActiveLeafChangeBridge.ts | 23 ------- 13 files changed, 35 insertions(+), 246 deletions(-) delete mode 100644 src/agentMode/ui/RelevantNotesShelfPanel.tsx delete mode 100644 src/agentMode/ui/homeShelfPrefs.ts delete mode 100644 src/agentMode/ui/useRelevantNotesPaneOpen.ts delete mode 100644 src/utils/registerActiveLeafChangeBridge.ts diff --git a/jest.config.js b/jest.config.js index d634ac13..fa7b91d1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,10 +13,6 @@ module.exports = { // jsdom; Jest can't parse ESM without extra config, so point at the CJS // build it ships under dist/. "^yaml$": "/node_modules/yaml/dist/index.js", - // @orama/orama resolves to its ESM "browser" entry under jsdom, which Jest - // can't parse; point at the CJS build it ships under dist/commonjs/ (same - // reason as yaml above). - "^@orama/orama$": "/node_modules/@orama/orama/dist/commonjs/index.js", "^@agentclientprotocol/sdk$": "/__mocks__/@agentclientprotocol/sdk.js", "^@anthropic-ai/claude-agent-sdk$": "/__mocks__/@anthropic-ai/claude-agent-sdk.js", // react-resizable-panels is ESM-only with no CJS build to point at; stub it. diff --git a/src/agentMode/ui/AgentHome.tsx b/src/agentMode/ui/AgentHome.tsx index 4bbdbfa0..bfea1eef 100644 --- a/src/agentMode/ui/AgentHome.tsx +++ b/src/agentMode/ui/AgentHome.tsx @@ -13,14 +13,7 @@ import { AgentWelcomeCard } from "@/agentMode/ui/AgentWelcomeCard"; import { CopilotBrandIcon } from "@/agentMode/ui/CopilotBrandIcon"; import { AgentHomeShelf, type AgentHomeShelfSection } from "@/agentMode/ui/AgentHomeShelf"; import { GlobalRecentChatsSection } from "@/agentMode/ui/GlobalRecentChatsSection"; -import { - getHomeShelfTab, - setHomeShelfTab, - HOME_SHELF_TAB_STORAGE_KEY, -} from "@/agentMode/ui/homeShelfPrefs"; import { ProjectPickerList } from "@/agentMode/ui/ProjectPickerList"; -import { RelevantNotesShelfPanel } from "@/agentMode/ui/RelevantNotesShelfPanel"; -import { useRelevantNotesPaneOpen } from "@/agentMode/ui/useRelevantNotesPaneOpen"; import { useAgentChatRuntimeState } from "@/agentMode/ui/hooks/useAgentChatRuntimeState"; import { useAgentHistoryControls } from "@/agentMode/ui/hooks/useAgentHistoryControls"; import { useAgentInputDrafts } from "@/agentMode/ui/hooks/useAgentInputDrafts"; @@ -51,7 +44,7 @@ import { getProjectLandingCaptureSignature } from "@/projects/projectContextSign import { getCachedProjectRecordById, useProjects } from "@/projects/state"; import { getSettings, settingsStore, updateSetting, useSettingsValue } from "@/settings/model"; import { useAtomValue } from "jotai"; -import { FileSearch, Files, Folder, MessageSquare } from "lucide-react"; +import { Files, Folder, MessageSquare } from "lucide-react"; import { Notice } from "obsidian"; import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; @@ -92,7 +85,6 @@ const AgentHomeInternal: React.FC = ({ const settings = useSettingsValue(); const eventTarget = useContext(EventTargetContext); const chatInput = useChatInput(); - const isRelevantNotesPaneOpen = useRelevantNotesPaneOpen(app); // Place the caret in the composer when the agent view opens so the user can // type immediately. AgentHome only mounts once preload settles and a session @@ -515,17 +507,9 @@ const AgentHomeInternal: React.FC = ({ // instance-level UI memory: wherever the user left the global shelf, they // return to it (entering a project from the Projects tab and backing out // lands on Projects again instead of snapping to Recent Chats). AgentHome - // stays mounted across those switches, so the state survives. On top of that - // in-instance memory we seed from (and write back to) device-local storage so - // the choice also survives a full remount / reload. null = nothing picked yet - // → the shelf resolves to its first selectable tab. - const [globalShelfTab, setGlobalShelfTabState] = useState(() => - getHomeShelfTab(HOME_SHELF_TAB_STORAGE_KEY) - ); - const setGlobalShelfTab = useCallback((id: string) => { - setGlobalShelfTabState(id); - setHomeShelfTab(HOME_SHELF_TAB_STORAGE_KEY, id); - }, []); + // stays mounted across those switches, so the state survives. null = nothing + // picked yet → the shelf resolves to its first selectable tab. + const [globalShelfTab, setGlobalShelfTab] = useState(null); // Chip-shelf sections for the landing. Each body renders lazily (only the open // section is mounted), so these render closures are cheap to recreate. @@ -549,23 +533,6 @@ const AgentHomeInternal: React.FC = ({ /> ), }, - // Hidden while the dedicated Relevant Notes pane is open, so the shelf - // doesn't duplicate a surface the user already has pinned alongside chat. - ...(isRelevantNotesPaneOpen - ? [] - : [ - { - id: "relevant-notes", - icon: , - title: "Relevant Notes", - renderBody: () => ( - void plugin.activateRelevantNotesView()} - onAddToChat={(text) => void plugin.insertTextIntoActiveChat(text)} - /> - ), - }, - ]), { id: "projects", icon: , @@ -598,8 +565,6 @@ const AgentHomeInternal: React.FC = ({ handleLoadChatHistory, runningChatIds, attentionChatIds, - isRelevantNotesPaneOpen, - plugin, ] ); diff --git a/src/agentMode/ui/AgentHomeShelf.test.tsx b/src/agentMode/ui/AgentHomeShelf.test.tsx index 63566068..502fe4ec 100644 --- a/src/agentMode/ui/AgentHomeShelf.test.tsx +++ b/src/agentMode/ui/AgentHomeShelf.test.tsx @@ -91,28 +91,3 @@ describe("AgentHomeShelf controlled mode", () => { expect(screen.queryByText("PROJECTS BODY")).toBeNull(); }); }); - -describe("AgentHomeTab count badge (via the shelf)", () => { - const withCount = (count?: number): AgentHomeShelfSection => ({ - id: "chats", - icon: , - title: "Recent Chats", - count, - renderBody: () =>
CHATS BODY
, - }); - - it("shows no badge when count is undefined (e.g. the Relevant Notes tab)", () => { - renderShelf([withCount(undefined)]); - expect(screen.getByRole("tab", { name: /Recent Chats/ }).textContent ?? "").not.toMatch(/\d/); - }); - - it("shows no badge when count is 0", () => { - renderShelf([withCount(0)]); - expect(screen.getByRole("tab", { name: /Recent Chats/ }).textContent ?? "").not.toContain("0"); - }); - - it("shows the badge when count is positive", () => { - renderShelf([withCount(3)]); - expect(screen.getByRole("tab", { name: /Recent Chats/ }).textContent ?? "").toContain("3"); - }); -}); diff --git a/src/agentMode/ui/AgentHomeShelf.tsx b/src/agentMode/ui/AgentHomeShelf.tsx index c092096a..9032c367 100644 --- a/src/agentMode/ui/AgentHomeShelf.tsx +++ b/src/agentMode/ui/AgentHomeShelf.tsx @@ -18,8 +18,7 @@ export interface AgentHomeShelfSection { /** Leading type icon for the tab. */ icon: React.ReactNode; title: string; - /** Optional tally shown next to the title; the badge renders only when > 0. */ - count?: number; + count: number; /** Rendered into the panel while this tab is the selected one. */ renderBody: () => React.ReactNode; /** diff --git a/src/agentMode/ui/AgentHomeTab.tsx b/src/agentMode/ui/AgentHomeTab.tsx index 83f92ed7..ae8edb5a 100644 --- a/src/agentMode/ui/AgentHomeTab.tsx +++ b/src/agentMode/ui/AgentHomeTab.tsx @@ -9,7 +9,7 @@ interface AgentHomeTabProps { /** Leading type icon (sized by the caller, typically `tw-size-4`). */ icon: React.ReactNode; title: string; - count?: number; + count: number; /** Whether this tab is the selected one. */ active: boolean; onClick: () => void; @@ -68,7 +68,7 @@ export const AgentHomeTab = memo(function AgentHomeTab({ > {icon} {title} - {!disabled && typeof count === "number" && count > 0 && ( + {!disabled && ( {count} diff --git a/src/agentMode/ui/CopilotAgentView.tsx b/src/agentMode/ui/CopilotAgentView.tsx index 0aa12825..40acb292 100644 --- a/src/agentMode/ui/CopilotAgentView.tsx +++ b/src/agentMode/ui/CopilotAgentView.tsx @@ -3,7 +3,6 @@ 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 { registerActiveLeafChangeBridge } from "@/utils/registerActiveLeafChangeBridge"; import { mountPluginViewRoot, type PluginViewRootHandle } from "@/utils/react/mountPluginViewRoot"; import * as Tooltip from "@radix-ui/react-tooltip"; import { ItemView, WorkspaceLeaf } from "obsidian"; @@ -47,8 +46,6 @@ export default class CopilotAgentView extends ItemView { const observers = attachChatViewLayoutObservers(this.containerEl); this.disposeLayoutObservers = observers.dispose; - registerActiveLeafChangeBridge(this, this.eventTarget); - this.registerEvent( this.app.workspace.on("layout-change", () => { window.requestAnimationFrame(() => observers.rebindDrawerObserver()); diff --git a/src/agentMode/ui/RelevantNotesShelfPanel.tsx b/src/agentMode/ui/RelevantNotesShelfPanel.tsx deleted file mode 100644 index d21f3df6..00000000 --- a/src/agentMode/ui/RelevantNotesShelfPanel.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { dismissPopOutHint, isPopOutHintDismissed } from "@/agentMode/ui/homeShelfPrefs"; -import { RelevantNotes } from "@/components/chat-components/RelevantNotes"; -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; -import { ExternalLink, X } from "lucide-react"; -import React, { useState } from "react"; - -interface RelevantNotesShelfPanelProps { - /** Open the dedicated Relevant Notes pane (the shelf is transient). */ - onPopOut: () => void; - /** Insert text (a `[[wikilink]]`) into the target chat input. */ - onAddToChat: (text: string) => void; -} - -/** - * Relevant Notes rendered inside the Agent Home shelf. The shelf is transient - * (it collapses as you chat), so a one-time, dismissible hint nudges the user - * toward the dedicated pane that persists alongside the conversation. Assumes - * the surrounding agent view already provides AppContext + EventTargetContext. - */ -export function RelevantNotesShelfPanel({ onPopOut, onAddToChat }: RelevantNotesShelfPanelProps) { - const [hintDismissed, setHintDismissed] = useState(() => isPopOutHintDismissed()); - - const handleDismiss = () => { - dismissPopOutHint(); - setHintDismissed(true); - }; - - return ( -
- {!hintDismissed && ( -
- - Open Relevant Notes in its own pane to keep it while you chat. - - - -
- )} -
- -
-
- ); -} diff --git a/src/agentMode/ui/homeShelfPrefs.ts b/src/agentMode/ui/homeShelfPrefs.ts deleted file mode 100644 index 7e80c9bc..00000000 --- a/src/agentMode/ui/homeShelfPrefs.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Device-local UI preferences for the Agent Home shelf, persisted in - * `window.localStorage` (which Obsidian never syncs). Mirrors the storage idiom - * in `deviceId.ts`: every access is wrapped in try/catch, reads return a safe - * default on failure, and writes swallow errors so a broken-storage device - * (disabled / restricted) never crashes the UI — it just loses persistence. - * - * These are intentionally device-local: which shelf tab you last viewed and - * whether you dismissed the pop-out hint are per-device UI state, not content - * that should ride a synced `data.json` to your other devices. - */ - -import { logWarn } from "@/logger"; - -export const HOME_SHELF_TAB_STORAGE_KEY = "copilot:home-shelf-tab:v1"; -const POPOUT_HINT_DISMISSED_KEY = "copilot:relevant-notes-popout-hint-dismissed:v1"; - -/** Read the last-selected shelf tab id, or `null` when unset or storage is unusable. */ -export function getHomeShelfTab(storageKey: string): string | null { - try { - const value = window.localStorage.getItem(storageKey); - return value && value.length > 0 ? value : null; - } catch { - return null; - } -} - -/** Persist the selected shelf tab id; no-op if storage is unusable. */ -export function setHomeShelfTab(storageKey: string, id: string): void { - try { - window.localStorage.setItem(storageKey, id); - } catch (e) { - logWarn("Failed to persist home shelf tab", e); - } -} - -/** Whether the user dismissed the Relevant Notes pop-out hint; defaults to false. */ -export function isPopOutHintDismissed(): boolean { - try { - return window.localStorage.getItem(POPOUT_HINT_DISMISSED_KEY) === "true"; - } catch { - return false; - } -} - -/** Mark the Relevant Notes pop-out hint dismissed; no-op if storage is unusable. */ -export function dismissPopOutHint(): void { - try { - window.localStorage.setItem(POPOUT_HINT_DISMISSED_KEY, "true"); - } catch (e) { - logWarn("Failed to persist pop-out hint dismissal", e); - } -} diff --git a/src/agentMode/ui/useRelevantNotesPaneOpen.ts b/src/agentMode/ui/useRelevantNotesPaneOpen.ts deleted file mode 100644 index 48870816..00000000 --- a/src/agentMode/ui/useRelevantNotesPaneOpen.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { RELEVANT_NOTES_VIEWTYPE } from "@/constants"; -import type { App } from "obsidian"; -import { useEffect, useState } from "react"; - -/** True while a dedicated Relevant Notes pane leaf is open; updates on layout-change. */ -export function useRelevantNotesPaneOpen(app: App): boolean { - const [open, setOpen] = useState( - () => app.workspace.getLeavesOfType(RELEVANT_NOTES_VIEWTYPE).length > 0 - ); - useEffect(() => { - // Re-sync on mount in case the workspace changed between the lazy-init read - // and this effect running (e.g. a layout restored in the same tick). - const update = () => setOpen(app.workspace.getLeavesOfType(RELEVANT_NOTES_VIEWTYPE).length > 0); - update(); - const ref = app.workspace.on("layout-change", update); - return () => app.workspace.offref(ref); - }, [app]); - return open; -} diff --git a/src/components/CopilotView.tsx b/src/components/CopilotView.tsx index bfac535e..8904fcab 100644 --- a/src/components/CopilotView.tsx +++ b/src/components/CopilotView.tsx @@ -4,7 +4,6 @@ import { CHAT_VIEWTYPE } from "@/constants"; import { ChatViewEventTarget, EventTargetContext } from "@/context"; import CopilotPlugin from "@/main"; import { FileParserManager } from "@/tools/FileParserManager"; -import { registerActiveLeafChangeBridge } from "@/utils/registerActiveLeafChangeBridge"; import { mountPluginViewRoot, type PluginViewRootHandle } from "@/utils/react/mountPluginViewRoot"; import * as Tooltip from "@radix-ui/react-tooltip"; import { ItemView, Platform, WorkspaceLeaf } from "obsidian"; @@ -57,8 +56,6 @@ export default class CopilotView extends ItemView { this.setupMobileKeyboardObserver(); this.setupDrawerHideObserver(); - registerActiveLeafChangeBridge(this, this.eventTarget); - // 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. diff --git a/src/components/RelevantNotesView.tsx b/src/components/RelevantNotesView.tsx index 5dfe6050..3430ab69 100644 --- a/src/components/RelevantNotesView.tsx +++ b/src/components/RelevantNotesView.tsx @@ -1,10 +1,9 @@ import { RelevantNotes } from "@/components/chat-components/RelevantNotes"; -import { RELEVANT_NOTES_VIEWTYPE } from "@/constants"; +import { EVENT_NAMES, RELEVANT_NOTES_VIEWTYPE } from "@/constants"; import { EventTargetContext } from "@/context"; import CopilotPlugin from "@/main"; import { createPluginRoot } from "@/utils/react/createPluginRoot"; -import { registerActiveLeafChangeBridge } from "@/utils/registerActiveLeafChangeBridge"; -import { ItemView, WorkspaceLeaf } from "obsidian"; +import { ItemView, MarkdownView, WorkspaceLeaf } from "obsidian"; import * as React from "react"; import { Root } from "react-dom/client"; @@ -13,9 +12,9 @@ import { Root } from "react-dom/client"; * * `RelevantNotes` reads the active file via `useActiveFile`, which seeds from * the current active file on mount and then updates on the `ACTIVE_LEAF_CHANGE` - * event on this view's own `eventTarget`. This view bridges that event itself - * via `registerActiveLeafChangeBridge` since no other component feeds its - * `eventTarget`. + * event on this view's own `eventTarget`. The plugin's global handler dispatches + * that event only to the legacy chat view, so this view feeds its own + * `eventTarget` (mirroring that handler's condition) on subsequent leaf changes. */ export default class RelevantNotesView extends ItemView { private root: Root | null = null; @@ -50,7 +49,13 @@ export default class RelevantNotesView extends ItemView { this.root = createPluginRoot(this.containerEl.children[1], this.app); this.renderView(); - registerActiveLeafChangeBridge(this, this.eventTarget); + this.registerEvent( + this.app.workspace.on("active-leaf-change", (leaf) => { + if (leaf?.view instanceof MarkdownView && leaf.view.file) { + this.eventTarget.dispatchEvent(new CustomEvent(EVENT_NAMES.ACTIVE_LEAF_CHANGE)); + } + }) + ); } private renderView(): void { diff --git a/src/main.ts b/src/main.ts index 6dae98c0..84d17a69 100644 --- a/src/main.ts +++ b/src/main.ts @@ -373,6 +373,22 @@ export default class CopilotPlugin extends Plugin { if (activeViewType === CHAT_VIEWTYPE || activeViewType === CHAT_AGENT_VIEWTYPE) { this.lastActiveChatViewType = activeViewType; } + + if (leaf && leaf.view instanceof MarkdownView) { + const file = leaf.view.file; + if (file) { + // Note: File tracking and real-time reindexing removed for simplicity + // Semantic search indexes are rebuilt manually or on startup as needed + const activeCopilotView = this.app.workspace + .getLeavesOfType(CHAT_VIEWTYPE) + .find((leaf) => leaf.view instanceof CopilotView)?.view as CopilotView; + + if (activeCopilotView) { + const event = new CustomEvent(EVENT_NAMES.ACTIVE_LEAF_CHANGE); + activeCopilotView.eventTarget.dispatchEvent(event); + } + } + } }) ); diff --git a/src/utils/registerActiveLeafChangeBridge.ts b/src/utils/registerActiveLeafChangeBridge.ts deleted file mode 100644 index 8c0d0338..00000000 --- a/src/utils/registerActiveLeafChangeBridge.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { EVENT_NAMES } from "@/constants"; -import { ItemView, MarkdownView } from "obsidian"; - -/** - * Bridge Obsidian's workspace `active-leaf-change` onto a view-local - * `eventTarget` so React consumers (via `useActiveFile`) re-read the active - * file when the user switches notes. - * - * Each chat-ish view self-owns this bridge rather than relying on a single - * plugin-level dispatch: `useActiveFile` listens on the view's own - * `eventTarget`, so a view that never feeds its target stays frozen at its - * mount seed. Registering through the view's `registerEvent` ties the listener - * to the view lifecycle (auto-unregistered on close). - */ -export function registerActiveLeafChangeBridge(view: ItemView, eventTarget: EventTarget): void { - view.registerEvent( - view.app.workspace.on("active-leaf-change", (leaf) => { - if (leaf?.view instanceof MarkdownView && leaf.view.file) { - eventTarget.dispatchEvent(new CustomEvent(EVENT_NAMES.ACTIVE_LEAF_CHANGE)); - } - }) - ); -}