agent-copy
Compact two-icon Copy / Copy-for-AI controls (components/agent-copy). Use when adding copy buttons to a row, card, list, or record; merging duplicate Copy/JSON/AI controls; continuing the copy rollout
SKILL.md.claude/skills/agent-copy/SKILL.md
GitHubagent-copy — copy data (human + AI) anywhere
<!-- rules:stamp -->Rules from
common-docs/systems/platform/ui-shell/FEATURE.md— edit there, never here (python3 common-docs/meta/scripts/one_home.py --write).
- Delete means archive, everywhere. A delete that does not archive is a defect.
- A menu regroup ships as a side-by-side demo page with a what-goes-where list, never as a direct change.
- Every surface mounts the canonical menu. Use
NonEditableContextMenuorEditableContextMenufromfeatures/context-menu-v3/. Never use a bespokeDropdownMenuor a rawcontextmenuhandler, and never keep both. - A menu without a source is a defect. A menu with no
contentSource,entityorsurfaceNameopens with every verb dark. Fix these before unwired surfaces. - Lists built on
EntityListPageinherit the row menu throughItemContextMenu. They need their own wiring only for surface-specific sections. - A window or overlay mounts its own menu. It never delegates to the page under it.
- Reuse the domain's action registry. Never create a second one.
- Secret-bearing surfaces follow
VaultContextMenu. PassgetApplicationScope, forceselection: "", put names and field keys in the content (never values or notes), and pass noentity. - Done means the menu opens clean. The console shows no
INERT MENUorVALUE MAPPING GAPscream. - Chat is a universal agent host. Never bind a page-agent roster to it.
- Anything with an id shows it through the one canonical id component (display plus one-click copy).
- Flagship and first-run screens get a bespoke elevated stage, not the 28px app-chrome controls.
- Before a link goes to Arman, every screen on its path passes the Steve Jobs bar (walked and fixed by an Opus reviewer).
- Admin tables filter, search and sort on the server, and people are picked by name or email, never by id.
- Wasted vertical space is the worst sin; every status shown is backed by real state.
- A new output style or menu layout is a NEW function used only by the new path; never thin a shared builder.
- A page fetches only what it shows, each thing once; every removed fetch ships its on-engagement load.<!-- rules:stamp-end -->
Current UI (2026-09-27):
<CopyButtons>renders the package-owned Alchemy copy menu (MatrxCopyMenufrom@ai-matrx/alchemy/react/workspace, one icon since 2026-09-12). Where this skill says "two-icon pair", read "the canonical CopyButtons menu" — never hand-build two icons beside it. The payload rules below are unchanged.
A reusable primitive for putting Copy, Copy JSON, Copy for AI, and export actions behind a compact two-icon pair on any row, card, list, or record. It is the orchestration glue between raw page data and an AI agent: today it copies to the clipboard so a human pastes into an agent; the end state (see Roadmap) is the agent reading that context directly and acting on the page.
Source + full docs: components/agent-copy/README.md.
Sibling skill (doctrine twin): aidream
../aidream/.claude/skills/copy-for-ai/SKILL.md — its
cx-explorer implementation is the platform's best-of-breed reference; keep the
two skills and the two AiCopyMenus in step.
Not this primitive: the live-chat message bar (AssistantActionBar /
messageActionRegistry) or markdown content actions (the rich-document-actions skill).
Branches (read only when the run needs them):
- Assigned a whole feature/module, not one page → read module-audit.md before wiring.
- Before wiring or editing any existing surface, continuing the rollout, checking whether a surface is already wired, or paying down known raw-dump debt → read rollout-status.md, and update it as you go.
- Wiring any surface in files, image-manager, podcasts, audio, or pdf → read docs/handoffs/agent-copy-media-cluster.md first (no signed URL or storage path ever reaches a payload); research, rag, cms, or api-integrations → read docs/handoffs/agent-copy-data-knowledge-cluster.md first.
🚨 THE DOORS — every copy, download, export, format read/write and write-to-page (ALC-20)
There is ONE door per verb; app code never opens its own. A hand-built one is refused by pnpm check:alchemy-doors (:self-test proves it can fail; baseline scripts/alchemy-doors-baseline.json only shrinks).
- Copy an id or plain string:
@ai-matrx/kit/clipboard—useClipboardin a component,copyTextelsewhere. Nevernavigator.clipboard,execCommand,ClipboardItem. Copy of content:<CopyButtons>(the Alchemy copy menu). - Download a file:
@ai-matrx/kit/download—downloadFile/downloadUrl. Never a blob + anchordownload, neversaveAs. Content exports: the Alchemy menu's download action. - Read or write a format (xlsx, csv, pdf, html, markdown): the Alchemy
/operateengines (@ai-matrx/alchemy/operate,/operate/formats/*, wired incomponents/agent-copy). Never importxlsxexceljsjspdfhtml2canvasmarkeddompurifypapaparsedirectly. - Write to the page: the surface write door — a declared write target plus its handler (
../aidream/apps/shared/chat/src/surfaces/runtime/). Never callapplySurfaceWriteyourself. - Offer an action or menu item: the ONE Alchemy action registry (
createActionRegistry,@ai-matrx/alchemy/actions). No private action/handler map. Not actions, and so exempt: client directives (the server-to-client stream vocabulary) and surface-config namespaces.
A need the door lacks becomes a new option IN the package, never a call-site workaround.
🚨 THE MISSION — a Copy-for-AI is an AI context source, not a copy button
The user clicks it because they are getting AI help with what they are doing right now. Before writing any payload, answer: "what is the user doing on this page the moment they click this?" — then hand the agent exactly that.
- THE WHAT-I-SEE LAW (Arman, 2026-08-12, in anger): the PRIMARY payload is the rendered surface converted to data — never a raw record/snapshot dump. A payload that dumps 50k chars of adjacent data while missing the red error the user is staring at is a defect, not a copy button.
- Errors first. Blockers, warnings, red validation text — the exact sentences rendered — are the highest-value content. Capture them verbatim.
- Mirror the page's leading KPIs. If the page opens with a metric strip
("5 blockers · 3 own access · 1 nested"), every payload from that page —
including section/panel payloads — carries those same numbers, verbatim, in
the body AND the envelope
attributes. Nothing on the page is interpretable without what the page leads with, and the agent must never recompute what the user already sees. - LIVE state, never saved rows. Build the payload inside the click handler
from current inputs/drafts. A form-heavy page's form values ARE the payload;
copying the fetched row after the user edited a field is lying to the agent.
Include an explicit
unsaved_changesdiff vs the saved record. (Broke twice on 2026-08-12 alone: access planner, agent-settings.) - Mirror the content extractor. Reuse the view's own formatter/extractor so the export is what the user actually sees, not a parallel re-derivation.
- A section payload states what it belongs to. Specifics are only valid with their parent context (record identity + the page's KPIs) in the envelope.
- The acceptance test: put your payload beside a screenshot. Could an agent reconstruct what the user sees — every error, every KPI, the current form values? If not, it fails. Run this before reporting done.
30-second mechanics
buildAgentPayload(input)— pure util. Wraps any data in an xml-ish block with<context>(auto-injected liveurl,route,copied-at+ yourlocation/description/context) and a<data format="json">body.attributescarry counts (rows,blockers,total_messages) — the payload self-describes so a future agent can decide what to fetch.<CopyButtons>— the UI. Renders one responsive two-icon pair, owns clipboard (with legacy fallback) + success toasts + click-propagation stopping. You passhuman(readable text) andagent(anAgentPayloadInput, a prebuilt string, or a builder fn) + alabel. Sizes:"xs"(h-5 — dense items, metric cards, per-field),"icon"(h-7 — rows/cards),"sm"(larger header target). Every size is icon-only. Passhuman/agentas functions — resolved at click time.- Copy exists at EVERY granularity — field/entry, item, row, list, record,
page. Never display data the user can't copy. Dense surfaces hide the control
until hover (
opacity-0 group-hover/x:opacity-100 focus-within:opacity-100). - Exactly the canonical compact two-icon copy/export pair: one icon-only human Copy control plus one icon-only CopyForAiIcon menu,
ordered Copy JSON → Copy for AI → shaped AI variants →
downloads/destinations. Large or visibly labeled top-level copy buttons are
banned. Keep JSON inside the Copy-for-AI menu; never create a third top-level
control. Scalars skip JSON. Copy-for-AI is NEVER just JSON in an envelope.
CopyButtonsowns the pair — passexportfor downloads and destinations,hideto drop any category (cards: omitexportorhide={["export"]}). A menu item mayonSelect/modalinstead of copying. Do not also renderExportMenubeside it. CopyForAiIconis canonical.Sparkles,Sparkle, bot, face, star, or any substitute is banned for AI copy. Tooltips/accessibility names carry the words; visible Copy/JSON/Copy-for-AI text is forbidden.- Every data surface offers EXPORT (
ExportMenu+export.ts): lists and tables get JSON + CSV downloads; pages get their data JSON. Copy without export is half the job. - Truncated lists must offer the rest — a "top 8" with no show-all toggle is a defect; copy/export always cover ALL rows, not the visible slice.
Sized to data — most Copy-for-AI controls are DROPDOWNS
It is impossible to guess what the user wants to share when there is real data. Judge the size class for EVERY surface — a judgment call about the page's usage, never a global rule:
| Data | Control | Menu contents |
|---|---|---|
| Small / bounded (one record, short list) | two icons; AI is direct only when exactly one AI action exists | Copy + JSON when structured + faithful AI |
| Medium (focused list, digestible page) | Copy icon + AI menu | Copy + JSON + faithful AI + 2–5 shaped variants |
| Massive (can reach ~10k+ chars: conversations, big tables, multi-section pages) | Copy icon + AI menu + custom workspace | Copy + JSON + AI variants + downloads + tunable custom |
- A single button on a payload that can reach ~10k chars is a defect — thousands of tokens the user can't see or control.
- The default (plain click) is the what-I-see variant. Wire
agent= the focused rendered-view payload, label it viaagentVariant(position:"first"); the raw full dump is the "Everything" menu variant, never the default. - Variants are shaped by USAGE, not arbitrary slices. Think through what is non-optional on this page vs optional. Good menus: "Overview + counts" / "Overview + summaries" / "Transcript focus" / "Everything" (cx-explorer bundle); "Schema + sample" / "This view (md)" / "Column profile" / "Full rows" (cx-explorer tables).
- Custom composer levers, by data shape (offer the ones that fit — never blind truncation): format (Markdown/CSV/JSON/key-value) · row count (All/5/25/100/custom) · which rows (first/last/sample) · per-cell char cap · visible-columns-only · drop-empty-columns · stub-JSON-cells · include-nulls · schema header · strip binary — and for narrative data: per-message caps, include thinking / tool calls / tool results, last-N, model-visible-only. Multi-table bundles get a per-section level dial (Full / Trunc / Per-row / Summary / Counts / Off) with the overview header recommended always-on.
- Live char / ~token / byte counts in every dialog — the user must know
what they're getting into. The chrome (
AiCopyMenu) renders these free. - A stub is honest: it states what was omitted and how big it was, so the
agent knows to ask. Shortened variants are lossy in DATA, never in ambient
context — envelope
context+ KPIs identical across variants. - "With prompt" variants: where a payload has one obvious next action,
offer a sibling variant that wraps the faithful payload in an instruction
brief. Canonical: Error Inspector — "Error(s)" (
agentVariant, first) + "Error(s) with prompt" (lib/diagnostics/buildCapturedErrorPayload.ts: prompt before, payload in its own XML tag, reminder after). Same family: the dead-ends / lint-debt consoles' paste-ready repair-brief buttons (features/admin/*/fix-prompt.ts). - Shortening logic lives in pure per-data builders
(
(data, opts) => { text, ...counts }, no React) in a*AiSources.ts/copy.tsmodule — never in the chrome, never inline at callsites. Derive preset variants from an existing section list (Backlinks doesapplyGroomerPresetover its groomer sections — never a second list).MatrxDataTabletakescopy.aiVariants/copy.aiCustomfor the toolbar view copy. - The Groomer is an item inside the page-level Copy-for-AI menu. It
opens a WindowPanel where the user grooms the whole-page payload: sections with
full/compact/brief/offdials, Everything/Balanced/Minimal presets, live size (chars + ~tokens), live preview. Full contract in the README. The page-level "everything" is composed from the per-section builders, never a parallel implementation. Passgroomer={getGroomerConfig}toCopyButtons. Never renderAgentCopyGroomerLauncherbeside it. - The "Copy for AI" button is a deliberate seam: when the surfaces-registry
- tool-injection layer lands, it flips from "copy to clipboard" to "hand
context + callable actions to the agent" and every existing callsite comes
along for free. Keep
kindslugs stable andattributesmeaningful.
- tool-injection layer lands, it flips from "copy to clipboard" to "hand
context + callable actions to the agent" and every existing callsite comes
along for free. Keep
Exemplars — study before building, best first
- aidream cx-explorer (admin dashboard,
/cx-explorer) — the platform's best.apps/dashboard/src/components/agent-copy/:tableRowsAiSources.ts(generic any-table variants + custom export),conversationBundleAiSources.ts("Open composer…" per-table level dials),conversationAiSources.ts(transcript levers),conversationTranscript.ts(reference pure builder),features/cx-explorer/model-context/wire-ai-source.ts(honest stubs + mirrored metrics). Its payload shaping is the reference; consolidate its legacy separate JSON-copy control into the canonical Copy-for-AI menu. - Backlinks —
features/marketing/components/backlinks/BacklinksWorkspace.tsx: full-granularity page (cards + dimension lists + table + groomer), presets derived from the groomer sections. - Error Inspector — the "with prompt" sibling-variant pattern (above).
- Access planner —
features/admin/relationships/access-planner/copy.ts+buildPanelViewinAccessPlannerImpl.tsx: what-I-see panel payload from LIVE form state withunsaved_changes, blockers verbatim, KPI framing; full dump demoted to "Everything".
How to wire a surface (the whole job)
import { CopyButtons } from "@/components/agent-copy/CopyButtons";
// per-row / per-card — one compact two-icon pair:
<CopyButtons
size="icon"
label={`Sandbox ${row.sandbox_id}`} // used in toast + tooltip
human={() => summary(row)} // page/feature-specific readable text
agent={() => ({
kind: "sandbox-instance", // STABLE root xml tag/identifier
location: "AI Matrx Admin — Sandbox Management",
description: "A single sandbox instance row.",
data: row, // rendered-view data (see MISSION)
summary: summary(row), // optional <summary> block
attributes: { id: row.id, status: row.status },
})}
/>
// whole-list / whole-page — the same icon-only pair in the header/toolbar:
<CopyButtons
size="sm"
label="All sandboxes"
human={() => list.map(summary).join("\n\n")}
json={() => list} // becomes the second menu item, never another icon
agent={() => ({ kind: "sandbox-instances", location, description,
data: list, attributes: { count: list.length },
context: { filter, total } })}
/>
Tables: use the built-in config, never hand-wire rows
MatrxDataTable takes a copy config (rowKind/listKind/humanRow/…) and
delivers ALL of: per-row two-icon action pairs, a toolbar this-view pair
(Copy + JSON + AI + JSON/CSV/Excel downloads + Google Sheets), a record control
in the row-window header, and per-field hover controls in DataRowInspector
(side panel + window). Page KPIs,
warnings, and live filters that must survive a zero-row view go in
copy.listContext; mirror the same scalar counts in listAttributes and
rowAttributes. Also pass
window={{ title }} so rows open the record window.
Domain-specific row AI actions (for example a paste-ready repair brief) go in
copy.rowAiVariants; the table threads them into that same row menu in cards,
desktop rows, side panels, and windows. Never add an action column or another
copy control for the variant.
Page already has its own header row above the table? Set
copy.showToolbar: false and put one CopyButtons with export IN that row —
otherwise the table renders a near-empty toolbar row holding only copy icons
(the exact mess Arman flagged on backlinks). Table titles are user words
("Backlinks"), never internal ("Stored backlink rows"); counts are a subtle
muted tabular-nums beside the title, never a sentence pushing the search.
JsonInspector takes agentCopy for raw-JSON surfaces.
Whole-page: one pair, Groomer inside the AI menu
Page header gets one icon-only CopyButtons pair. Its AI menu contains
JSON, the what-I-see payload, Everything, shaped variants, and the Groomer
custom workspace whose sections mirror the page's areas. Declare sections once
and derive the quick payload from sections.build("full") — never maintain two
section lists or place another launcher beside the control. Supply the workspace
through groomer={getGroomerConfig} on that same CopyButtons.
Step-by-step
- Find where the list actually renders. Most
/administration/*pages are thin wrappers (9–25 lines) that delegate to a feature component — the.map()lives infeatures/*, not the page. Wire it in the feature component so admin AND user surfaces both benefit. (Quick check:wc -lthe page; <30 lines ⇒ it's a wrapper, go find the component it renders.) - Answer the MISSION question for this surface: what is the user doing here, what does the page lead with, where do errors render, what is live form state? That answer IS the primary payload spec. Then judge the size class per the table above.
- Add a shared
humansummary in the feature'sformat.ts/copy.ts(e.g.lib/sandbox/format.ts,features/ai-models/format.ts). Reuse it for both the row and the list. Never duplicate the summary across files. - Per-row: drop
<CopyButtons size="icon" …>in the row's action cell. - Whole-list: drop
<CopyButtons size="sm" …>in the toolbar/header, guarded bylist.length > 0. - Detail/record pages: one
<CopyButtons size="sm">in the header that copies the rendered record view — live URL + full CURRENT state is the highest-value capture. - Set a stable
kind, a clearlocation(include the route), and usefulattributes/context(counts + the page KPIs). - Run the acceptance test (payload vs screenshot), then
pnpm exec tsc --noEmitthe touched files; commit per page/component.
Module-audit protocol — sweep a feature BEFORE wiring
Assigned a whole feature/module (not one page) → read module-audit.md before wiring: enumerate surfaces, classify each element, size its AI control, audit existing payloads against the MISSION, and emit the coverage table before writing code.
Pitfalls (these will bite you)
- Clickable rows: if the
<tr>/row has anonClick(navigate/select), wrap<CopyButtons>in<span onClick={(e) => e.stopPropagation()}>(or put it in a cell that already stops propagation) so copying doesn't also select/navigate. SeeAiModelTableRowActions and the invitation-requests cell for the pattern. - Don't reinvent the envelope. The agent flavor is
buildAgentPayloadonly. Don't hand-roll xml or a JSON dump at the callsite (that anti-pattern is what this primitive replaced on the admin sandbox page). - Never remove a working feature. Consolidate duplicate AI buttons into ONE dropdown; never delete an existing copy / download / redaction affordance.
- Skip non-record surfaces. Tools/composers/visualizers (email composer, SQL workbench, schema visualizer, markdown tester, component demos) have no copyable record — don't force buttons there. Copy belongs on lists & records.
- Don't overwhelm. Favor per-row + copy-all on lists; a single whole-record copy on detail pages. More than that clutters.
size="icon"is h-7 w-7; if a row uses denser actions (h-6) it'll be a hair larger — acceptable, don't fight it with overrides.
Rollout status
Continuing the rollout, checking whether a surface is already wired, or touching a known offender → read rollout-status.md (what is done, law-compliant form surfaces, known raw-dump debt, open per-item gaps) — and update that file as you go.
Roadmap — from "copy" to "connect"
The full vision lives in components/agent-copy/README.md:
page-level state capture, integration with the surfaces registry
(features/surfaces/, see the surface-authoring skill), automatic screenshots
(hooks/useScreenCapture.ts), and dynamic tool injection (register a page's
state + callbacks so an agent can call them with args). Keep kind/attributes
stable now so they become the tool vocabulary later. Extraction stays a pure
function of (data, options) so AI features can call the same builders with no
clicking; payloads self-describe (summary + counts) so a future agent can fetch
just the slice it wants.