Skip to content
KONIGI Patterns

AI Assistants / Memory and context / Scoped context

‹ 5 of 5

Scoped context

People need to know which files and workspace content the assistant can access.

Updated September 12, 2026

Problem

People cannot judge an answer if they do not know which files or workspace content the assistant could access.

Solution

Show a short list of the documents, projects, or datasets available for the current request. Let people add or remove items near the message field.

Start with the content relevant to the current task, such as the open document. Make it easy to expand that selection. Respect access permissions when listing and retrieving material.

Distinguish what the assistant could use from what it actually used in an answer. Show when the selection changes, especially after navigating to another part of the app.

Use when

Answers depend on which part of a workspace the assistant can access.

Don’t use when

The assistant has exactly one possible scope and no way to change it. A chip showing the only document that could ever be in play is noise dressed as control.

Trade-offs

Explicit selection adds work, but automatic selection can be hard to understand. Wider access can introduce irrelevant material and use more context space. Different permissions can give colleagues different answers, so the available material needs to be visible.

Checklist

  • Can the viewer see what’s in scope without asking?
  • Can they add and remove items in one action?
  • Is the default the narrow scope or the broad one?
  • Is scope bounded by the viewer’s own permissions, and is that stated?
  • Does the answer say which of the in-scope items it actually used?
  • Does scope persist across turns, and across navigation?
  • How does scope interact with the context budget?
  • What happens when an in-scope document changes mid-conversation?
  • Is scope carried into a shared conversation, and should it be?
  • Can two colleagues tell why they got different answers?

Compare

Notion composes scope through @-mentions in the prompt, so the working set is typed into the question and visible in the text itself. Claude uses projects as the unit, where a deliberately assembled set of documents is the scope and the boundary is something the viewer built. Microsoft Copilot scopes to the tenant with permission trimming. It has the broadest reach of anything here, and the per-person boundary becomes the defining design problem. Cursor shows the working set as an explicit, editable list of files, treating scope as something to curate rather than infer and assuming a user who wants that job.

Assistant sidebar often displays the selected context. Attachment tray adds external files. Command menu provides shortcuts for selecting items.

Source list identifies the material used in an answer. Context meter shows how much information fits in the current request.

Scoped context anatomy A working set shown as removable chips above the composer, bounded by the viewer's own permissions, with one item greyed as unreachable and a note that the same question gives two colleagues different answers. What it can see, as a list you can change seeing this page × Rivera project × + In scope Q3 forecast Rivera brief meeting notes, Aug 14 more pages Out of scope finance/, no permission the boundary is per person two colleagues ask the same question and get different answers, correctly 1 2 3 4 1 COMPOSABLE, NOT CONFIGURED The default is where the viewer is sitting. Changing it is a click, not a settings trip. 2 START NARROW Widening is easier to reason about than reining in, and it is cheaper. 3 BOUNDED BY PERMISSION The only defensible default, and it makes scope a property of the person asking. 4 TWO WRONG GUESSES Assuming too much trusts an answer from one file. Too little pastes in what it had. Broad scope gives better answers and makes them unreproducible, because nobody knows what was read.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

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.

shadcn
npx shadcn@latest add badge button
Tokens
--card--card-foreground--muted-foreground--border--status-warn--chart-1
seeing
this page
Rivera project

In scope

  • Q3 forecast
  • Rivera brief
  • meeting notes, Aug
  • 14 more pages

Out of scope

  • finance/, no permission

ScopeChips.tsxChips for the working set, the pages they resolve to, and the paths the viewer's own permissions keep out. The denied list is a required prop.

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 (
    <div>
      <div className="flex flex-wrap items-center gap-2">
        <span className="text-[11px] text-muted-foreground">seeing</span>
        {scope.map((s) => {
          const isHere = s.id === here;
          return (
            <Badge
              key={s.id}
              variant="outline"
              className={cn("gap-2 rounded-full py-1 font-normal", isHere && "border-chart-1 bg-chart-1/10 text-chart-1")}
              title={isHere ? s.pages[0] : `${s.pages.length} pages`}
            >
              {isHere ? "this page" : s.label}
              <button
                type="button"
                onClick={() => onRemove(s.id)}
                className={cn("leading-none", !isHere && "text-muted-foreground")}
                aria-label={`remove ${s.label} from scope`}
              >
                ×
              </button>
            </Badge>
          );
        })}
        {/* Widening is one click, here, rather than a trip to settings. */}
        <Button variant="outline" size="sm" className="h-7 rounded-full px-2.5 text-[11px] text-muted-foreground" onClick={onAdd} aria-label="add to scope">
          +
        </Button>
      </div>

      <div className="mt-4 grid grid-cols-2 gap-5">
        <div className="rounded-lg border border-dashed p-3">
          <p className="text-[11px] uppercase tracking-wide text-muted-foreground">In scope</p>
          <ul className="mt-2 text-[11px] text-card-foreground">
            {pages.slice(0, listed).map((p) => <li key={p} className="py-1">{p}</li>)}
            {rest > 0 && <li className="py-1 text-muted-foreground">{rest} more {rest === 1 ? "page" : "pages"}</li>}
          </ul>
        </div>
        <div className="rounded-lg border border-dashed p-3">
          <p className="text-[11px] uppercase tracking-wide text-muted-foreground">Out of scope</p>
          {denied.length ? (
            <ul className="mt-2 text-[11px] text-status-warn">
              {denied.map((d) => <li key={d} className="py-1">{d}, no permission</li>)}
            </ul>
          ) : (
            <p className="mt-2 text-[11px] text-muted-foreground">nothing you can open is excluded</p>
          )}
        </div>
      </div>
    </div>
  );
}

demo.tsxHow it is called: the open page plus a sixteen-page project, with one folder the viewer cannot read. Remove drops a chip; add appends one.

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 (
    <div className="rounded-lg border bg-card p-4">
      <ScopeChips
        scope={scope}
        here="page:q3-forecast"
        denied={["finance/"]}
        onRemove={(id) => setScope(scope.filter((s) => s.id !== id))}
        onAdd={() => setScope([...scope, { id: "page:figures", label: "figures spreadsheet", pages: ["figures spreadsheet"] }])}
      />
    </div>
  );
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

No screenshots yet for this pattern. Browse Products to see other examples.