context-assignment
Context and scope-tagging UI. Use when adding a context picker to a surface, tagging an entity to scopes, setting or filtering by the active (working) context, showing per-entity context status, promp
SKILL.md.claude/skills/context-assignment/SKILL.md
GitHubContext assignment — picking the right component
The mental model (read this or you WILL wire it wrong)
- Membership, not ownership. A person belongs to MULTIPLE organizations,
all equal (no personal or business type —
common-docs/policies/access-ladder.md). Never assume one org per user. Every project and task belongs to exactly one organization (organization_idNOT NULL, live); a SCOPE assignment on them can be empty ("unassigned"), and that is normal, not an error. - The hierarchy: organization → scope types (the org's custom dimensions, e.g. Clients, Matters) → scopes (instances, e.g. "Acme Corp") → context items (typed fields per scope). Tasks may live in projects but DON'T have to; projects may live in orgs but DON'T have to.
- A scope NEVER implies its organization. Org is an independent context
dimension — it is part of a selection only when explicitly checked.
Display follows the same rule (
ContextSummaryChipsenforces it). - Three runtime layers — NEVER conflate them (full table:
../common-docs/systems/data/scopes-context/STATE.md→ "Runtime context — three layers"):- Layer A — Active (working/passive) context = "what I'm doing right
now"; ephemeral; lives in
appContextSlice; feeds every agent run automatically (execute-instance stamps it). MULTI-scope (keyed by scope id since 2026-06-12), single org/project/task, plus independent active scope types (active_scope_type_ids, 2026-06-30 — "whole dimension in play, no instance chosen"). Writers MUST be Surface A (ESLint-gated; the write actions includesetActiveScopeTypes). - Layer B — Reference tree = what exists (org→type→scope→item). Cache
only; fetched once at boot. Being consolidated onto the hierarchy owner
— read scope data via hierarchy selectors, do NOT reach for the fragmented
scopeTypes/scopes/… fan-out slices in new code. - Layer C — Durable object assignment = "this entity belongs to these";
persisted in canonical
platform.associationsvia thesetEntityScopeschokepoint, from the user's explicit UI selection. (The legacyctx_scope_assignmentstable is RETIRED — FE cut over 2026-06, zero DB functions reference it; never write to or read from it.) - A user ACTION (Layer C) must read the explicit selection, never Layer A
just because it's loaded. A successful write that sourced Layer A is worse
than a failure — it silently mis-binds. Never auto-convert active → durable
(suggest only). The condemned
agent_surfacebinding service (features/surfaces) is the cautionary example of doing this wrong.
- Layer A — Active (working/passive) context = "what I'm doing right
now"; ephemeral; lives in
Component selection table
| You need… | Use | Mode/notes |
|---|---|---|
| Tag an entity (file/note/agent/…) inline on a page | ContextAssignmentField |
mode="assignment", pass subject; use dimensions={["scopes"]} when the surface is scope-only (no project/task FK intent) |
| Same, without blocking the page | ContextAssignmentPopover |
trigger = your button |
| Same, as an explicit modal step | ContextAssignmentDialog |
controlled open |
| Same, floating/draggable | ContextAssignmentWindow |
inline-controlled |
| Set the WORKING context anywhere (header, toolbar, rail, composer) | ActiveContextLensChip |
THE one scope control; iconOnly for rails, attention for "needs a scope", fill for sidebars, onOpenPreview adds the preview eye |
| Same field inside a tab panel / drawer (no trigger) | ActiveContextPanel |
composes ContextAssignmentField mode="active"; default checkboxVariant="standard"; used by RunControlsMenu Context tab |
| Clear working context (rose Eraser + "Context", app-wide) | ClearContextButton |
dispatches clearContext; wired into every Surface A control |
| Show context status per entity (amber/green nudge) | ContextStatusButton |
pass knownScopeCount on list rows (bulk!), omit on single-entity surfaces |
| Display a selection readably | ContextSummaryChips |
compact chips for headers; org only if explicit |
| Labeled active/filter breakdown in the field footer | ContextSelectionSummary |
Org · Scope Types (group) · per-type scopes · Projects · Tasks |
| Prompt for context during an upload | UploadContextPrompt |
Save awaits awaitFileIds() — handles both races |
| Filter a list by context (no saving!) | ContextAssignmentField mode="filter" |
emits via onSelectionChange; zero save-side effects |
All in features/scopes/components/context-assignment/ except
ActiveContextLensChip + ActiveContextPanel (features/scopes/components/active-context/ —
Surface A writers MUST live there; ESLint + FEATURE.md enforce it).
Hard rules
- Fetch discipline. The org/type/scope tree is fetched ONCE at boot into
Redux (
ensureScopeTree→state.scopesTree) and refreshed only byscopeTreeInvalidationMiddlewareon structural mutations. Components NEVER refetch it. It is durably warm-cached across hard reload by the sync engine (scopesTreePolicyinfeatures/scopes/redux/scopesSlice.ts): a warm boot rehydrates the tree before paint andensureScopeTree()skips the network entirely (nostaleAfter/remote.fetch— the tree changes rarely). Projects/tasks/items go throughcontext-assignment/data.ts(60s TTL + in-flight dedup). List surfaces usegetEntityScopesBulk/primeEntityScopes— N rows must never mean N requests. Add new reads todata.ts, never to a component. - Write paths. Scopes →
useEntityScopes().setScopes/setEntityScopesthunk only. Active context → dispatch fromactive-context/components only. Entity FKs (e.g. a note's project_id) → that feature's save pipeline, applied fromonSaved's selection (seefeatures/notes/components/NoteContextSection.tsxas the template). - Dimension gating. Pass
dimensions={["scopes"]}(or any subset of"scopes" | "projects" | "tasks") to hide sections that do not apply — e.g. project settings (scope tagging only) vs notes (scopes + project/task FK intent). Default is all three. Org dropdown (assignment) and org rows (active/filter) are independent ofdimensions. - Org default-but-changeable. Assignment mode defaults to "All
organizations" (
ALL_ORGSsentinel — nothing filtered, scope sections grouped per org,selection.organizationId = null). Surfaces that "enforce" an org passinitialOrganizationIdto override that starting selection; the user can always switch (including back to All). - No layout shift. Fixed section heights; fixed-size check targets; status icons swap glyphs, never dimensions.
- Mobile = one bottom sheet. Every wrapper (
ContextAssignmentPopover,ContextAssignmentDialog,ContextAssignmentWindow,UploadContextPrompt,ActiveContextLensChip) switches toContextSheetonuseIsMobile()— never a desktop popover/dialog/window on a phone.ContextSheet(built on theBottomSheetprimitive) hosts the body withfill, which makes the field's own section list the single scroll area and pins the footer. Never set a fixedsectionHeightinside a sheet (that re-introduces nested scrolling) — passfillinstead. The field's org-of-record row stacks (flex-col sm:flex-row) so it never overflows a narrow screen. - Project/task durable links are LIVE via
associationsService.setTargets(platform.associations, replace-semantics) — the field's live save path andUploadContextPromptboth write them. Never reintroduce a log-and-toast stub. - Active context is MULTI-SCOPE — never radio. Any number of scopes per
scope type (Arman, 2026-07-07). Toggle with
addActiveScope/removeActiveScope(additive/surgical); never evict same-type siblings, and never read ascope_selectionsKEY as a scope_type_id — useselectActiveScopeIdsByType.
Live references
- Design surface / every variant demoed:
/demos/scopes/context-lab(app/(dev)/demos/scopes/context-lab/page.dev.tsx). - Real integrations to copy from: files table cell
(
features/files/components/surfaces/desktop/FileContextCell.tsx), upload prompt host (features/files/components/surfaces/PageShell.tsx), note adapter (NoteContextSection.tsx), header button (ChatRunHeader.tsx), run-controls tab (RunControlsMenu.tsxContext tab →ActiveContextPanel). - Current state:
features/scopes/FEATURE.md(the live SOR for scope/context on the FE).