Skip to content

FEAT: Multi-Entity Scope for Prompts & Skills - #208

Merged
mbeps merged 3 commits into
mainfrom
dev
Nov 9, 2026
Merged

mbeps merged 3 commits into
mainfrom
dev

Conversation

@mbeps

@mbeps mbeps commented Nov 9, 2026

Copy link
Copy Markdown
Owner

1. High-Level Overview

Goal: Enable prompt shortcuts and agent skills to be scoped to multiple projects, multiple assistants, both simultaneously, or neither (global), replacing single-entity mutual exclusion with multi-entity array scoping and an intuitive Shadcn UI selector.

flowchart TD
    subgraph UI ["User Interface (Shadcn UI)"]
        Tab["Scope & Visibility Tab / Form"]
        Mode["Mode: Global vs. Scoped"]
        ProjSelect["Projects Multi-Select (Combobox / Popover + Badges)"]
        AsstSelect["Assistants Multi-Select (Combobox / Popover + Badges)"]
    end

    subgraph DataModel ["Data Model & Validation"]
        Zod["Zod Schemas: projectIds: string[], assistantIds: string[]"]
        Drizzle["Drizzle: project_ids text[], assistant_ids text[]"]
    end

    subgraph Runtime ["Execution & Scoping Context"]
        FilterPrompts["filterAccessiblePrompts(prompts, context)"]
        FilterSkills["filterAccessibleSkills(skills, context)"]
        ChatCtx["loadChatContext & registerSkillTool"]
    end

    Mode -->|"Global"| Zod
    Mode -->|"Scoped"| ProjSelect & AsstSelect
    ProjSelect & AsstSelect --> Zod
    Zod --> Drizzle
    Drizzle --> Runtime
Loading

Global Constraints

  • Zero-Git commit policy (only read-only git commands).
  • Use Podman for database containers if needed.
  • No modifications to Shadcn UI primitives in components/ui/; compose them into high-level features.
  • Radical simplicity (YAGNI/Ponytail): Reuse existing array column patterns from assistant and project rather than introducing junction tables.
  • No backwards compatibility required; migrate existing PostgreSQL data directly.

2. User Review Required

Important

Database Migration: The single-column foreign keys project_id and assistant_id on both prompt and skill tables are replaced with PostgreSQL native array columns project_ids text[] DEFAULT '{}'::text[] NOT NULL and assistant_ids text[] DEFAULT '{}'::text[] NOT NULL. Existing rows with non-null values will be migrated into 1-element arrays before dropping the legacy columns.

Note

Scope UI Concept:
The UI replaces the 3 mutually-exclusive radio buttons (Global, Project, Assistant) with:

  1. A clean two-state selector: Global (available everywhere) vs. Scoped (restricted to specific entities).
  2. When Scoped is active, two multi-select pickers appear side-by-side or stacked:
    • Projects Multi-Select: Combobox/Popover with search, checkable items, and removable badges.
    • Assistants Multi-Select: Combobox/Popover with search, checkable items, and removable badges.
  3. Users can select any combination (e.g., 2 projects AND 1 assistant, or 3 projects only, or 2 assistants only).

3. Alternatives Evaluated

Architecture Pros Cons Verdict
Array Columns (project_ids text[], assistant_ids text[]) 100% consistent with project.skill_ids, project.prompt_ids, assistant.skill_ids, assistant.prompt_ids. Zero joins needed. Instant client hydration into Zustand store. Minimal code changes. Relies on PostgreSQL array operations instead of standard relational junction tables. Selected (Recommended)
Junction Tables (prompt_projects, prompt_assistants, skill_projects, skill_assistants) Standard relational 3NF design with native cascading foreign keys. Requires 4 new tables, complex SQL joins on every fetch (listPrompts, listSkills, loadChatContext), complex Drizzle relational mapping, much higher code bloat. Violates YAGNI. Rejected
JSONB Scope Document (scopes: jsonb) Flexible schema. Overkill for simple lists of UUIDs, worse typing in Drizzle, harder array search index. Rejected

4. Edge Cases & Failure Modes

  • Entity Deletion: If a user deletes a project or assistant, any prompt or skill that included that entity ID will still function for its remaining scopes. If it had only that entity, it becomes global or empty scoped. We also sanitize IDs in filterAccessiblePrompts / filterAccessibleSkills.
  • Empty Scoped Selection: If a user selects "Scoped" mode but doesn't select any projects or assistants, form validation requires at least 1 selection or switching back to "Global".
  • Chat in Project WITH Assistant: If a chat is set to Project A and uses Assistant B, prompts/skills scoped to Project A, OR Assistant B, OR Global (governed by modes) will all be accessible.
  • Pre-filling from URL parameters: When opening /settings/prompts/new?projectId=123, projectIds is initialized with ['123'], and the user can easily add more projects or assistants.

mbeps added 3 commits November 9, 2026 09:15
1. **Database & Migrations**:
   - Replaced single `projectId` and `assistantId` foreign keys with PostgreSQL text arrays `projectIds: text("project_ids").array().notNull().default(sql`'{}'::text[]`)` and `assistantIds: text("assistant_ids").array().notNull().default(sql`'{}'::text[]`)` in [`prompt-schema.ts`](./drizzle/schemas/prompt-schema.ts) and [`skill-schema.ts`](./drizzle/schemas/skill-schema.ts).
   - Created migration [`0053_multi_scope_prompts_skills.sql`](./drizzle/migrations/0053_multi_scope_prompts_skills.sql) to backfill existing values into arrays and create GIN indexes.

2. **Validation & Server Actions**:
   - Updated [`schemas/prompt/prompt.ts`](./schemas/prompt/prompt.ts) and [`schemas/skill/skill.ts`](./schemas/skill/skill.ts) to accept `projectIds: z.array(z.string()).default([])` and `assistantIds: z.array(z.string()).default([])`, removing legacy mutual exclusion.
   - Updated ownership validation and mutation logic in [`actions/prompts/`](./actions/prompts/) and [`actions/skills/`](./actions/skills/).
   - Updated runtime matching in [`load-chat-context.ts`](./lib/chat/load-chat-context.ts) and [`register-skill-tool.ts`](./lib/chat/register-skill-tool.ts) with `ANY(project_ids)` / `ANY(assistant_ids)` and cardinality checks for global availability.

3. **UI Components & UX**:
   - [`EntityMultiSelect`](./components/shared/entity-multi-select.tsx): Shadcn Popover + Command multi-select with search, select/clear all, and removable badge chips.
   - [`ScopeConfigurator`](./components/shared/scope-configurator.tsx): Two-mode selector ("Global" vs "Scoped") with concurrent multi-project and multi-assistant pickers and active item counters.
   - Integrated into [`PromptScopeTab`](./components/prompt/prompt-scope-tab.tsx), [`PromptForm`](./components/prompt/prompt-form.tsx), [`SkillScopeTab`](./components/skill/skill-scope-tab.tsx), and [`CreateSkillForm`](./components/skill/create-skill-form.tsx).
   - Updated [`PromptCard`](./components/prompt/prompt-card.tsx) and [`SkillCard`](./components/skill/skill-card.tsx) badges to display multi-project and multi-assistant assignment counts.
@mbeps
mbeps merged commit e199530 into main Nov 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant