Scoped context What the assistant can see, as a list you can change. The working set is bounded by the viewer's own permissions, so two colleagues asking the same question get different answers, correctly. npx shadcn@latest add badge button Tokens this needs: --card, --card-foreground, --muted-foreground, --border, --status-warn, --chart-1 The status, chart, scale, state and direction names are an extension, not a rename. shadcn has --destructive and five --chart-* and nothing else in this territory. ──────────────────────────────────────────────────────────────────────── // ScopeChips.tsx ──────────────────────────────────────────────────────────────────────── import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; /** One thing in the working set: a page, a project, a dataset. It carries * the pages it resolves to, so the list below the chips is computed rather * than asserted. */ export type ScopeItem = { id: string; label: string; pages: string[]; }; type Props = { scope: ScopeItem[]; /** Which item is where the viewer is sitting. It is the default scope and * the one chip that reads as the page itself. */ here?: string; /** Paths inside the scope the viewer's own permissions exclude. Required, * even when empty: scope is bounded by permission, and a component that * cannot say so leaves two colleagues arguing over different answers. */ denied: string[]; onRemove: (id: string) => void; onAdd: () => void; /** How many in-scope pages to name before collapsing to a count. */ listed?: number; }; export function ScopeChips({ scope, here, denied, onRemove, onAdd, listed = 3 }: Props) { const pages = scope.flatMap((s) => s.pages); const rest = pages.length - listed; return (
seeing {scope.map((s) => { const isHere = s.id === here; return ( {isHere ? "this page" : s.label} ); })} {/* Widening is one click, here, rather than a trip to settings. */}

In scope

    {pages.slice(0, listed).map((p) =>
  • {p}
  • )} {rest > 0 &&
  • {rest} more {rest === 1 ? "page" : "pages"}
  • }

Out of scope

{denied.length ? (
    {denied.map((d) =>
  • {d}, no permission
  • )}
) : (

nothing you can open is excluded

)}
); } ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { useState } from "react"; import { ScopeChips, type ScopeItem } from "./ScopeChips"; /** * The viewer is on the Q3 forecast page and has added the Rivera project, * sixteen pages of it. The finance folder is in the project, and this viewer * cannot read it. */ const START: ScopeItem[] = [ { id: "page:q3-forecast", label: "Q3 forecast", pages: ["Q3 forecast"] }, { id: "project:rivera", label: "Rivera project", pages: ["Rivera brief", "meeting notes, Aug", ...Array.from({ length: 14 }, (_, i) => `Rivera page ${i + 1}`)], }, ]; export default function Demo() { const [scope, setScope] = useState(START); return (
setScope(scope.filter((s) => s.id !== id))} onAdd={() => setScope([...scope, { id: "page:figures", label: "figures spreadsheet", pages: ["figures spreadsheet"] }])} />
); }