From c0e4fc590c873f9562df8e503e8b0bbada1a5948 Mon Sep 17 00:00:00 2001 From: murashit Date: Fri, 10 Jul 2026 12:41:53 +0900 Subject: [PATCH] Share vault reference providers across chat panels --- ...ser-context-reference-provider.obsidian.ts | 36 +++++++++++++++ .../vault-note-candidate-provider.obsidian.ts | 44 +++++++++++++++++++ .../vault-note-candidate-provider.test.ts | 29 ++++++++++++ 3 files changed, 109 insertions(+) diff --git a/src/features/chat/host/obsidian/vault-composer-context-reference-provider.obsidian.ts b/src/features/chat/host/obsidian/vault-composer-context-reference-provider.obsidian.ts index 49a3a452..0ed6070d 100644 --- a/src/features/chat/host/obsidian/vault-composer-context-reference-provider.obsidian.ts +++ b/src/features/chat/host/obsidian/vault-composer-context-reference-provider.obsidian.ts @@ -13,6 +13,42 @@ interface EventSource { } export class VaultComposerContextReferenceProvider implements ComposerContextReferenceProvider { + private readonly shared: SharedComposerContext; + private disposed = false; + + constructor(private readonly app: App) { + const existing = sharedComposerContexts.get(app); + if (existing) { + existing.consumers += 1; + this.shared = existing; + return; + } + this.shared = { tracker: new VaultComposerContextTracker(app), consumers: 1 }; + sharedComposerContexts.set(app, this.shared); + } + + contextReferences(sourcePath: string): ComposerContextReferences { + return this.shared.tracker.contextReferences(sourcePath); + } + + dispose(): void { + if (this.disposed) return; + this.disposed = true; + this.shared.consumers -= 1; + if (this.shared.consumers > 0) return; + this.shared.tracker.dispose(); + sharedComposerContexts.delete(this.app); + } +} + +interface SharedComposerContext { + readonly tracker: VaultComposerContextTracker; + consumers: number; +} + +const sharedComposerContexts = new WeakMap(); + +class VaultComposerContextTracker { private readonly unregisterEvents: (() => void)[] = []; private lastMarkdownView: MarkdownView | null = null; diff --git a/src/features/chat/host/obsidian/vault-note-candidate-provider.obsidian.ts b/src/features/chat/host/obsidian/vault-note-candidate-provider.obsidian.ts index 92c6ff37..4f3ed853 100644 --- a/src/features/chat/host/obsidian/vault-note-candidate-provider.obsidian.ts +++ b/src/features/chat/host/obsidian/vault-note-candidate-provider.obsidian.ts @@ -22,7 +22,51 @@ interface MetadataCacheWithTags { getTags?: () => unknown; } +interface SharedCandidateCatalog { + readonly catalog: VaultNoteCandidateCatalog; + consumers: number; +} + +const candidateCatalogs = new WeakMap(); + export class VaultNoteCandidateProvider implements NoteCandidateProvider { + private readonly shared: SharedCandidateCatalog; + private disposed = false; + + constructor(private readonly app: App) { + const existing = candidateCatalogs.get(app); + if (existing) { + existing.consumers += 1; + this.shared = existing; + return; + } + this.shared = { catalog: new VaultNoteCandidateCatalog(app), consumers: 1 }; + candidateCatalogs.set(app, this.shared); + } + + candidates(sourcePath: string): readonly NoteCandidate[] { + return this.shared.catalog.candidates(sourcePath); + } + + tags(): readonly string[] { + return this.shared.catalog.tags(); + } + + resolveMention(target: string, sourcePath: string): WikiLinkMention | null { + return this.shared.catalog.resolveMention(target, sourcePath); + } + + dispose(): void { + if (this.disposed) return; + this.disposed = true; + this.shared.consumers -= 1; + if (this.shared.consumers > 0) return; + this.shared.catalog.dispose(); + candidateCatalogs.delete(this.app); + } +} + +class VaultNoteCandidateCatalog { private readonly unregisterEvents: (() => void)[] = []; private fileCandidatesCache: FileCandidate[] | null = null; private tagCandidatesCache: string[] | null = null; diff --git a/tests/features/chat/host/vault-note-candidate-provider.test.ts b/tests/features/chat/host/vault-note-candidate-provider.test.ts index adaa33f7..676a877a 100644 --- a/tests/features/chat/host/vault-note-candidate-provider.test.ts +++ b/tests/features/chat/host/vault-note-candidate-provider.test.ts @@ -147,6 +147,23 @@ describe("VaultNoteCandidateProvider", () => { expect(app.offref).toHaveBeenCalledTimes(6); }); + it("shares candidate caches and event subscriptions across panel providers", () => { + const getFiles = vi.fn(() => vaultFiles([{ basename: "Alpha", path: "Alpha.md", stat: { mtime: 1 } }])); + const app = appFixture({ getFiles }); + const first = new VaultNoteCandidateProvider(app); + const second = new VaultNoteCandidateProvider(app); + + first.candidates("Inbox.md"); + second.candidates("Inbox.md"); + first.dispose(); + + expect(getFiles).toHaveBeenCalledOnce(); + expect(app.offref).not.toHaveBeenCalled(); + + second.dispose(); + expect(app.offref).toHaveBeenCalledTimes(6); + }); + it("resolves wikilinks through metadata cache before direct path fallback", () => { const linked = tFile("notes/Alpha.md", "Alpha"); const direct = tFile("Alpha.md", "Alpha direct"); @@ -218,6 +235,18 @@ describe("VaultNoteCandidateProvider", () => { }, }); }); + + it("shares active-view event tracking across panel context providers", () => { + const app = appFixture(); + const first = new VaultComposerContextReferenceProvider(app); + const second = new VaultComposerContextReferenceProvider(app); + + first.dispose(); + expect(app.offref).not.toHaveBeenCalled(); + + second.dispose(); + expect(app.offref).toHaveBeenCalledTimes(2); + }); }); interface AppFixture extends App {