| name | agent-copy |
| description | Add the canonical single icon-only copy/export control to any surface that shows data (rows, cards, lists, detail/record pages) using `components/agent-copy`: one CopyForAiIcon menu containing Copy, Copy JSON, Copy for AI, AI variants, downloads, destinations, and any custom/Groomer workspace. Build payloads as WHAT THE USER SEES (rendered view, live form state, errors, KPIs), sized to the data. Use when wiring copy actions, consolidating duplicate Copy/JSON/AI/Groomer controls, continuing the app-wide rollout, reviewing or upgrading a Copy-for-AI payload, or extending agent payload context. Triggers on `components/agent-copy/**`, `CopyButtons`, `AiCopyMenu`, `AgentCopyGroomerLauncher`, `buildAgentPayload`, "add copy buttons", "copy this row/list/record", or "copy for AI/agent". NOT for the live-chat message bar (`AssistantActionBar` / `messageActionRegistry`) or markdown content actions (the `rich-document-actions` skill). |
agent-copy — copy data (human + AI) anywhere
A reusable primitive for putting Copy, Copy JSON, Copy for AI, and
export actions behind one icon 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
/Users/armanisadeghi/code/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.
🚨 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_changes diff 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 live url, route, copied-at + your
location/description/context) and a <data format="json"> body.
attributes carry counts (rows, blockers, total_messages) — the
payload self-describes so a future agent can decide what to fetch.
<CopyButtons> — the UI. Renders one responsive action control, owns clipboard (with
legacy fallback) + success toasts + click-propagation stopping. You pass
human (readable text) and agent (an AgentPayloadInput, a prebuilt
string, or a builder fn) + a label. Sizes: "xs" (h-5 — dense items,
metric cards, per-field), "icon" (h-7 — rows/cards), "sm" (larger header
target). Every size is icon-only. Pass human/agent as 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 one compact top-level control: the canonical
CopyForAiIcon
opens one menu ordered Copy → Copy JSON → Copy for AI → shaped AI variants →
downloads/destinations. Large or visibly labeled top-level copy buttons are
banned. Scalars skip JSON. Copy-for-AI is NEVER just JSON in an envelope.
CopyButtons is the one control — pass export for downloads and
destinations, hide to drop any category (cards: omit export or
hide={["export"]}). A menu item may
onSelect / modal instead of copying. Do not also render ExportMenu
beside it.
CopyForAiIcon is 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.
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) | one icon; direct only when exactly one action exists | Copy + JSON when structured + faithful AI |
| Medium (focused list, digestible page) | one icon menu | Copy + JSON + faithful AI + 2–5 shaped variants |
| Massive (can reach ~10k+ chars: conversations, big tables, multi-section pages) | one icon 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 via agentVariant
(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.ts module — never in the chrome, never inline at callsites. Derive
preset variants from an existing section list (Backlinks does
applyGroomerPreset over its groomer sections — never a second list).
takes / for the toolbar
view copy.
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 unified 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 +
buildPanelView in AccessPlannerImpl.tsx: what-I-see panel payload from
LIVE form state with unsaved_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";
<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 control 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 single-icon action menus, a toolbar this-view menu
(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.
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 control, Groomer inside the menu
Page header gets one icon-only CopyButtons control. Its 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 in features/*, not the page. Wire it in the feature component so
admin AND user surfaces both benefit. (Quick check: wc -l the 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
human summary in the feature's format.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 by list.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 clear location (include the route), and useful
attributes/context (counts + the page KPIs).
- Run the acceptance test (payload vs screenshot), then
pnpm exec tsc --noEmit the touched files; commit per page/component.
Module-audit protocol — sweep a feature BEFORE wiring
When assigned a whole feature/module (not one page), do the coverage audit
first and emit the gap list; only then wire, batch by batch:
- Enumerate surfaces. Routes (
app/** for the feature — remember thin
wrappers delegate to features/*), window panels, overlays/dialogs that
show data, and demo routes. The feature's /[feature]/admin map and
FEATURE.md are the fast index; grep for .map( in its components to
find every rendered list.
- Classify each rendered data element as one of: list/table (needs
row control + view menu with export), record/detail (header control +
per-field), field group / metric cards (hover-reveal
xs controls),
whole page (one control with Groomer inside its menu when multi-section), or non-record
tool (composer/visualizer — SKIP, no forced buttons).
- Size each one's AI control (single icon /
aiVariants dropdown /
+aiCustom composer) per the sized-to-data table above, and note truncated
lists that lack a show-all — those are defects, list them.
- Audit EXISTING payloads against the MISSION. A wired surface whose
payload is a raw dump, reads saved rows instead of live state, or misses
rendered errors/KPIs is a defect even though the buttons exist — list
these too.
- Emit the coverage table (surface → element → class → current state →
planned control) in your summary/handoff BEFORE writing code, then wire in
per-page commits using the step-by-step above.
Pitfalls (these will bite you)
- Clickable rows: if the
<tr>/row has an onClick (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. See AiModelTable RowActions and the invitation-requests
cell for the pattern.
- Don't reinvent the envelope. The agent flavor is
buildAgentPayload only.
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 (update this as you go)
Done:
- Primitive + README + roadmap (
components/agent-copy/), xs size, groomer
window (AgentCopyGroomerWindow + groomer-types.ts) routed from the one AI
menu; a separate launcher beside the control is forbidden.
- Graded Copy-for-AI variants:
AiCopyMenu, CopyButtons.aiVariants,
CopyButtons.aiCustom, MatrxDataTable copy.aiVariants/aiCustom, shared
clipboard.ts, plus buildGroomerPresetPayload / groomerPresetVariants
(groomer-types) and keyFieldsAiVariant (marketing copy-payloads.ts).
Wired on the medium/massive marketing site tabs (keywords, ranks, findings,
analysis, audit, links, crawls, discovery, cost + backlinks reference);
small bounded tabs deliberately keep the plain single control — the sized-to-data call
is part of the job. AiCopyMenu remains in step with aidream
apps/dashboard/src/components/agent-copy/AiCopyMenu.tsx.
- Built-in integrations:
MatrxDataTable copy config → row/view/window/field
menus; DataRowInspector per-field hover copy; JsonInspector agentCopy.
- Shared formatters:
lib/sandbox/format.ts, features/ai-models/format.ts,
features/marketing/components/backlinks/format.ts,
features/sharing/format.ts.
- Sharing / access (2026-08-15) — wired in
features/sharing/*, so every
surface that renders sharing UI gained copy at once (ShareModal and its
window, file info, notes, RAG data stores, agent share panel, marketing site
access). PermissionsList: per-grant and list controls with export
(JSON + CSV) over ALL grants, ShowAllToggle above a 12-row preview, and a
copyable empty state. AccessSummaryPanel: panel + per-reason controls,
reason CSV export, and a payload for its ERROR branch (a failed reachability
read means UNKNOWN, never "private"); its reason rows now come from the
shared accessReasonRows extractor that the view itself renders, so copy
cannot drift from screen. PublicAccessTab declares its rendered sentences
once and both renders and copies them. The share tabs stay composers — no
forced record buttons — but their rendered failures are copyable with LIVE
form state. SiteAccessWorkspace adds the page unified menu, threads
one (identity + KPIs) into every child, and offers an
"Errors & access blockers" variant beside the what-I-see default.
is the what-I-see KPI mirror here, the analogue.
Law-compliant FORM surfaces (audited/wired 2026-08-15 — the form sweep):
These are verified against THE WHAT-I-SEE LAW: payload built inside the click
handler from LIVE inputs, explicit unsaved_changes diff vs the saved record,
rendered validation/error text captured verbatim, and the page's leading strip
carried in the body AND envelope attributes.
agent-apps/route/AgentAppSettingsContent — six staged fields + dirty diff,
rate-limit validation verbatim, commit-on-change controls reported as saved,
tabs made controlled so the payload names the open tab.
administration/agents/agent-apps/edit/[id] — header pair carrying the
Analytics card KPIs verbatim; flags when a draft-holding dialog is open.
agent-apps/components/UpdateAgentAppModal — live drafts + the rendered
text-destructive error.
agent-apps/components/AgentAppAdminActions — rate-limit editor drafts.
feedback/components/FeedbackDetailDialog — was the worst offender: it
dumped the fetched item while nine live controls and four unsent composers
sat on screen. Now sends live form values, the unsaved diff (mirroring
handleSaveDecision's own predicates), and the unsent drafts; the raw dump
is demoted to an "Everything" variant that states it excludes unsaved edits.
Header chips now ride on the per-comment/per-message section payloads too.
tool-call-visualization/admin/mcp-tools ToolEditPage / ToolCreatePage —
live draft, the red JSON Error: … text verbatim, and all three save
blockers with their toast copy.
- Shared builders live in
features/agent-apps/format.ts,
administration/users/feedback/format.ts, and the mcp-tools format.ts —
never at the callsites.
Two things the 2026-08-15 pass found that the brief had wrong, worth knowing:
the agent-apps edit page is under app/(admin)/administration/agents/…, not
app/(core)/…; and in tool-call-visualization/admin/mcp-tools only
ToolViewPage was ever wired — the two editors were unwired, not
raw-dumping. (The rollout list's "mcp-tools" refers to the separate
features/tool-registry tree.)
Known debt (still outstanding): surfaces wired BEFORE the MISSION section
existed carry raw-dump payloads that fail the what-I-see test — auditing them
is step 4 of the module-audit protocol. Any you touch is boy-scout territory:
upgrade the payload while you're there. The 2026-08-15 pass paid down the
FORM surfaces only; known remaining offenders, all raw data: record
dumps with no page KPIs in attributes:
agent-apps/route/AgentAppOverviewContent — data: { app, agent, variables, contextSlots }; the page renders a six-chip stat strip that no payload
carries. Its per-variable and per-context-slot section pairs also lack
parent context.
mcp-tools/ToolViewPage — data: tool (read-only, so the record largely IS
the view, but it carries no chips in attributes and mixes the live
isActive toggle with saved tool fields in one payload).
- The rest of the pre-2026-08-12 list above is unaudited.
Open per-item gaps on the version-diff page: its two lists render in
files outside that page — the History tab's rows in
features/agents/components/diff/VersionHistoryTimeline.tsx, and the
per-field diff rows in the shared components/diff/views/* (used by other
features). Both lists are covered today at whole-list granularity from the
page toolbar (a "Version history" variant + all-versions CSV, and the
changed-field list + CSV), but neither has a per-row pair yet. Wiring those
means editing the timeline and the shared diff views — do it deliberately,
and keep the shared views generic (take an optional copy config, don't
hard-code agent payloads into components/diff/).
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.