From 0ddaab53e4f7e86257481a8f030edfc757147a18 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 16 Apr 2026 15:15:44 +0200 Subject: [PATCH] feat(workbench): Scene.scopeTagIds + reactive scene-scope store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WorkbenchScene grows an optional scopeTagIds field so scenes can act as data-scope lenses: when set, module queries filter records to those tagged with at least one of the scene's tags (+ untagged = global). New reactive store scene-scope.svelte.ts: - setSceneScopeTagIds(ids) — called by scene store on switch/init - getSceneScopeTagIds() — read by module queries - filterBySceneScope() — reusable filter (same semantics as AI scope-context's filterByScope) Wired into workbench-scenes.svelte.ts: - setActiveScene() syncs scope on manual switch - liveQuery subscription syncs scope on init/sync/tab-focus Module queries can now opt into scene scoping by calling filterBySceneScope in their useAll* hooks — not wired yet (each module opts in as needed). The foundation is in place. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web/src/lib/stores/scene-scope.svelte.ts | 54 +++++++++++++++++++ .../src/lib/stores/workbench-scenes.svelte.ts | 7 +++ .../web/src/lib/types/workbench-scenes.ts | 9 ++++ 3 files changed, 70 insertions(+) create mode 100644 apps/mana/apps/web/src/lib/stores/scene-scope.svelte.ts diff --git a/apps/mana/apps/web/src/lib/stores/scene-scope.svelte.ts b/apps/mana/apps/web/src/lib/stores/scene-scope.svelte.ts new file mode 100644 index 000000000..7dfe943de --- /dev/null +++ b/apps/mana/apps/web/src/lib/stores/scene-scope.svelte.ts @@ -0,0 +1,54 @@ +/** + * Reactive scene scope — propagates the active scene's scopeTagIds to + * module queries so the UI can filter records by the scene's tags. + * + * Updated by the workbench layout whenever the active scene changes. + * Module queries read `currentScopeTagIds` and filter accordingly. + * Undefined = no filtering (everything visible). + * + * This is the UI-facing counterpart of the AI-facing `withAgentScope` + * in `data/ai/scope-context.ts`. Both use the same tag system; the AI + * scope is ephemeral (duration of a mission run), this one is long- + * lived (persists as long as the scene is active). + */ + +/** Reactive scope state — set by workbench layout, read by module queries. */ +let _scopeTagIds = $state(undefined); + +/** Set the active scene's scope. Called from the workbench layout + * when the active scene changes. Pass undefined to clear. */ +export function setSceneScopeTagIds(tagIds: readonly string[] | undefined): void { + _scopeTagIds = tagIds?.length ? tagIds : undefined; +} + +/** Read the current scene scope. Returns undefined when no scope is + * active (= show everything). Module queries use this to filter. */ +export function getSceneScopeTagIds(): readonly string[] | undefined { + return _scopeTagIds; +} + +/** + * Utility: filter records by the active scene scope. Same semantics as + * `filterByScope` in AI scope-context: untagged records pass through, + * tagged records must match at least one scope tag. + * + * Synchronous if tagIds are already loaded; async variant for + * junction-table lookups (same signature as the AI version). + */ +export async function filterBySceneScope( + records: T[], + getTagIdsForRecord: (record: T) => Promise +): Promise { + const scope = _scopeTagIds; + if (!scope) return records; + + const scopeSet = new Set(scope); + const results: T[] = []; + for (const r of records) { + const tagIds = await getTagIdsForRecord(r); + if (tagIds.length === 0 || tagIds.some((id) => scopeSet.has(id))) { + results.push(r); + } + } + return results; +} diff --git a/apps/mana/apps/web/src/lib/stores/workbench-scenes.svelte.ts b/apps/mana/apps/web/src/lib/stores/workbench-scenes.svelte.ts index 581a2fecc..c141fd8d3 100644 --- a/apps/mana/apps/web/src/lib/stores/workbench-scenes.svelte.ts +++ b/apps/mana/apps/web/src/lib/stores/workbench-scenes.svelte.ts @@ -21,6 +21,7 @@ import type { WorkbenchScene, WorkbenchSceneApp, } from '$lib/types/workbench-scenes'; +import { setSceneScopeTagIds } from './scene-scope.svelte'; const TABLE = 'workbenchScenes'; const ACTIVE_SCENE_LS_KEY = 'mana:workbench:activeSceneId'; @@ -163,6 +164,9 @@ export const workbenchScenesStore = { activeSceneIdState = next; writeActiveIdToStorage(next); } + // Sync scope when scenes reload (init, sync pull, tab focus). + const activeScope = visible.find((s) => s.id === (next ?? activeSceneIdState)); + setSceneScopeTagIds(activeScope?.scopeTagIds); initializedState = true; }, error: (err) => { @@ -183,6 +187,9 @@ export const workbenchScenesStore = { if (!scenesState.some((s) => s.id === id)) return; activeSceneIdState = id; writeActiveIdToStorage(id); + // Sync scene scope for module queries + const scene = scenesState.find((s) => s.id === id); + setSceneScopeTagIds(scene?.scopeTagIds); }, async createScene(opts: { diff --git a/apps/mana/apps/web/src/lib/types/workbench-scenes.ts b/apps/mana/apps/web/src/lib/types/workbench-scenes.ts index 34826d657..8fc7bdcb0 100644 --- a/apps/mana/apps/web/src/lib/types/workbench-scenes.ts +++ b/apps/mana/apps/web/src/lib/types/workbench-scenes.ts @@ -42,6 +42,15 @@ export interface WorkbenchScene { * settings. See docs/plans/multi-agent-workbench.md §Phase 5d. */ viewingAsAgentId?: string; + /** + * Tag-based data scope for this scene (Phase 6). When set, module + * queries in apps rendered within this scene filter records to only + * those tagged with at least one of these global tag IDs (+ untagged + * records which are globally visible). Auto-inferred from the bound + * agent's `scopeTagIds` if the user hasn't overridden it explicitly. + * Undefined = no filtering, everything visible. + */ + scopeTagIds?: string[]; } /** Dexie row shape (adds the BaseRecord audit fields stamped by hooks). */