Skip to content

feat(tool-cards): render the element page/extension tools acted on - #348

Open
omridevk wants to merge 22 commits into
mainfrom
feat/tool-cards
Open

feat(tool-cards): render the element page/extension tools acted on#348
omridevk wants to merge 22 commits into
mainfrom
feat/tool-cards

Conversation

@omridevk

@omridevk omridevk commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Works toward #344. Draft on purpose — not mergeable yet. The element-capture half is still to come and lands on this same branch.

Every page.* call rendered as raw JSON, because pageActionTool matched conciv_page — a tool name #284 replaced with 37 page.<verb> tools. ToolCallCard matches on part.name, nothing matched, so everything fell through to ToolFallback.

Landed so far

Dispatch. ToolCallCard resolves extension card → builtin card → MetaToolCardToolFallback. The extension layer already existed end to end (ToolBuilder.rendercollectToolRenderers → chat pane) and had no callers; layer 3 is new. ToolFallback now only serves tools that declare no meta at all — i.e. foreign harness tools.

The wire. ToolViewMeta gained category, hint, positional, approval, inputSchema, outputSchema, errors, so a card can be built from a declaration alone. input/output were renamed to inputSchema/outputSchema so the widget's structural assignment needs no adapter.

MetaToolCard in @conciv/ui-kit-chat, rendering from meta alone and naming no tool: label → title by state, positional → headline argument, summary/hint, icon, category → accent, remaining input fields → chips, mutating → writes badge, mirrors → "shown on your page", errors → the declared message, outputSchema → result view. schemaParams and tool-icon moved into ui-kit-chat (the card needs them; the other direction is a cycle).

Declared errors are read from the structured payload. errorReply (core/src/api/mcp.ts) already emits {error: {message, name?, code?}}; the card now reads error.code and matches it against the tool's declared errors instead of splitting the "CODE: message" string. That string packing exists only to cross the code-mode sandbox boundary and stays.

PageActionCard / pageActionTool deleted. Result views survive as page-result-views.tsx primitives (A11yNodeList, PageHtmlBlock, PageValueChip) for the per-verb cards.

PermissionCard renders again for tools reaching the first three layers. The Show stays — ToolFallback draws its own approval buttons and would otherwise double the prompt.

nowTitle reads label.running from the live registry catalog before its hardcoded table. The table remains for foreign harness tools (Bash, Edit, Read, Grep, TodoWrite), which no extension of ours declares.

ToolAccent deleted — dead, and its members never matched the category values tools actually declare.

Two UI landmines found on the way

Silently dead UnoCSS classes. A quoted attribute selector inside an arbitrary variant, combined with an arbitrary property, generates nothing:

OK    [&_[data-part="item"]]:flex                       quoted + standard utility
FAIL  [&_[data-kind="key"]]:[color:var(--chat-text-3)]   quoted + arbitrary property
OK    [&_[data-kind=key]]:[color:var(--chat-text-3)]     unquoted + arbitrary property

Half the JsonTreeView styling was dead on arrival — no syntax colours, no hover. Nothing in typecheck, lint or tests catches this; it only shows up as something looking wrong on screen. A lint rule for the pattern is worth considering separately.

Ark anatomy styling. JsonTreeView.Tree renders its own parts, so you cannot put a class on each one and descendant selectors are the only hook — which had produced a 2000-character class string. It is now a rule in presetConciv emitting plain nested CSS. transformerVariantGroup was the alternative, but transformers are top-level config only and cannot ship from a preset, so any consumer that forgot to enable it would get silently empty CSS — the same failure class as above. Rules ship with the preset.

Also dropped the row border-radius: 8px on a 20.5px row rendered every hovered row as a lozenge and read as scalloped notches between rows.

Still to come on this branch

  • Verify tool call ids survive a transcript reload — every capture is keyed by that id
  • Element capture in @conciv/page (rrweb node + ancestor skeleton + shared CSS bundle, password masking at serialize time)
  • toolCaptures storage, so captures reach the transcript UI and never the model
  • ElementCapture in protocol and <ElementPreview> in ui-kit-chat
  • Page extension packaging and its own cards via .render()
  • Move conciv_ui / conciv_open / conciv_extensions cards to packages/tools, execute_typescript to core

Gates

19/19 test tasks (99 ui-kit-chat, 199 storybook), typecheck 92/92, lint 0 errors, format clean, fallow audit verdict pass.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added rich tool cards for page actions, reads, edits, console output, React inspection, effects, code execution, and UI prompts.
    • Added before-and-after element previews, accessibility views, HTML formatting, result chips, status indicators, and structured error messages.
    • Added interactive choices, confirmations, diffs, and forms with answer submission.
    • Added capture history that persists across reloads and can be refreshed during a session.
  • Bug Fixes

    • Sensitive fields are masked, and captured content is sanitized before display.
    • Improved fallback rendering for unknown or malformed tool results.

omridevk and others added 2 commits August 8, 2026 19:30
Every page.* tool rendered as raw JSON: pageActionTool matched the
name conciv_page, which PR #284 replaced with 37 page.<verb> tools.

Widen ToolViewMeta to the full declared ToolMeta set and carry
inputSchema/outputSchema/errors/approval through the catalog wire, so a
card can be built from a declaration alone. Add MetaToolCard in
ui-kit-chat beside the ToolFallback it replaces; ToolCallCard now
resolves extension card -> builtin card -> meta card -> ToolFallback,
and renders PermissionCard for anything that reaches one of the first
two layers. Delete PageActionCard, keep its result views as reusable
primitives for the per-verb card families.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change replaces legacy page and UI tool cards with package-owned registries and metadata-driven fallback cards. It adds sanitized element captures with CSS deduplication, database persistence, session retrieval, unified answer submission, expanded tool metadata, and Solid/Vite packaging.

Changes

Tool cards and frozen captures

Layer / File(s) Summary
Tool contracts and execution
packages/protocol/*, packages/contract/*, packages/extension/*, packages/core/src/page-bus.ts, packages/page/*
Tool signatures expose schemas, errors, approval data, and capture modes. Page execution returns optional capture bundles.
Capture storage and session retrieval
packages/db/*, packages/core/src/api/rpc/*, apps/conciv/src/pane/*
The database stores before/after captures and deduplicated CSS bundles. RPC and app hooks load and refresh captures.
Package-owned card rendering
packages/ui-kit-chat/*, packages/tools/src/cards/*, packages/extensions/page/src/client/*, packages/core/src/cards/*
Extension cards take precedence over builtin cards. Metadata-driven cards handle declared schemas, errors, approvals, mirrors, and captures.
Host integration and validation
apps/conciv/*, packages/*/test/*, packages/*/vite.config.ts, packages/*/uno.config.ts
The app wires registries, capture lookup, and UI replies. Browser, integration, Storybook, and build configuration updates cover the new paths.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title identifies the main change: rendering cards for page and extension tools.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feat/tool-cards
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tool-cards

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds metadata-driven tool cards so declared page and extension tools no longer fall back to raw tool rendering.

Changes:

  • Adds MetaToolCard and layered card dispatch with approval support.
  • Expands registry metadata with schemas, errors, and approval details.
  • Removes obsolete conciv_page cards and preserves reusable page result views.

Reviewed changes

Copilot reviewed 30 out of 31 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/ui-kit-chat/test/tool-call-card-dispatch.browser.test.tsx Tests dispatch precedence and approvals.
packages/ui-kit-chat/test/schema-params.test.ts Tests schema field projection.
packages/ui-kit-chat/src/styled/tools/tool-call-card.tsx Adds metadata-driven dispatch.
packages/ui-kit-chat/src/styled/tools/meta-tool-card.tsx Implements generic metadata cards.
packages/ui-kit-chat/src/styled/tools/meta-tool-card.stories.tsx Adds card examples.
packages/ui-kit-chat/src/styled/tool-icon.tsx Moves shared tool icons.
packages/ui-kit-chat/src/primitives/tools/schema-params.ts Exposes structured schema fields.
packages/ui-kit-chat/src/index.tsx Exports new card primitives.
packages/ui-kit-chat-tools/test/schema-params.test.ts Removes relocated tests.
packages/ui-kit-chat-tools/test/registry-card-declarations.browser.test.tsx Updates declaration-card tests.
packages/ui-kit-chat-tools/test/page-tool-cards.browser.test.tsx Tests page metadata cards.
packages/ui-kit-chat-tools/test/page-action-card-title.browser.test.tsx Removes obsolete card tests.
packages/ui-kit-chat-tools/test/new-tool-projection.browser.test.tsx Updates projection coverage.
packages/ui-kit-chat-tools/test/catalog-cards.browser.test.tsx Tests metadata rendering behavior.
packages/ui-kit-chat-tools/src/styled/tools/tool-chip.stories.tsx Uses relocated schema helper.
packages/ui-kit-chat-tools/src/styled/tools/page-result-views.tsx Preserves page result primitives.
packages/ui-kit-chat-tools/src/styled/tools/page-result-views.stories.tsx Adds result-view examples.
packages/ui-kit-chat-tools/src/styled/tools/loaded-tools-card.tsx Uses shared schema helper.
packages/ui-kit-chat-tools/src/styled/tools/builtin-tool-cards.ts Removes stale page registration.
packages/ui-kit-chat-tools/src/styled/page-action-card.tsx Deletes obsolete page card.
packages/ui-kit-chat-tools/src/styled/page-action-card.stories.tsx Deletes obsolete stories.
packages/ui-kit-chat-tools/src/primitives/tools/now-title.ts Reads live catalog labels.
packages/ui-kit-chat-tools/src/index.tsx Updates public exports.
packages/protocol/src/tool-view-types.ts Expands tool view metadata.
packages/extension/test/tool-registry.test.ts Updates renamed schema fields.
packages/extension/src/tool-registry.ts Renames catalog schema properties.
packages/core/src/chat/capabilities.ts Adapts capability signatures.
packages/core/src/api/rpc/router.ts Projects expanded metadata over RPC.
packages/contract/src/contract.ts Expands wire signature schema.
packages/cli/src/tool-command.ts Consumes renamed input schema.
docs/superpowers/specs/2026-08-08-page-tool-cards-design.md Documents the broader card design.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +74 to +78
function declaredMessage(raw: string, errors: readonly ToolViewError[] | undefined): string {
if (errors === undefined) return raw
const code = raw.split(':')[0]?.trim() ?? ''
const declared = errors.find((candidate) => candidate.code === code)
return declared?.message ?? raw
Comment on lines +131 to +133
<Match when={view() === 'code'}>
<ResultBlock name="result.txt" contents={props.raw} />
</Match>
await expect.element(page.getByText('do something the widget has no cosmetics for')).toBeVisible()
expect(document.body.textContent).not.toContain(GENERIC_PAGE_TITLE)
await page.getByRole('button').click()
await vi.waitFor(() => expect(codeBlockText()).toContain('shipped-42'))
omridevk and others added 3 commits August 8, 2026 23:08
…extracts them

The generic tool-result tree used quoted attribute selectors combined with
arbitrary CSS properties (e.g. [&_[data-part="x"]]:[color:var(--y)]); UnoCSS's
source extractor drops any class matching that exact combination, so half the
tree's classes silently produced no CSS. Dropping the quotes (valid since every
attribute value here is a plain identifier) fixes extraction.

Also tunes the tree for a compact chat card: per-depth indentation via Ark's
--depth var, single-line truncation on branch/item text so long values no
longer wrap and float the chevron, and hover/focus-visible affordances on rows.

Re-points the NestedListResult story at a generic diagnostics payload instead
of an accessibility-node shape, which is getting its own purpose-built
A11yNodeList view and was misrepresenting what this fallback renders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s string

Styling Ark's JsonTreeView means reaching its internal anatomy by
data-part, which it renders itself, so the component cannot put a class
on each part. That had produced one unreadable class string in the card.

Move it to a preset rule emitting plain nested CSS. Presets can ship
rules, so it travels with presetConciv() and needs no per-app config;
transformerVariantGroup would have been the other option but transformers
are top-level config only and cannot ship from a preset, which would
silently break any consumer that forgot to enable it.

Drop the row border-radius while here: an 8px radius on a 20.5px row
rendered every hovered row as a lozenge and read as scalloped notches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t beside the transcript

Page act and edit-live verbs now freeze the element they act on: an rrweb
serialization with its ancestor skeleton, a masked descriptor, and a hashed
page css bundle shipped once per page. Edit verbs capture both sides, act
verbs only the post-execute side.

The capture rides beside `result` on the page reply so nothing in a tool's
return value can carry it, and the server splits them: the result continues
to the harness untouched, the capture is written to `tool_captures` keyed by
the tool call id that code mode minted, with css text in `css_bundles` keyed
by hash. Both are dropped with the session.

The widget reads them back through `captures.list` and hands each card a
`capture` prop; rendering lands in a later slice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 36 out of 37 changed files in this pull request and generated no new comments.

Suppressed comments (5)

packages/ui-kit-chat-tools/test/catalog-cards.browser.test.tsx:69

  • This assertion reaches through the diffs-container implementation and its shadow root. Widget UI tests in this repository are required to use user-facing role/text locators instead of CSS or implementation-detail selectors; assert the rendered result through a text locator and remove this helper.
  return Array.from(document.querySelectorAll('diffs-container'))
    .map((host) => host.shadowRoot?.textContent ?? '')
    .join('\n')

packages/ui-kit-chat/src/styled/tools/tool-call-card.tsx:24

  • While the registry query is still loading, declared() is necessarily undefined, so an existing page.* call temporarily renders through ToolFallback (and can switch approval ownership when the query resolves). This contradicts the intended invariant that the raw fallback is used only after the loaded catalog confirms no metadata; the removed pending-catalog test covered this case. Treat an unloaded catalog as meta-card-owned until it resolves.
    if (declared()) return MetaToolCard
    return props.fallback ?? ToolFallback

packages/uno-preset/src/json-tree.ts:6

  • s is an abbreviated identifier, which this repository’s TypeScript conventions prohibit. Rename it to a self-explanatory name such as selector and update the template references.
    packages/ui-kit-chat-tools/src/styled/tools/page-result-views.stories.tsx:41
  • This story assertion depends on the diffs-container tag and its shadow-root implementation. UI assertions are required to use user-facing role/text queries so the test remains stable if the code-block implementation changes.
        Array.from(canvasElement.querySelectorAll('diffs-container'))
          .map((host) => host.shadowRoot?.textContent ?? '')
          .join('\n'),
      ).toContain('hero'),

packages/ui-kit-chat/src/styled/tool-card.tsx:40

  • The tooltip trigger is rendered as a non-focusable <span> inside Collapsible.Trigger (the actual button). Keyboard focus lands on the outer button, so Ark’s tooltip trigger never receives focus and the title tooltip is hover-only. Compose the tooltip trigger onto the collapsible button itself (or forward its trigger props to that button) so the same element supports hover and keyboard focus.
        <Tooltip.Trigger
          asChild={(triggerProps) => (
            <span {...triggerProps()} class={TITLE}>
              {props.title}
            </span>
          )}

omridevk and others added 17 commits August 9, 2026 02:34
…efore they can be overwritten

foldRichRunMessagesIntoHistory only folded a run into session_history
when history already existed or a part was image/document, so a
first-turn code-mode page.* tool call (no rich part, empty history)
stayed unflushed in run_messages and was silently overwritten the
moment a later turn's onMessagesChange fired for a transcriptHistory
harness — orphaning the stored element capture with no transcript
part to hang it off.

Stamp every synthetic code-mode TOOL_CALL_START chunk with an explicit
codeModeSynthetic marker at emission (code-mode-parts.ts) instead of
inferring from the optional parentToolCallId, since that field is only
populated when the ai-core tool executor auto-stamps a nested call and
isn't a dependable structural signal on its own. hasRichPart now treats
any tool-call part carrying that marker the same as image/document: the
CLI transcript can never reproduce it, so it must be folded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rt signal, not on stream end

withAutoApproval opened chat.subscribe and awaited its pump loop in a
finally block, relying entirely on the underlying transport to reject
the async iterator once abort() fired. That makes termination hostage
to whatever the SSE/fetch stack does with an in-flight read at the
exact moment of abort, instead of something the pump itself controls.

pumpApprovals now races each iterator.next() against a promise derived
directly from the AbortSignal, so calling abort() deterministically
ends the loop the instant the awaited call settles, independent of
transport behavior or how much unrelated traffic is still flowing on
the session's stream.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ElementPreview.Root/Frame/Descriptor rebuilds an rrweb-serialized
element capture into an inert shadow root and crops to the target's
box; MetaToolCard renders it (after, falling back to before) with a
descriptor-chip fallback when no node was captured. Adds the direct
rrweb-snapshot dependency (dedupes to the existing 2.1.0 copy) and a
shared Chip primitive reused by both the preview and the card.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fore rebuild/serialize

Both the capture path (packages/page/src/element-capture.ts) and the defensive
rebuild path (packages/ui-kit-chat/src/styled/element-preview.tsx) strip every
on* attribute and javascript: URLs (entity-decoded, with all control/space
characters removed before the scheme check — closes the java\tscript: and
javascript&colon; bypasses) from href/src/xlink:href/srcdoc/formaction, and
drop <iframe>/<object>/<embed> nodes entirely, recursively over the whole
serialized tree. UNSAFE_allowUnprotectedRebuild stays required for shadow-root
rebuild, but the payload is now inert by construction on both sides of the
wire, and element-preview.tsx clones the node before mutating so the
tanstack-query cache's object is never touched.

ElementPreview.Root now owns a loading/ready/failed status signal in context:
Frame hides (no role/aria-label/aria-busy) instead of staying an invisible
opacity-0 box when the node is missing its data-rr-target marker or fails to
rebuild, Descriptor takes over, and the loading state shows the sunken
box + shimmer instead of nothing. Crop scale is capped at 2x. MetaToolCard
composes Frame and Descriptor as unconditional siblings under Root and lets
context drive which one is visible.

session-captures.ts pins staleTime: Infinity — captures are immutable per
toolCallId, so window focus shouldn't re-fetch css bundles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copy the recorder extension's packaging exactly: client.ts becomes
client.tsx, tsdown builds the node-side entries (server, shared/defs)
while vite builds the externalized Solid client bundle. No card exists
yet, so client.tsx stays a trivial defineExtension() placeholder;
future .render() card work lands on this same entry.

Exempt vite.config.ts from fallow's duplication gate the same way
vitest.config.ts already is: three externalized-extension packages
now share the same ~10-line vite lib config shape, and extracting it
into a shared package would either bloat @conciv/vitest-config (used
by every package) or reintroduce a dependency cycle through
@conciv/extension-testkit -> @conciv/core -> @conciv/extension-page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Platform-ladder fix: crypto.subtle.digest('SHA-256', ...) replaces the
hand-rolled fnv1a hash for deduping shipped css bundles. Keeps the
"css" prefix convention consumers already match on (/^css/), just with
a real digest.

crypto.subtle.digest is async, so the design splits capture from
shipping instead of threading async through ClientToolCtx (which would
have forced every verb body in extensions/page/src/client/bodies.ts to
await ctx.target()/ctx.resolve()): takeElementCapture/collectPendingCss
stay synchronous and only collect raw css text; the makePageToolDispatcher
post-execute path (already async) hashes each pending capture's css and
runs the once-per-page dedupe there via buildCaptureBundle. bodies.ts
and ClientToolCtx are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ension

Move A11yNodeList/PageHtmlBlock/PageValueChip and page-format from
ui-kit-chat-tools into packages/extensions/page/src/client per the
colocation rule: page-specific presentation code lives with the page
extension, not in the shared tools ui-kit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add three tool-card families under packages/extensions/page/src/client/cards
(act, edit-live, read-value), composed from ui-kit-chat, ui-kit-system and
solid-diffs primitives. Lift the shared card presentation vocabulary into
packages/ui-kit-chat/src/primitives/tools/tool-presentation.ts and have
MetaToolCard consume it, so the page extension's cards and the generic
fallback card share one visual language.

Wire up the page extension's first real .render() registration and add a
browser test (apps/conciv/test/tool-card-dispatch.browser.test.tsx) that
proves dispatch precedence — a registered extension render wins over the
generic MetaToolCard fallback.

Add the page extension's storybook glob to apps/storybook/.storybook/main.ts,
an ignoreDependencies entry in .fallowrc.json for the new package, and update
the lockfile for the moved/new deps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lete the six families

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Before/After tab visibility checks raced Ark's collapsible open
animation, causing intermittent storybook failures. Wrap both in the
existing waitFor idiom already used elsewhere in this story file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Packages that define tools must be able to build inline tool cards without
depending on @conciv/ui-kit-chat-tools, which would close a package cycle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Slice 2E of #344 §1b. conciv_ui, conciv_open and conciv_extensions cards move
into @conciv/tools; the execute_typescript card moves into @conciv/core. Both
packages gain a browser export condition and a vite client build copied from
the page extension, and each exposes a ToolCardEntry[] the chat pane merges
ahead of builtinToolCards — no global registry, no side-effect imports.

conciv_open and conciv_ui declare their running title on the card entry they
now ship, so nowTitle's hardcoded table drops both. ui-kit-chat-tools keeps
only the foreign harness tool cards it legitimately owns.

The embed inlines @conciv/core alongside @conciv/tools, so embed consumers do
not have to resolve it, and the unocss content globs pick up the new card
locations plus the page extension's cards, which slice 2D never added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… meta

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DoneCard was only kept alive by its public re-export; no consumer
referenced it. Traced its schema (DoneCardSchema/DoneCard in
@conciv/protocol/done-types) and found it dead too, with its only
public exposure being the protocol package's subpath export, so
done-types.ts is removed along with its exports/tsdown entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…and card vocabulary

Element preview: only element nodes reach the rrweb rebuild (a serialized
Document node used to reach rebuild with the live document and wipe the
app via document.open), captured page css is confined by an outer frame
wrapper with contain:strict so :host!important rules cannot escape,
isCustom is stripped before rebuild so captures cannot define custom
elements on the app registry, and the crop re-runs through a resize
observer so a preview mounted in a hidden tab crops when it first gains
size instead of never. A capture-bundle failure in the page dispatcher
degrades to no capture instead of failing a tool call that already
succeeded. The tool-card title tooltip moves onto the collapsible
trigger button so keyboard and screen-reader users reach it.

Shared vocabulary stops being copied: CHIP, a styled JsonTree, NoteRow/
MirrorRow and ChipRow now ship once from ui-kit-chat and every card
imports them; the cards vite builds externalize the full recorder list
so @tanstack/ai is no longer inlined into dist/cards.js.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n card shape

Re-cropping measured transformed rects and compounded scale on every
tab round-trip; cropToTarget now measures the frame first (hidden-tab
resize events become no-ops) and resets the replica transform before
measuring, so every run is idempotent. The entrance pop was a
persistent class that replayed on each display toggle; it is now a
one-shot WAAPI animation on the loading-to-ready transition, guarded
for reduced motion. CollapsibleCard resolves its shape from resolved
children: body content renders the collapsible with chevron and
aria-expanded, no content renders a static header row with no button
and no dead tab stop, and streaming content upgrades the row in place.
That retires the empty-expand affordance on bodyless UiCard, todo,
search and tool-lookup cards through one shared mechanism. The
mutating badge reads edits page, defined once in the shared card
vocabulary. Ships the conciv_ui unification spec (addResult contract).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sult contract

Ported from assistant-ui: answering a human-in-the-loop question is
completing the pending tool call, not a side-channel capability. Every
tool card now receives a non-optional addResult(value) prop, bound at
the dispatch layer from the host's single ToolViewCtx.addResult wiring
(mirroring respondApproval); the app wires it once to its uiReply
mutation. The packaged UiCard absorbs the interactive
Choices/Confirm/Diff/Form internals from the app card, rendering the
pending question, a sending state that disables the controls, and an
answered summary derived purely from the tool result, so reload and
replay are correct by construction. The app's shadow card and dispatch
entry are deleted. Conformance rode along: ui-kit-system
Button/TextField/Ark Select (Select.Label completed) replace hand-rolled
controls, SolidFileDiff renders the diff kind, and the embed integration
test asserts the answered status region instead of the old literal.

Closes the conciv_ui dual-card finding from the branch review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@omridevk
omridevk marked this pull request as ready for review August 9, 2026 21:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 182 out of 186 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (6)

packages/page/src/element-capture.ts:108

  • The serialized target subtree has no size bound. Because mutating tools accept arbitrary selectors, targeting a large container such as body can persist an unbounded rrweb tree; every session load then returns all such trees through captures.list, causing database growth and large RPC/UI allocations. Enforce a serialized capture byte/node cap and fall back to the descriptor when exceeded.
    packages/page/src/page-tool-dispatcher.ts:147
  • For capture: 'after' tools, the only snapshot is attempted after execution. Common actions such as clicking a dismiss/submit control can detach or replace the target; takeElementCapture then rejects it because it is no longer connected, leaving the card with no capture. Retain a pre-action snapshot as a fallback and prefer the post-action snapshot when it succeeds.
    packages/db/src/capture-queries.ts:66
  • Deleting an unreferenced CSS row invalidates the page-side deduper. makeCssBundleDeduper remembers that hash for the lifetime of the connected page and will send only {hash} on later captures; after deleting the last referencing session, a new session on the same page can therefore store captures whose CSS can never be resolved. Either make deduplication acknowledgement/storage-aware or retain/re-request bundles when garbage collecting them.
    packages/page/src/page-tool-dispatcher.ts:113
  • PageCaptureBundle has only one cssBundle, but this loop can capture different stylesheets before and after an edit. The second new hash overwrites the first bundle while the before capture still references its old hash, and ToolCaptureView then applies one CSS string to both previews. Preserve every referenced CSS bundle and resolve CSS per capture side.
    packages/ui-kit-chat/src/index.tsx:196
  • These exports expose test/story fixtures from the production @conciv/ui-kit-chat entry point; repository guidance prohibits test code in product source. Move the fixture data to a testkit or Storybook-only module instead of publishing it as runtime API.
    packages/extensions/page/src/client/cards/shared.tsx:105
  • Every page verb now uses these custom cards, but this helper bypasses the declared-error resolver. It renders both the structured {error:{message,code}} form and the code-mode synthetic {error:"CODE: message"} form as raw JSON, so page cards never show the declaration's user-facing message. Normalize synthetic error chunks to the structured shape and share the metadata-aware resolver with these cards.

Comment thread packages/db/src/schema.ts
payload: text('payload', {mode: 'json'}).$type<ElementCapture>().notNull(),
createdAt: integer('created_at').notNull(),
},
(table) => [primaryKey({name: 'tool_captures_pk', columns: [table.toolCallId, table.kind]})],
Comment on lines +94 to +98
delete attributes[name]
continue
}
if (URL_ATTRIBUTES.has(lowered) && typeof value === 'string' && isJavascriptUrl(value)) delete attributes[name]
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🧹 Nitpick comments (8)
packages/ui-kit-chat/test/element-preview.browser.test.tsx (1)

180-203: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Exercise each hostile execution path.

This test does not click either link. It does not assert that onerror and onclick attributes are removed. It only checks the malformed java\tscript: URL.

A sanitizer regression that preserves a normal javascript: URL or an event handler can pass. Assert that these attributes are absent. Click the ordinary link and verify that XSS_FLAG remains unset.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui-kit-chat/test/element-preview.browser.test.tsx` around lines 180
- 203, Extend the test around the hostile payload in the existing element
preview case to assert that onerror and onclick attributes are absent from the
sanitized shadow content. Also locate the ordinary hostile link, click it, and
verify XSS_FLAG remains unset, while preserving the existing malformed
java\tscript: href assertion and safe-content checks.
packages/db/src/schema.ts (1)

43-53: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Add a non-unique session_id index and generate the migration.

sessionCaptures and deleteSessionCaptures filter by tool_captures.session_id. The composite primary key (tool_call_id, kind) cannot support these lookups, so SQLite scans the table. Declare index('tool_captures_session_id_idx').on(table.sessionId) and include the generated migration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/src/schema.ts` around lines 43 - 53, Add the non-unique sessionId
index to the toolCaptures table definition using the name
tool_captures_session_id_idx, then generate and include the corresponding
database migration. Preserve the existing composite primary key and all other
columns unchanged.
packages/db/drizzle/20260808214400_tool_captures/snapshot.json (1)

367-426: 🧹 Nitpick | 🔵 Trivial

Consider indexing tool_captures.session_id.

The snapshot declares only the composite primary key (tool_call_id, kind). Capture reload and session cleanup filter by session_id, which requires a full table scan without an index. Add an index in packages/db/src/schema.ts and regenerate the migration if those access paths exist. The same applies to css_bundles.session_id.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/drizzle/20260808214400_tool_captures/snapshot.json` around lines
367 - 426, Update the schema definition for tool_captures and css_bundles in
schema.ts to add indexes on session_id, supporting capture reload and session
cleanup queries; then regenerate the corresponding Drizzle migration/snapshot so
the indexes are represented in the database schema.
packages/page/test/element-capture.browser.test.ts (1)

202-227: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Also assert that no hostile handler executed.

The test proves the handlers are absent from the serialized capture. It does not prove the fixture payload stayed inert. img onerror and the iframe srcdoc script run when the fixture mounts in the host page, and a later regression in the fixture would go unnoticed. Add a direct assertion on the sentinel.

💚 Proposed extra assertion
     const serialized = JSON.stringify(node)
     expect(serialized).not.toContain('onerror')
     expect(serialized).not.toContain('onmouseover')
     expect(serialized).not.toContain('javascript:')
     expect(serialized.toLowerCase()).not.toContain('iframe')
+    expect(Reflect.get(window, '__xssCapture')).toBeUndefined()
   })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/page/test/element-capture.browser.test.ts` around lines 202 - 227,
Update the test case around captureOf and the hostile fixture to assert the
handler-execution sentinel remains unset, in addition to the existing
serialized-node checks. Verify the sentinel after the fixture mounts and capture
completes, covering both the img onerror and iframe srcdoc payloads.
packages/core/test/chat/approving-call-post-result-traffic.it.test.ts (1)

42-54: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the tool output and the latency the test name claims.

JSON.stringify(outcome) contains 'target' even if the outcome only echoes the input elementId. The test also states "resolves quickly", but only the 8 s timeout bounds the duration, and the noise stream lasts about 900 ms. Assert the removed field and a duration below the stream length.

♻️ Proposed stronger assertions
       const approvingCall = makeApprovingCallTool(kit.base, session)
+      const started = Date.now()
       const outcome = await approvingCall('canvas.delete', {elementId: 'target'})
-      expect(JSON.stringify(outcome)).toContain('target')
+      expect(Date.now() - started).toBeLessThan(NOISE_CHUNK_COUNT * NOISE_INTERVAL_MS)
+      expect(JSON.stringify(outcome)).toContain('"removed":"target"')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/test/chat/approving-call-post-result-traffic.it.test.ts` around
lines 42 - 54, Strengthen the test around makeApprovingCallTool by recording the
call start time, asserting the outcome’s removed field confirms the target was
deleted, and asserting the call duration is below the long-running stream
duration (about 900 ms). Keep the existing cleanup and timeout behavior
unchanged.
packages/embed/vite.config.ts (1)

7-7: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Inline only @conciv/core/cards.

@conciv/core resolves to the server entry, while the widget uses the browser-safe @conciv/core/cards entry. The current prefix also inlines @conciv/core/app and unrelated names such as @conciv/core-server. Use an exact cards-entry allowlist, and retain the @conciv/extension/*, Ark, and Solid externalization assertions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/embed/vite.config.ts` at line 7, Update INLINED_PREFIXES to remove
the broad `@conciv/core` prefix and allow only the exact `@conciv/core/cards` entry
for inlining. Preserve the existing `@conciv/page`, `@conciv/app`, and `@conciv/tools`
entries, along with the `@conciv/extension/`*, Ark, and Solid externalization
behavior.

Source: Coding guidelines

packages/extensions/page/test/defs.test.ts (1)

41-41: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the as const assertion.

MIRROR_VERBS is only used in a spread within an equality assertion. Literal tuple inference is not required, so the assertion is unnecessary and violates the TypeScript guideline that prohibits as.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extensions/page/test/defs.test.ts` at line 41, Remove the `as const`
assertion from the `MIRROR_VERBS` definition, leaving the array contents and
spread-based equality assertion unchanged.

Source: Coding guidelines

packages/ui-kit-chat/src/styled/tools/tool-call-card.tsx (1)

35-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The inherited capture prop is silently ignored.

ToolCallCardProps extends Omit<ToolCardProps, 'addResult'>, so it still declares an optional capture field. Line 35 always resolves the capture from props.ctx.captureFor. A caller that passes capture directly gets no effect and no type error.

Either prefer the explicit prop, or omit capture from the public props type.

♻️ Option 1: honour the explicit prop
-        capture={props.ctx.captureFor?.(props.part.id)}
+        capture={props.capture ?? props.ctx.captureFor?.(props.part.id)}
♻️ Option 2: remove it from the public surface
-export type ToolCallCardProps = Omit<ToolCardProps, 'addResult'> & {
+export type ToolCallCardProps = Omit<ToolCardProps, 'addResult' | 'capture'> & {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui-kit-chat/src/styled/tools/tool-call-card.tsx` at line 35, Update
the capture handling in ToolCallCard so the declared optional capture prop is
not ignored: prefer an explicitly supplied props.capture value and fall back to
props.ctx.captureFor?.(props.part.id), or remove capture from ToolCallCardProps
if it is intentionally unsupported.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.fallowrc.json:
- Line 23: Restrict the Fallow exclusions in the configuration to only the
specific affected vite.config.ts path and package containing
`@conciv/extension-page`, or remove them after resolving the findings; do not use
repository-wide glob or package-wide exemptions, and audit the changed code with
Fallow before finishing.

In `@docs/superpowers/specs/2026-08-08-page-tool-cards-design.md`:
- Line 113: Add a language identifier to both fenced code blocks in the
document, using text for language-neutral examples so each fence satisfies
markdownlint MD040.

In `@packages/core/src/tool-registry.ts`:
- Around line 35-40: Update the conciv_page invocation and storeCapture flow so
direct conciv_page calls preserve and pass their tool-call ID even when no
ToolRequest exists. Adjust storeCapture’s inputs or caller to supply the session
and ID explicitly, while retaining existing ToolRequest handling and capture
persistence for normal tool calls.

In `@packages/core/test/chat/code-mode-reload-fold.it.test.ts`:
- Around line 56-60: Update bootScripted to call bootKit without passing
tmpdir() as cwd, allowing the harness adapter to use its sandbox-virtual
/workspace default. Keep the existing harness creation, cleanup registration,
and returned kit/harness behavior unchanged.

In `@packages/db/src/capture-queries.ts`:
- Around line 12-34: Update writeToolCapture and deleteSessionCaptures in
packages/db/src/capture-queries.ts:12-34 and
packages/db/src/capture-queries.ts:57-66 to execute their CSS/capture writes,
reference scan, and orphan cleanup within database transactions. Ensure each
operation uses the transaction handle consistently so deletion cannot interleave
between CSS insertion and capture upserts.

In `@packages/embed/test/helpers/page-plane-host.ts`:
- Around line 4-12: The openPagePlaneHost setup flow must clean up when
page.goto, waitForFunction, or subscribed rejects. Wrap the setup and return
logic in try/finally so observer.dispose() always runs, and close the page in
the failure path before rethrowing the original error.

In `@packages/embed/uno.config.ts`:
- Around line 14-16: Update the core source glob in the UnoCSS configuration to
include the top-level src/cards.tsx entry in addition to files under src/cards/,
ensuring utility classes from both locations are scanned for the embed bundle.

In `@packages/extension/src/types.ts`:
- Line 24: Update the normal chat tool execution flow in runtime.ts to include
the current tool-call ID when constructing the ToolRequest passed to
registry.call. Ensure the value propagates into runClientTool so capture
persistence works for page-tool calls, while preserving existing behavior when
no ID is available.

In `@packages/extensions/page/src/client/cards/shared.tsx`:
- Around line 100-106: Update cardErrorMessage to accept the ToolViewMeta.errors
catalog, resolve declared error codes from result.content to their matching
catalog message before calling resultText, and retain the existing direct-error
and fallback behavior when no catalog match exists. Update all callers to pass
the available errors catalog.

In `@packages/harness-testkit/src/call-tool.ts`:
- Around line 83-105: Update the approval flow around pumpApprovals and
rpc.chat.permissionDecision to pass abort.signal into the RPC call, attach
pumpApprovals’s rejection handler immediately when starting it, and await
iterator.return() during cancellation. Preserve normal abort completion while
propagating non-abort iterator or approval-decision failures so they remain
observable.

In `@packages/page/src/element-capture.ts`:
- Around line 59-68: Update decodeEntities and isJavascriptUrl so malformed or
out-of-range numeric entities, including values above 0x10FFFF, are treated as
unsafe and make isJavascriptUrl return true instead of throwing. Ensure
neutralizeAttributes removes such URL attributes, and add a regression test
covering an invalid numeric entity.
- Around line 105-123: Update serializeWithAncestors to enforce a node or byte
budget for the serialized capture, including descendants produced by
serializeNodeWithId and ancestor wrappers rather than only limiting
ANCESTOR_CAP. When the budget would be exceeded, omit the serialized node
payload while retaining the descriptor-only capture, and preserve the existing
dangerous-node and ancestor handling behavior.

In `@packages/page/src/page-tool-dispatcher.ts`:
- Around line 100-115: Update PageCaptureBundle and buildCaptureBundle so every
unique shipped CSS bundle is retained by its hash instead of overwriting
bundle.cssBundle for each entry. Preserve each capture’s cssBundleId and ensure
both before and after payloads remain resolvable by the host. Add an integration
test covering a before-after capture where the stylesheet changes between
captures.

In `@packages/ui-kit-chat/src/styled/collapsible-card.tsx`:
- Around line 87-90: Update the fallback branch in the collapsible-card
component so static cards preserve local.tooltip/titleTooltip behavior. Apply
the same tooltip wrapper used for regular card content around StaticRow, or pass
the tooltip value into StaticRow, while keeping the existing CardFrame and
local.class behavior unchanged.

In `@packages/ui-kit-chat/src/styled/element-preview.tsx`:
- Around line 29-33: Harden the element-preview rebuild flow by rejecting
executable and resource-loading elements before rebuild, including script and
link in addition to the existing DANGEROUS_TAGS handling. Prefer a render-only
element allowlist, or route rebuilding through rebuildIntoSandboxedIframe, so
UNSAFE_allowUnprotectedRebuild cannot connect unsafe content through
shadow.appendChild(built). Add browser coverage verifying script and link
payloads are rejected or safely isolated.

---

Nitpick comments:
In `@packages/core/test/chat/approving-call-post-result-traffic.it.test.ts`:
- Around line 42-54: Strengthen the test around makeApprovingCallTool by
recording the call start time, asserting the outcome’s removed field confirms
the target was deleted, and asserting the call duration is below the
long-running stream duration (about 900 ms). Keep the existing cleanup and
timeout behavior unchanged.

In `@packages/db/drizzle/20260808214400_tool_captures/snapshot.json`:
- Around line 367-426: Update the schema definition for tool_captures and
css_bundles in schema.ts to add indexes on session_id, supporting capture reload
and session cleanup queries; then regenerate the corresponding Drizzle
migration/snapshot so the indexes are represented in the database schema.

In `@packages/db/src/schema.ts`:
- Around line 43-53: Add the non-unique sessionId index to the toolCaptures
table definition using the name tool_captures_session_id_idx, then generate and
include the corresponding database migration. Preserve the existing composite
primary key and all other columns unchanged.

In `@packages/embed/vite.config.ts`:
- Line 7: Update INLINED_PREFIXES to remove the broad `@conciv/core` prefix and
allow only the exact `@conciv/core/cards` entry for inlining. Preserve the
existing `@conciv/page`, `@conciv/app`, and `@conciv/tools` entries, along with the
`@conciv/extension/`*, Ark, and Solid externalization behavior.

In `@packages/extensions/page/test/defs.test.ts`:
- Line 41: Remove the `as const` assertion from the `MIRROR_VERBS` definition,
leaving the array contents and spread-based equality assertion unchanged.

In `@packages/page/test/element-capture.browser.test.ts`:
- Around line 202-227: Update the test case around captureOf and the hostile
fixture to assert the handler-execution sentinel remains unset, in addition to
the existing serialized-node checks. Verify the sentinel after the fixture
mounts and capture completes, covering both the img onerror and iframe srcdoc
payloads.

In `@packages/ui-kit-chat/src/styled/tools/tool-call-card.tsx`:
- Line 35: Update the capture handling in ToolCallCard so the declared optional
capture prop is not ignored: prefer an explicitly supplied props.capture value
and fall back to props.ctx.captureFor?.(props.part.id), or remove capture from
ToolCallCardProps if it is intentionally unsupported.

In `@packages/ui-kit-chat/test/element-preview.browser.test.tsx`:
- Around line 180-203: Extend the test around the hostile payload in the
existing element preview case to assert that onerror and onclick attributes are
absent from the sanitized shadow content. Also locate the ordinary hostile link,
click it, and verify XSS_FLAG remains unset, while preserving the existing
malformed java\tscript: href assertion and safe-content checks.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a6ab5c22-8e5a-4f00-9759-9cf88ee6734d

📥 Commits

Reviewing files that changed from the base of the PR and between 9af0758 and e3c3692.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (185)
  • .fallowrc.json
  • apps/conciv/package.json
  • apps/conciv/src/pane/chat-pane.tsx
  • apps/conciv/src/pane/conciv-ui-card.tsx
  • apps/conciv/src/pane/session-captures.ts
  • apps/conciv/src/pane/tool-view-ctx.ts
  • apps/conciv/test/helpers/fake-core-router.ts
  • apps/conciv/test/helpers/fake-core.ts
  • apps/conciv/test/kit-controls.browser.test.tsx
  • apps/conciv/test/tool-card-dispatch.browser.test.tsx
  • apps/conciv/uno.config.ts
  • apps/storybook/.storybook/main.ts
  • apps/storybook/package.json
  • apps/storybook/uno.config.ts
  • docs/superpowers/specs/2026-08-08-page-tool-cards-design.md
  • docs/superpowers/specs/2026-08-09-conciv-ui-card-unification-design.md
  • packages/cli/src/tool-command.ts
  • packages/contract/src/contract.ts
  • packages/core/package.json
  • packages/core/src/api/execute-schemas.ts
  • packages/core/src/api/mcp.ts
  • packages/core/src/api/rpc/router.ts
  • packages/core/src/api/rpc/sessions.ts
  • packages/core/src/app.ts
  • packages/core/src/cards.tsx
  • packages/core/src/cards/code-run-card.stories.tsx
  • packages/core/src/cards/code-run-card.tsx
  • packages/core/src/chat/capabilities.ts
  • packages/core/src/chat/code-mode-parts.ts
  • packages/core/src/chat/code-mode.ts
  • packages/core/src/page-bus.ts
  • packages/core/src/tool-registry.ts
  • packages/core/test/api/page/tool-capture.it.test.ts
  • packages/core/test/builtin-tool-calls.test.ts
  • packages/core/test/chat/approving-call-post-result-traffic.it.test.ts
  • packages/core/test/chat/code-mode-parts.test.ts
  • packages/core/test/chat/code-mode-reload-fold.it.test.ts
  • packages/core/test/client-tool-gate.test.ts
  • packages/core/tsconfig.cards.build.json
  • packages/core/tsconfig.cards.json
  • packages/core/uno.config.ts
  • packages/core/vite.config.ts
  • packages/db/drizzle/20260808214400_tool_captures/migration.sql
  • packages/db/drizzle/20260808214400_tool_captures/snapshot.json
  • packages/db/src/capture-queries.ts
  • packages/db/src/index.ts
  • packages/db/src/run-queries.ts
  • packages/db/src/schema.ts
  • packages/db/test/capture-queries.test.ts
  • packages/embed/test/element-capture.it.test.ts
  • packages/embed/test/embed.it.test.ts
  • packages/embed/test/helpers/page-plane-host.ts
  • packages/embed/test/mount-externals.test.ts
  • packages/embed/uno.config.ts
  • packages/embed/vite.config.ts
  • packages/extension-testkit/src/card-harness.tsx
  • packages/extension/src/collect-client.ts
  • packages/extension/src/define-tool.ts
  • packages/extension/src/tool-registry.ts
  • packages/extension/src/types.ts
  • packages/extension/test/tool-registry.test.ts
  • packages/extensions/page/.gitignore
  • packages/extensions/page/package.json
  • packages/extensions/page/src/client.tsx
  • packages/extensions/page/src/client/bodies.ts
  • packages/extensions/page/src/client/cards/act-card.stories.tsx
  • packages/extensions/page/src/client/cards/act-card.tsx
  • packages/extensions/page/src/client/cards/console-card.stories.tsx
  • packages/extensions/page/src/client/cards/console-card.tsx
  • packages/extensions/page/src/client/cards/edit-live-card.stories.tsx
  • packages/extensions/page/src/client/cards/edit-live-card.tsx
  • packages/extensions/page/src/client/cards/effect-card.stories.tsx
  • packages/extensions/page/src/client/cards/effect-card.tsx
  • packages/extensions/page/src/client/cards/react-card.stories.tsx
  • packages/extensions/page/src/client/cards/react-card.tsx
  • packages/extensions/page/src/client/cards/read-bulk-card.stories.tsx
  • packages/extensions/page/src/client/cards/read-bulk-card.tsx
  • packages/extensions/page/src/client/cards/read-value-card.stories.tsx
  • packages/extensions/page/src/client/cards/read-value-card.tsx
  • packages/extensions/page/src/client/cards/shared.tsx
  • packages/extensions/page/src/client/cards/story.fixtures.ts
  • packages/extensions/page/src/client/js-beautify-html.d.ts
  • packages/extensions/page/src/client/page-format.ts
  • packages/extensions/page/src/client/page-result-views.stories.tsx
  • packages/extensions/page/src/client/page-result-views.tsx
  • packages/extensions/page/src/shared/defs.ts
  • packages/extensions/page/test/defs.test.ts
  • packages/extensions/page/test/tsconfig.json
  • packages/extensions/page/tsconfig.build.json
  • packages/extensions/page/tsconfig.json
  • packages/extensions/page/tsconfig.refs.json
  • packages/extensions/page/tsdown.config.ts
  • packages/extensions/page/uno.config.ts
  • packages/extensions/page/vite.config.ts
  • packages/extensions/test-runner/test/test-card.browser.test.tsx
  • packages/extensions/whiteboard/src/client/model/comments.tsx
  • packages/harness-testkit/src/call-tool.ts
  • packages/page/package.json
  • packages/page/src/css-bundle.ts
  • packages/page/src/element-capture.ts
  • packages/page/src/element-descriptor.ts
  • packages/page/src/page-driver.ts
  • packages/page/src/page-snapshot.ts
  • packages/page/src/page-tool-dispatcher.ts
  • packages/page/test/css-bundle.test.ts
  • packages/page/test/element-capture.browser.test.ts
  • packages/page/test/page-dispatcher.browser.test.ts
  • packages/protocol/package.json
  • packages/protocol/src/chat-types.ts
  • packages/protocol/src/done-types.ts
  • packages/protocol/src/element-capture-types.ts
  • packages/protocol/src/page-types.ts
  • packages/protocol/src/tool-view-types.ts
  • packages/protocol/tsdown.config.ts
  • packages/tools/package.json
  • packages/tools/src/cards.tsx
  • packages/tools/src/cards/extensions-card.tsx
  • packages/tools/src/cards/inline-cards.stories.tsx
  • packages/tools/src/cards/open-card.tsx
  • packages/tools/src/cards/ui-card.stories.tsx
  • packages/tools/src/cards/ui-card.tsx
  • packages/tools/tsconfig.cards.build.json
  • packages/tools/tsconfig.cards.json
  • packages/tools/uno.config.ts
  • packages/tools/vite.config.ts
  • packages/ui-kit-chat-tools/package.json
  • packages/ui-kit-chat-tools/src/index.tsx
  • packages/ui-kit-chat-tools/src/primitives/tools/inline-tool.tsx
  • packages/ui-kit-chat-tools/src/primitives/tools/now-title.ts
  • packages/ui-kit-chat-tools/src/styled/done-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/done-card.tsx
  • packages/ui-kit-chat-tools/src/styled/page-action-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/page-action-card.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/apply-patch-diff.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/bash-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/builtin-tool-cards.ts
  • packages/ui-kit-chat-tools/src/styled/tools/discovered-apis-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/file-read-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/inline-tool.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/inline-tool.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/loaded-tools-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/loaded-tools-card.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/todo-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/tool-chip.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/tool-lookup-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/ui-chip-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/ui-chip-card.tsx
  • packages/ui-kit-chat-tools/test/catalog-cards.browser.test.tsx
  • packages/ui-kit-chat-tools/test/new-tool-projection.browser.test.tsx
  • packages/ui-kit-chat-tools/test/page-action-card-title.browser.test.tsx
  • packages/ui-kit-chat-tools/test/page-tool-cards.browser.test.tsx
  • packages/ui-kit-chat-tools/test/registry-card-declarations.browser.test.tsx
  • packages/ui-kit-chat-tools/test/schema-params.test.ts
  • packages/ui-kit-chat/package.json
  • packages/ui-kit-chat/src/index.tsx
  • packages/ui-kit-chat/src/primitives/message/message.tsx
  • packages/ui-kit-chat/src/primitives/tools/schema-params.ts
  • packages/ui-kit-chat/src/primitives/tools/tool-presentation.ts
  • packages/ui-kit-chat/src/store/element-capture.fixtures.ts
  • packages/ui-kit-chat/src/store/tool-context.tsx
  • packages/ui-kit-chat/src/styled/chip.tsx
  • packages/ui-kit-chat/src/styled/collapsible-card.tsx
  • packages/ui-kit-chat/src/styled/element-preview.stories.tsx
  • packages/ui-kit-chat/src/styled/element-preview.tsx
  • packages/ui-kit-chat/src/styled/json-tree.tsx
  • packages/ui-kit-chat/src/styled/tool-card.tsx
  • packages/ui-kit-chat/src/styled/tool-fallback.stories.tsx
  • packages/ui-kit-chat/src/styled/tool-icon.tsx
  • packages/ui-kit-chat/src/styled/tools/inline-row.tsx
  • packages/ui-kit-chat/src/styled/tools/meta-tool-card.stories.tsx
  • packages/ui-kit-chat/src/styled/tools/meta-tool-card.tsx
  • packages/ui-kit-chat/src/styled/tools/note-row.tsx
  • packages/ui-kit-chat/src/styled/tools/permission-card.stories.tsx
  • packages/ui-kit-chat/src/styled/tools/permission-card.tsx
  • packages/ui-kit-chat/src/styled/tools/tool-call-card.tsx
  • packages/ui-kit-chat/test/collapsible-card-shape.browser.test.tsx
  • packages/ui-kit-chat/test/element-preview.browser.test.tsx
  • packages/ui-kit-chat/test/meta-tool-card-error.browser.test.tsx
  • packages/ui-kit-chat/test/schema-params.test.ts
  • packages/ui-kit-chat/test/tool-call-card-dispatch.browser.test.tsx
  • packages/ui-kit-system/src/index.tsx
  • packages/ui-kit-system/src/json-tree-view.tsx
  • packages/ui-kit-system/src/select.tsx
  • packages/uno-preset/src/index.ts
  • packages/uno-preset/src/json-tree.ts
💤 Files with no reviewable changes (13)
  • packages/ui-kit-chat-tools/package.json
  • packages/ui-kit-chat-tools/src/styled/tools/builtin-tool-cards.ts
  • packages/ui-kit-chat-tools/src/styled/done-card.tsx
  • apps/conciv/src/pane/conciv-ui-card.tsx
  • packages/ui-kit-chat-tools/test/page-action-card-title.browser.test.tsx
  • packages/ui-kit-chat-tools/src/styled/ui-chip-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/page-action-card.stories.tsx
  • packages/ui-kit-chat-tools/src/primitives/tools/inline-tool.tsx
  • packages/ui-kit-chat-tools/src/styled/done-card.stories.tsx
  • packages/ui-kit-chat-tools/test/schema-params.test.ts
  • packages/ui-kit-chat-tools/src/styled/ui-chip-card.tsx
  • packages/protocol/src/done-types.ts
  • packages/ui-kit-chat-tools/src/styled/page-action-card.tsx

Comment thread .fallowrc.json
"e2e/vite-solid/**",
"e2e/vite-vanilla/**",
"**/vitest.config.ts",
"**/vite.config.ts",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Keep Fallow exclusions scoped to proven false positives.

Line 23 excludes every vite.config.ts from Fallow. This includes build configurations changed by this PR. Line 73 suppresses dependency analysis for @conciv/extension-page in every package. Restrict each exclusion to the affected path, or resolve the finding without a blanket exemption.

As per coding guidelines, “Before finishing work, audit changed code with Fallow and fix findings introduced by the changes.”

Also applies to: 73-73

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.fallowrc.json at line 23, Restrict the Fallow exclusions in the
configuration to only the specific affected vite.config.ts path and package
containing `@conciv/extension-page`, or remove them after resolving the findings;
do not use repository-wide glob or package-wide exemptions, and audit the
changed code with Fallow before finishing.

Source: Coding guidelines


Capture happens client-side, inside the page verb, at execute time:

```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add language identifiers to both fenced code blocks.

markdownlint reports MD040 for both fences. Use text if the examples are intentionally language-neutral.

Proposed fix
- ```
+ ```text

Also applies to: 154-154

🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 113-113: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-08-08-page-tool-cards-design.md` at line 113, Add
a language identifier to both fenced code blocks in the document, using text for
language-neutral examples so each fence satisfies markdownlint MD040.

Source: Linters/SAST tools

Comment thread packages/core/src/tool-registry.ts
Comment on lines +56 to +60
async function bootScripted(): Promise<{kit: Kit; harness: TestHarness}> {
const harness = createTestHarness(requireClaude())
const kit = await bootKit({cwd: tmpdir()}, harness)
cleanups.push(() => kit.cleanup())
return {kit, harness}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not pass a host path as the harness workdir.

Line 58 passes the host-absolute result of tmpdir() as cwd. Remove this override so the adapter uses its sandbox-virtual /workspace default.

As per coding guidelines, “Harness workdirs are sandbox-virtual: adapters default to /workspace, and must never receive a host-absolute cwd.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/test/chat/code-mode-reload-fold.it.test.ts` around lines 56 -
60, Update bootScripted to call bootKit without passing tmpdir() as cwd,
allowing the harness adapter to use its sandbox-virtual /workspace default. Keep
the existing harness creation, cleanup registration, and returned kit/harness
behavior unchanged.

Source: Coding guidelines

Comment on lines +12 to +34
if (cssBundle !== undefined) {
await db
.insert(cssBundles)
.values({hash: cssBundle.hash, sessionId: params.sessionId, css: cssBundle.css, createdAt})
.onConflictDoNothing()
}
for (const capture of [before, after]) {
if (capture === undefined) continue
const row = {
toolCallId: params.toolCallId,
kind: capture.kind,
sessionId: params.sessionId,
cssBundleId: capture.cssBundleId ?? null,
payload: capture,
createdAt,
}
await db
.insert(toolCaptures)
.values(row)
.onConflictDoUpdate({
target: [toolCaptures.toolCallId, toolCaptures.kind],
set: {sessionId: row.sessionId, cssBundleId: row.cssBundleId, payload: row.payload, createdAt: row.createdAt},
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline packages/db/src/db.ts --items all
rg -n -C 3 '\.transaction\(' packages/db/src

Repository: conciv-dev/conciv

Length of output: 1158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- applicable skill files ---'
find .. -name 'SKILL.md' -o -name 'skill.md' 2>/dev/null | sort

printf '%s\n' '--- database source map ---'
ast-grep outline packages/db/src/capture-queries.ts --items all
ast-grep outline packages/db/src/db.ts --items all
ast-grep outline packages/db/src/capture-schema.ts --items all 2>/dev/null || true

printf '%s\n' '--- relevant source ---'
cat -n packages/db/src/capture-queries.ts
cat -n packages/db/src/db.ts

printf '%s\n' '--- capture schema references ---'
rg -n -C 4 'toolCaptures|cssBundles|deleteSessionCaptures|writeToolCapture' packages/db/src packages --glob '*.{ts,tsx,js,jsx}'

Repository: conciv-dev/conciv

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- review skill ---'
cat ../git/.claude/skills/conciv-review/SKILL.md

printf '%s\n' '--- database schema ---'
cat -n packages/db/src/schema.ts | sed -n '40,70p'

printf '%s\n' '--- session deletion call path ---'
cat -n packages/core/src/api/rpc/sessions.ts | sed -n '75,105p'
rg -n -C 5 'deleteSessionCaptures\(' packages --glob '*.{ts,tsx,js,jsx}'

printf '%s\n' '--- database package configuration ---'
cat packages/db/package.json
rg -n -C 3 'sqlite|drizzle' package.json packages/db/package.json pnpm-lock.yaml | head -120

printf '%s\n' '--- workspace intent configuration ---'
find . -maxdepth 3 -iname '*intent*' -o -iname 'AGENTS.md' -o -iname 'CLAUDE.md' | sort

Repository: conciv-dev/conciv

Length of output: 29564


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- workspace instructions ---'
cat AGENTS.md
cat CLAUDE.md

printf '%s\n' '--- TanStack Intent package skills ---'
pnpm dlx `@tanstack/intent`@latest list

Repository: conciv-dev/conciv

Length of output: 15870


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import sqlite3

schema = """
CREATE TABLE css_bundles (
  hash TEXT PRIMARY KEY,
  session_id TEXT NOT NULL,
  css TEXT NOT NULL,
  created_at INTEGER NOT NULL
);
CREATE TABLE tool_captures (
  tool_call_id TEXT NOT NULL,
  kind TEXT NOT NULL,
  session_id TEXT NOT NULL,
  css_bundle_id TEXT,
  payload TEXT NOT NULL,
  created_at INTEGER NOT NULL,
  PRIMARY KEY (tool_call_id, kind)
);
"""

def missing_bundle_count(connection):
    return connection.execute("""
        SELECT COUNT(*)
        FROM tool_captures AS captures
        LEFT JOIN css_bundles AS bundles
          ON bundles.hash = captures.css_bundle_id
        WHERE captures.css_bundle_id IS NOT NULL
          AND bundles.hash IS NULL
    """).fetchone()[0]

def seed_css(connection):
    connection.execute(
        "INSERT INTO css_bundles VALUES (?, ?, ?, ?)",
        ("css-1", "session-1", "body{}", 1),
    )

def insert_capture(connection):
    connection.execute(
        "INSERT INTO tool_captures VALUES (?, ?, ?, ?, ?, ?)",
        ("call-1", "after", "session-1", "css-1", "{}", 2),
    )

def delete_session_captures(connection):
    connection.execute(
        "DELETE FROM tool_captures WHERE session_id = ?",
        ("session-1",),
    )
    referenced = {
        row[0]
        for row in connection.execute(
            "SELECT DISTINCT css_bundle_id FROM tool_captures"
        )
        if row[0] is not None
    }
    if not referenced:
        connection.execute("DELETE FROM css_bundles")
    else:
        placeholders = ",".join("?" for _ in referenced)
        connection.execute(
            f"DELETE FROM css_bundles WHERE hash NOT IN ({placeholders})",
            tuple(referenced),
        )

connection = sqlite3.connect(":memory:")
connection.executescript(schema)
seed_css(connection)
delete_session_captures(connection)
insert_capture(connection)
assert missing_bundle_count(connection) == 1
print("separate statements: missing CSS bundle references =", missing_bundle_count(connection))

connection = sqlite3.connect(":memory:")
connection.executescript(schema)
connection.execute("BEGIN")
seed_css(connection)
insert_capture(connection)
connection.commit()
connection.execute("BEGIN")
delete_session_captures(connection)
connection.commit()
assert missing_bundle_count(connection) == 0
print("atomic write/delete transactions: missing CSS bundle references =", missing_bundle_count(connection))
PY

Repository: conciv-dev/conciv

Length of output: 278


Make capture writes and cleanup atomic.

writeToolCapture executes the CSS insert and capture upserts as separate statements. If deleteSessionCaptures runs between them, it can delete the CSS bundle before the capture row is inserted. Wrap both operations, including the reference scan and orphan cleanup, in transactions.

📍 Affects 1 file
  • packages/db/src/capture-queries.ts#L12-L34 (this comment)
  • packages/db/src/capture-queries.ts#L57-L66
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/src/capture-queries.ts` around lines 12 - 34, Update
writeToolCapture and deleteSessionCaptures in
packages/db/src/capture-queries.ts:12-34 and
packages/db/src/capture-queries.ts:57-66 to execute their CSS/capture writes,
reference scan, and orphan cleanup within database transactions. Ensure each
operation uses the transaction handle consistently so deletion cannot interleave
between CSS insertion and capture upserts.

Comment on lines +59 to +68
function decodeEntities(value: string): string {
return value
.replace(/&#x([0-9a-f]+);?/gi, (_match, hex: string) => String.fromCodePoint(Number.parseInt(hex, 16)))
.replace(/&#(\d+);?/g, (_match, dec: string) => String.fromCodePoint(Number.parseInt(dec, 10)))
.replace(/&([a-z]+);?/gi, (match, name: string) => NAMED_ENTITIES[name.toLowerCase()] ?? match)
}

function isJavascriptUrl(value: string): boolean {
const stripped = decodeEntities(value).replace(CONTROL_AND_SPACE_PATTERN, '')
return JAVASCRIPT_SCHEME_PATTERN.test(stripped)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

printf '%s\n' '--- candidate skills ---'
find .. -iname '*skill*' -o -iname 'AGENTS.md' -o -iname 'CONTRIBUTING.md' | head -80
printf '%s\n' '--- target file ---'
cat -n packages/page/src/element-capture.ts | sed -n '1,130p'

Repository: conciv-dev/conciv

Length of output: 16816


🏁 Script executed:

cat -n AGENTS.md
printf '%s\n' '--- review skill ---'
cat -n .claude/skills/conciv-review/SKILL.md
printf '%s\n' '--- target symbols and tests ---'
rg -n "decodeEntities|isJavascriptUrl|neutralizeAttributes|element-capture|javascript:" packages/page --glob '!dist/**'
printf '%s\n' '--- package files ---'
find packages/page -maxdepth 3 -type f | sort

Repository: conciv-dev/conciv

Length of output: 33300


🏁 Script executed:

pnpm dlx `@tanstack/intent`@latest list
printf '%s\n' '--- capture implementation map ---'
ast-grep outline packages/page/src/element-capture.ts
printf '%s\n' '--- capture flow ---'
cat -n packages/page/src/element-capture.ts | sed -n '126,280p'
printf '%s\n' '--- browser test coverage ---'
cat -n packages/page/test/element-capture.browser.test.ts | sed -n '70,245p'

Repository: conciv-dev/conciv

Length of output: 12666


🏁 Script executed:

node - <<'JS'
const namedEntities = {amp: '&', colon: ':', tab: '\t', newline: '\n'}
const controlAndSpacePattern = new RegExp(`[${String.fromCharCode(0)}-${String.fromCharCode(0x20)}]`, 'g')

function decodeEntities(value) {
  return value
    .replace(/&`#x`([0-9a-f]+);?/gi, (_match, hex) => String.fromCodePoint(Number.parseInt(hex, 16)))
    .replace(/&#(\d+);?/g, (_match, dec) => String.fromCodePoint(Number.parseInt(dec, 10)))
    .replace(/&([a-z]+);?/gi, (match, name) => namedEntities[name.toLowerCase()] ?? match)
}

for (const value of ['&`#x110000`;', '&`#1114112`;', '&`#xFFFFFFFF`;', 'java&`#x110000`;script:']) {
  try {
    const decoded = decodeEntities(value)
    console.log(JSON.stringify({value, decoded, javascript: /^javascript:/i.test(decoded.replace(controlAndSpacePattern, ''))}))
  } catch (error) {
    console.log(JSON.stringify({value, throws: error instanceof Error ? error.message : String(error)}))
  }
}
JS
printf '%s\n' '--- callers of takeElementCapture ---'
rg -n -C 4 "takeElementCapture\\(" packages/page/src packages/page/test
printf '%s\n' '--- package test configuration ---'
cat -n packages/page/package.json
cat -n packages/page/vitest.config.ts

Repository: conciv-dev/conciv

Length of output: 4668


🏁 Script executed:

cat -n packages/page/src/page-tool-dispatcher.ts | sed -n '55,125p'
printf '%s\n' '--- capture entry construction ---'
rg -n -C 8 "pendingCss|entries\\.push|before|after|capture" packages/page/src/page-tool-dispatcher.ts
printf '%s\n' '--- all capture callers and exception handling ---'
rg -n -C 6 "captureElements|take\\(|pageCapture|ElementCapture" packages/page/src packages/page/test

Repository: conciv-dev/conciv

Length of output: 18306


Treat out-of-range numeric entities as unsafe URL values.

String.fromCodePoint throws for values above 0x10FFFF, such as &#x110000;. The capture dispatcher catches this exception and silently drops the affected capture. Make invalid numeric entities cause isJavascriptUrl to return true, so neutralizeAttributes removes the URL attribute. Add a regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/page/src/element-capture.ts` around lines 59 - 68, Update
decodeEntities and isJavascriptUrl so malformed or out-of-range numeric
entities, including values above 0x10FFFF, are treated as unsafe and make
isJavascriptUrl return true instead of throwing. Ensure neutralizeAttributes
removes such URL attributes, and add a regression test covering an invalid
numeric entity.

Comment on lines +105 to +123
function serializeWithAncestors(el: Element, doc: Document): SerializedNode | null {
const target = serializeNodeWithId(el, serializeOptions(doc, false))
if (target === null || isDangerousTag(target)) return null
scrubSensitiveValues(target)
markTarget(target)
let chained = target
let ancestor = el.parentElement
let depth = 0
while (ancestor !== null && depth < ANCESTOR_CAP) {
const serialized = serializeNodeWithId(ancestor, serializeOptions(doc, true))
if (serialized === null || isDangerousTag(serialized)) break
const wrapped = withSingleChild(serialized, chained)
if (wrapped === null) break
chained = wrapped
ancestor = ancestor.parentElement
depth += 1
}
neutralizeSubtree(chained)
return chained

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

printf '%s\n' '--- skill files ---'
find .. -iname '*skill*' -o -iname 'AGENTS.md' -o -iname 'CLAUDE.md' | head -80
printf '%s\n' '--- target outline ---'
ast-grep outline packages/page/src/element-capture.ts --lang typescript
printf '%s\n' '--- target context ---'
sed -n '1,220p' packages/page/src/element-capture.ts

Repository: conciv-dev/conciv

Length of output: 18932


🏁 Script executed:

printf '%s\n' '--- repository instructions ---'
cat AGENTS.md
cat CLAUDE.md
printf '%s\n' '--- review skill ---'
cat .claude/skills/conciv-review/SKILL.md
printf '%s\n' '--- capture types and consumers ---'
rg -n -C 3 'ElementCapture|takeElementCapture|pendingCss|capture.*node|node.*capture' packages --glob '*.{ts,tsx,js,jsx}'

Repository: conciv-dev/conciv

Length of output: 50375


🏁 Script executed:

printf '%s\n' '--- TanStack Intent skill check ---'
pnpm dlx `@tanstack/intent`@latest list
printf '%s\n' '--- page package skill candidates ---'
find packages/page -maxdepth 3 -type f \( -iname 'SKILL.md' -o -iname '*skill*' \) -print
printf '%s\n' '--- capture dispatch and protocol ---'
sed -n '74,125p' packages/page/src/page-tool-dispatcher.ts
cat -n packages/protocol/src/element-capture-types.ts
printf '%s\n' '--- relevant storage/RPC paths ---'
rg -n -C 3 'ToolCaptureRowSchema|SessionCapturesSchema|capture|cssBundle' packages/core packages/storage packages/protocol packages/ui-kit-chat --glob '*.{ts,tsx}' | head -260
printf '%s\n' '--- rrweb package metadata and serializer source ---'
node -e "const p=require.resolve('rrweb-snapshot/package.json'); console.log(p)"
rrweb_pkg=$(node -e "const p=require.resolve('rrweb-snapshot/package.json'); console.log(require('node:path').dirname(p))")
rg -n -C 8 'function serializeNodeWithId|const serializeNodeWithId|skipChild|childNodes' "$rrweb_pkg" --glob '*.{js,mjs,cjs,ts}' | head -240

Repository: conciv-dev/conciv

Length of output: 29684


🏁 Script executed:

printf '%s\n' '--- rrweb installation and package declaration ---'
find node_modules . -path '*rrweb-snapshot*' -print 2>/dev/null | head -80
rg -n 'rrweb-snapshot' package.json packages/*/package.json pnpm-lock.yaml
printf '%s\n' '--- capture persistence and transport call sites ---'
git ls-files | rg 'capture|session|tool-result|page'
rg -n -C 5 'SessionCaptures|ToolCaptureRow|capture:' packages/core packages/client packages/page packages/protocol packages/ui-kit-chat --glob '*.{ts,tsx}' | head -320
printf '%s\n' '--- capture tests around large or nested DOM cases ---'
sed -n '1,260p' packages/page/test/element-capture.browser.test.ts
printf '%s\n' '--- preview rebuild path ---'
sed -n '175,245p' packages/ui-kit-chat/src/styled/element-preview.tsx

Repository: conciv-dev/conciv

Length of output: 43677


🏁 Script executed:

printf '%s\n' '--- capture database schema and queries ---'
sed -n '1,240p' packages/db/src/capture-queries.ts
rg -n -C 5 'toolCaptures|capture|json|text' packages/db/src packages/db/drizzle/20260808214400_tool_captures --glob '*.{ts,sql,json}'
printf '%s\n' '--- session capture ingestion ---'
rg -n -C 6 'insert.*capture|capture.*insert|ToolCaptureRowSchema|SessionCapturesSchema|toolCaptureViews' packages/core apps packages/db packages/client --glob '*.{ts,tsx}' | head -260
printf '%s\n' '--- rrweb serializer implementation ---'
rg -n -C 14 'serializeNodeWithId|skipChild' node_modules/.pnpm/rrweb-snapshot@2.1.0/node_modules/rrweb-snapshot/dist/rrweb-snapshot.js | head -300
printf '%s\n' '--- available DOM runtimes ---'
for p in jsdom happy-dom linkedom playwright playwright-core; do
  node -e "try { console.log('$p', require.resolve('$p/package.json')) } catch { console.log('$p unavailable') }"
done

Repository: conciv-dev/conciv

Length of output: 50373


🏁 Script executed:

printf '%s\n' '--- rrweb serializer symbols ---'
grep -n -E 'serializeNodeWithId|skipChild' node_modules/.pnpm/rrweb-snapshot@2.1.0/node_modules/rrweb-snapshot/dist/rrweb-snapshot.js | head -40
printf '%s\n' '--- package exports and source map names ---'
cat node_modules/.pnpm/rrweb-snapshot@2.1.0/node_modules/rrweb-snapshot/package.json
printf '%s\n' '--- DOM runtimes ---'
for p in jsdom happy-dom linkedom playwright playwright-core; do
  node -e "try { console.log('$p', require.resolve('$p/package.json')) } catch { console.log('$p unavailable') }"
done
printf '%s\n' '--- focused session capture consumers ---'
rg -n -C 8 'sessionCaptures\(|writeToolCapture\(|captureFor|captureViews' packages/core apps packages/client packages/db --glob '*.{ts,tsx}'

Repository: conciv-dev/conciv

Length of output: 15208


🏁 Script executed:

printf '%s\n' '--- rrweb serializer implementation ---'
sed -n '1200,1340p' node_modules/.pnpm/rrweb-snapshot@2.1.0/node_modules/rrweb-snapshot/dist/rrweb-snapshot.js
printf '%s\n' '--- RPC schema for captures ---'
rg -n -C 8 'captures.*list|captures:' packages/contract packages/core --glob '*.{ts,tsx}'
printf '%s\n' '--- client capture loading ---'
rg -n -C 8 'captures\.list|sessionCaptures|toolCaptureViews' apps packages/client --glob '*.{ts,tsx}'

Repository: conciv-dev/conciv

Length of output: 15966


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
import re

capture = Path('packages/page/src/element-capture.ts').read_text()
serializer = Path('node_modules/.pnpm/rrweb-snapshot@2.1.0/node_modules/rrweb-snapshot/dist/rrweb-snapshot.js').read_text()
schema = Path('packages/db/src/schema.ts').read_text()
queries = Path('packages/db/src/capture-queries.ts').read_text()
contract = Path('packages/contract/src/contract.ts').read_text()

checks = {
    'target_serializes_with_descendants': 'serializeNodeWithId(el, serializeOptions(doc, false))' in capture,
    'ancestor_cap_is_only_loop_bound': re.search(r'while \\(ancestor !== null && depth < ANCESTOR_CAP\\)', capture) is not None,
    'serializer_records_children_when_skipChild_false': 'let recordChild = !skipChild' in serializer,
    'serializer_recurses_over_each_child': 'for (const childN of Array.from(index.childNodes(n)))' in serializer
        and 'const serializedChildNode = serializeNodeWithId(childN, bypassOptions)' in serializer,
    'capture_payload_is_json_text': "payload: text('payload', {mode: 'json'})" in schema,
    'capture_is_stored_unchanged': 'payload: capture' in queries,
    'capture_list_returns_payload': 'capture: row.payload' in queries,
    'capture_rpc_returns_session_captures': 'list: oc.input(SessionIdInput).output(SessionCapturesSchema)' in contract,
    'no_budget_identifier_in_target': not re.search(r'\\b(?:NODE|BYTE|PAYLOAD|CAPTURE)_?(?:CAP|LIMIT|BUDGET)\\b', capture, re.I),
}
for name, passed in checks.items():
    print(f'{name}: {passed}')
if not all(checks.values()):
    raise SystemExit('deterministic invariant check failed')

def synthetic_capture_size(children: int) -> int:
    node = {'type': 2, 'tagName': 'div', 'attributes': {}, 'childNodes': [
        {'type': 2, 'tagName': 'span', 'attributes': {}, 'childNodes': []}
        for _ in range(children)
    ]}
    import json
    return len(json.dumps(node, separators=(',', ':')))

for count in (0, 10, 1000):
    print(f'synthetic descendants={count} json_bytes={synthetic_capture_size(count)}')
PY

Repository: conciv-dev/conciv

Length of output: 553


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
import json
import re

capture = Path('packages/page/src/element-capture.ts').read_text()
serializer = Path('node_modules/.pnpm/rrweb-snapshot@2.1.0/node_modules/rrweb-snapshot/dist/rrweb-snapshot.js').read_text()
schema = Path('packages/db/src/schema.ts').read_text()
queries = Path('packages/db/src/capture-queries.ts').read_text()
contract = Path('packages/contract/src/contract.ts').read_text()

checks = {
    'target_serializes_with_descendants': 'serializeNodeWithId(el, serializeOptions(doc, false))' in capture,
    'ancestor_cap_is_only_loop_bound': 'while (ancestor !== null && depth < ANCESTOR_CAP)' in capture,
    'serializer_records_children_when_skipChild_false': 'let recordChild = !skipChild' in serializer,
    'serializer_recurses_over_each_child': (
        'for (const childN of Array.from(index.childNodes(n)))' in serializer
        and 'const serializedChildNode = serializeNodeWithId(childN, bypassOptions)' in serializer
    ),
    'capture_payload_is_json_text': "payload: text('payload', {mode: 'json'})" in schema,
    'capture_is_stored_unchanged': 'payload: capture' in queries,
    'capture_list_returns_payload': 'capture: row.payload' in queries,
    'capture_rpc_returns_session_captures': 'list: oc.input(SessionIdInput).output(SessionCapturesSchema)' in contract,
    'no_budget_identifier_in_target': not re.search(r'\b(?:NODE|BYTE|PAYLOAD|CAPTURE)_?(?:CAP|LIMIT|BUDGET)\b', capture, re.I),
}
for name, passed in checks.items():
    print(f'{name}: {passed}')
if not all(checks.values()):
    raise SystemExit('deterministic invariant check failed')

def synthetic_capture_size(children: int) -> int:
    node = {
        'type': 2,
        'tagName': 'div',
        'attributes': {},
        'childNodes': [
            {'type': 2, 'tagName': 'span', 'attributes': {}, 'childNodes': []}
            for _ in range(children)
        ],
    }
    return len(json.dumps(node, separators=(',', ':')))

for count in (0, 10, 1000):
    print(f'synthetic descendants={count} json_bytes={synthetic_capture_size(count)}')
PY

Repository: conciv-dev/conciv

Length of output: 637


Bound the serialized capture payload.

serializeNodeWithId(el, serializeOptions(doc, false)) includes every descendant. ANCESTOR_CAP limits only ancestor wrappers. Add a node or byte budget. When the budget is exceeded, omit node and retain the descriptor-only capture.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/page/src/element-capture.ts` around lines 105 - 123, Update
serializeWithAncestors to enforce a node or byte budget for the serialized
capture, including descendants produced by serializeNodeWithId and ancestor
wrappers rather than only limiting ANCESTOR_CAP. When the budget would be
exceeded, omit the serialized node payload while retaining the descriptor-only
capture, and preserve the existing dangerous-node and ancestor handling
behavior.

Comment on lines +100 to +115
async function buildCaptureBundle(
entries: readonly PendingBundleEntry[],
shipCss: CssBundleDeduper,
): Promise<PageCaptureBundle | undefined> {
if (entries.length === 0) return undefined
const bundle: PageCaptureBundle = {}
for (const entry of entries) {
if (entry.pendingCss === null) {
bundle[entry.kind] = entry.capture
continue
}
const shipped = await shipCss(entry.pendingCss)
bundle[entry.kind] = {...entry.capture, cssBundleId: shipped.hash}
if (shipped.bundle !== undefined) bundle.cssBundle = shipped.bundle
}
return bundle

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve every CSS bundle referenced by a capture.

Line 113 overwrites bundle.cssBundle for each capture entry. If a before-after tool changes a stylesheet, the before capture can reference the first hash while the outgoing bundle contains only the last CSS payload. The host cannot resolve the first capture stylesheet from this response.

Change PageCaptureBundle to carry every unique shipped CSS bundle, keyed or listed by hash. Persist each emitted bundle. Add an integration test where CSS changes between the before and after captures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/page/src/page-tool-dispatcher.ts` around lines 100 - 115, Update
PageCaptureBundle and buildCaptureBundle so every unique shipped CSS bundle is
retained by its hash instead of overwriting bundle.cssBundle for each entry.
Preserve each capture’s cssBundleId and ensure both before and after payloads
remain resolvable by the host. Add an integration test covering a before-after
capture where the stylesheet changes between captures.

Comment on lines +87 to +90
fallback={
<CardFrame class={local.class}>
<StaticRow header={local.header} />
</CardFrame>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep tooltips for static cards.

ToolCard passes titleTooltip into this component. This fallback ignores local.tooltip, so a card without body content loses its title tooltip. Wrap StaticRow with the same tooltip behavior, or pass tooltip support into StaticRow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui-kit-chat/src/styled/collapsible-card.tsx` around lines 87 - 90,
Update the fallback branch in the collapsible-card component so static cards
preserve local.tooltip/titleTooltip behavior. Apply the same tooltip wrapper
used for regular card content around StaticRow, or pass the tooltip value into
StaticRow, while keeping the existing CardFrame and local.class behavior
unchanged.

Comment thread packages/ui-kit-chat/src/styled/element-preview.tsx

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 182 out of 186 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (6)

packages/ui-kit-chat/src/styled/element-preview.tsx:170

  • Replaying the captured CSS and DOM can issue network requests. The sanitizer preserves normal src/href URLs, and arbitrary page CSS can contain url() or @import; inserting them here causes the transcript viewer to fetch attacker-controlled resources even though the subtree is inert. Strip or safely rewrite all network-capable URLs in both serialized attributes and CSS before rebuilding, since inert only blocks interaction, not resource loading.
    packages/extensions/page/src/client/cards/shared.tsx:105
  • The specialized page cards bypass the structured declared-error handling added to MetaToolCard. For an MCP error payload such as {"error":{"message":"...","code":"NO_MATCH"}}, result.error is absent and this returns the entire JSON string, so every page card shows raw JSON instead of the declared message. Parse the structured payload here and resolve its code against the card metadata's errors, then pass that metadata from the page cards.
    packages/core/src/tool-registry.ts:38
  • Captures from ordinary direct chat tool calls are dropped here because buildChatTools creates the ToolRequest at packages/core/src/chat/runtime.ts:102 without a toolCallId. Only code-mode calls currently add one, so direct page.* calls can render without their stored capture. Thread the actual streamed tool-call ID into the execution request (and add a direct-chat integration test) before requiring it here.
    packages/page/src/element-capture.ts:108
  • The full target subtree is serialized without any size or node-count limit. A valid call targeting body or a large application root can therefore send and persist megabytes of DOM per side, despite CSS having a 512 KiB cap, causing transcript/database growth and UI stalls. Apply a capture budget and fall back to the descriptor-only representation when it is exceeded.
    packages/page/src/page-tool-dispatcher.ts:113
  • A before/after capture can reference two different stylesheet hashes when the tool changes page CSS, but this single field is overwritten by the last entry. The earlier capture then references a hash whose CSS was never sent or stored, so its frozen preview loses its original styling. Carry all newly encountered CSS bundles (or store CSS per capture side) instead of only the final one.
    packages/page/src/css-bundle.ts:42
  • This marks a stylesheet as shipped before the receiver acknowledges or persists it. If a page call has no tool-call ID (for example the current direct-chat/RPC paths) or storage transiently fails, the full bundle is discarded, and every later capture sends only the hash, leaving previews permanently without CSS. Either resend the small bundle with each capture and rely on DB conflict deduplication, or add an acknowledgement/reset mechanism before caching it client-side.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
packages/embed/test/element-capture.it.test.ts (1)

19-28: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Create a fresh session for each test.

sessionId is created once in beforeAll, so all four tests share one persisted capture list. Earlier tests can change the rows searched by later tests. Concurrent or retried tests can also make the count assertions flaky. Create and clean up a session in beforeEach/afterEach, or reset captures before each test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/embed/test/element-capture.it.test.ts` around lines 19 - 28, Update
the test setup around sessionId so each test receives an isolated fresh session
instead of sharing the session created in beforeAll. Move session creation into
beforeEach and clean up or reset the session state in afterEach, while keeping
browser, kit, and host initialization shared.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/embed/test/element-capture.it.test.ts`:
- Around line 84-90: Update the read-verb test around page.text and capturesFor
to compare the complete capture state before and after the call using stable row
identity and content, rather than querying the fabricated "never-minted" ID.
Preserve the assertion that page.text creates no capture and ensure the
comparison detects replacements as well as count changes.
- Around line 53-60: Extend the capture assertions after the existing
before-capture checks to locate the row with kind "after" and validate its
post-action state. Use the same selectorPath-filtered captures and assert the
after capture reflects the expected updated accessible name and DOM attributes,
ensuring a duplicated pre-action snapshot cannot pass.
- Around line 66-71: Strengthen the test “never lets a password value reach the
stored capture or the tool result” by locating captures for the page.fill call
using its actual tool-call ID or the `#secret` selector, asserting that both
before and after captures exist, and then checking every matching capture for
PASSWORD redaction alongside the existing result check.

In `@packages/ui-kit-chat/test/element-preview.browser.test.tsx`:
- Around line 44-46: Update the anchor fixture in the relevant browser test
cases to include a usable ID, trigger a click on that anchor before checking the
XSS flag, and then assert that window.${XSS_FLAG} remains unset. Apply the same
interaction and assertion sequence to both affected cases.

---

Nitpick comments:
In `@packages/embed/test/element-capture.it.test.ts`:
- Around line 19-28: Update the test setup around sessionId so each test
receives an isolated fresh session instead of sharing the session created in
beforeAll. Move session creation into beforeEach and clean up or reset the
session state in afterEach, while keeping browser, kit, and host initialization
shared.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 62ec716e-2f77-4b12-a54a-a03f89ce5ffb

📥 Commits

Reviewing files that changed from the base of the PR and between 9af0758 and e3c3692.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (185)
  • .fallowrc.json
  • apps/conciv/package.json
  • apps/conciv/src/pane/chat-pane.tsx
  • apps/conciv/src/pane/conciv-ui-card.tsx
  • apps/conciv/src/pane/session-captures.ts
  • apps/conciv/src/pane/tool-view-ctx.ts
  • apps/conciv/test/helpers/fake-core-router.ts
  • apps/conciv/test/helpers/fake-core.ts
  • apps/conciv/test/kit-controls.browser.test.tsx
  • apps/conciv/test/tool-card-dispatch.browser.test.tsx
  • apps/conciv/uno.config.ts
  • apps/storybook/.storybook/main.ts
  • apps/storybook/package.json
  • apps/storybook/uno.config.ts
  • docs/superpowers/specs/2026-08-08-page-tool-cards-design.md
  • docs/superpowers/specs/2026-08-09-conciv-ui-card-unification-design.md
  • packages/cli/src/tool-command.ts
  • packages/contract/src/contract.ts
  • packages/core/package.json
  • packages/core/src/api/execute-schemas.ts
  • packages/core/src/api/mcp.ts
  • packages/core/src/api/rpc/router.ts
  • packages/core/src/api/rpc/sessions.ts
  • packages/core/src/app.ts
  • packages/core/src/cards.tsx
  • packages/core/src/cards/code-run-card.stories.tsx
  • packages/core/src/cards/code-run-card.tsx
  • packages/core/src/chat/capabilities.ts
  • packages/core/src/chat/code-mode-parts.ts
  • packages/core/src/chat/code-mode.ts
  • packages/core/src/page-bus.ts
  • packages/core/src/tool-registry.ts
  • packages/core/test/api/page/tool-capture.it.test.ts
  • packages/core/test/builtin-tool-calls.test.ts
  • packages/core/test/chat/approving-call-post-result-traffic.it.test.ts
  • packages/core/test/chat/code-mode-parts.test.ts
  • packages/core/test/chat/code-mode-reload-fold.it.test.ts
  • packages/core/test/client-tool-gate.test.ts
  • packages/core/tsconfig.cards.build.json
  • packages/core/tsconfig.cards.json
  • packages/core/uno.config.ts
  • packages/core/vite.config.ts
  • packages/db/drizzle/20260808214400_tool_captures/migration.sql
  • packages/db/drizzle/20260808214400_tool_captures/snapshot.json
  • packages/db/src/capture-queries.ts
  • packages/db/src/index.ts
  • packages/db/src/run-queries.ts
  • packages/db/src/schema.ts
  • packages/db/test/capture-queries.test.ts
  • packages/embed/test/element-capture.it.test.ts
  • packages/embed/test/embed.it.test.ts
  • packages/embed/test/helpers/page-plane-host.ts
  • packages/embed/test/mount-externals.test.ts
  • packages/embed/uno.config.ts
  • packages/embed/vite.config.ts
  • packages/extension-testkit/src/card-harness.tsx
  • packages/extension/src/collect-client.ts
  • packages/extension/src/define-tool.ts
  • packages/extension/src/tool-registry.ts
  • packages/extension/src/types.ts
  • packages/extension/test/tool-registry.test.ts
  • packages/extensions/page/.gitignore
  • packages/extensions/page/package.json
  • packages/extensions/page/src/client.tsx
  • packages/extensions/page/src/client/bodies.ts
  • packages/extensions/page/src/client/cards/act-card.stories.tsx
  • packages/extensions/page/src/client/cards/act-card.tsx
  • packages/extensions/page/src/client/cards/console-card.stories.tsx
  • packages/extensions/page/src/client/cards/console-card.tsx
  • packages/extensions/page/src/client/cards/edit-live-card.stories.tsx
  • packages/extensions/page/src/client/cards/edit-live-card.tsx
  • packages/extensions/page/src/client/cards/effect-card.stories.tsx
  • packages/extensions/page/src/client/cards/effect-card.tsx
  • packages/extensions/page/src/client/cards/react-card.stories.tsx
  • packages/extensions/page/src/client/cards/react-card.tsx
  • packages/extensions/page/src/client/cards/read-bulk-card.stories.tsx
  • packages/extensions/page/src/client/cards/read-bulk-card.tsx
  • packages/extensions/page/src/client/cards/read-value-card.stories.tsx
  • packages/extensions/page/src/client/cards/read-value-card.tsx
  • packages/extensions/page/src/client/cards/shared.tsx
  • packages/extensions/page/src/client/cards/story.fixtures.ts
  • packages/extensions/page/src/client/js-beautify-html.d.ts
  • packages/extensions/page/src/client/page-format.ts
  • packages/extensions/page/src/client/page-result-views.stories.tsx
  • packages/extensions/page/src/client/page-result-views.tsx
  • packages/extensions/page/src/shared/defs.ts
  • packages/extensions/page/test/defs.test.ts
  • packages/extensions/page/test/tsconfig.json
  • packages/extensions/page/tsconfig.build.json
  • packages/extensions/page/tsconfig.json
  • packages/extensions/page/tsconfig.refs.json
  • packages/extensions/page/tsdown.config.ts
  • packages/extensions/page/uno.config.ts
  • packages/extensions/page/vite.config.ts
  • packages/extensions/test-runner/test/test-card.browser.test.tsx
  • packages/extensions/whiteboard/src/client/model/comments.tsx
  • packages/harness-testkit/src/call-tool.ts
  • packages/page/package.json
  • packages/page/src/css-bundle.ts
  • packages/page/src/element-capture.ts
  • packages/page/src/element-descriptor.ts
  • packages/page/src/page-driver.ts
  • packages/page/src/page-snapshot.ts
  • packages/page/src/page-tool-dispatcher.ts
  • packages/page/test/css-bundle.test.ts
  • packages/page/test/element-capture.browser.test.ts
  • packages/page/test/page-dispatcher.browser.test.ts
  • packages/protocol/package.json
  • packages/protocol/src/chat-types.ts
  • packages/protocol/src/done-types.ts
  • packages/protocol/src/element-capture-types.ts
  • packages/protocol/src/page-types.ts
  • packages/protocol/src/tool-view-types.ts
  • packages/protocol/tsdown.config.ts
  • packages/tools/package.json
  • packages/tools/src/cards.tsx
  • packages/tools/src/cards/extensions-card.tsx
  • packages/tools/src/cards/inline-cards.stories.tsx
  • packages/tools/src/cards/open-card.tsx
  • packages/tools/src/cards/ui-card.stories.tsx
  • packages/tools/src/cards/ui-card.tsx
  • packages/tools/tsconfig.cards.build.json
  • packages/tools/tsconfig.cards.json
  • packages/tools/uno.config.ts
  • packages/tools/vite.config.ts
  • packages/ui-kit-chat-tools/package.json
  • packages/ui-kit-chat-tools/src/index.tsx
  • packages/ui-kit-chat-tools/src/primitives/tools/inline-tool.tsx
  • packages/ui-kit-chat-tools/src/primitives/tools/now-title.ts
  • packages/ui-kit-chat-tools/src/styled/done-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/done-card.tsx
  • packages/ui-kit-chat-tools/src/styled/page-action-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/page-action-card.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/apply-patch-diff.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/bash-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/builtin-tool-cards.ts
  • packages/ui-kit-chat-tools/src/styled/tools/discovered-apis-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/file-read-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/inline-tool.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/inline-tool.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/loaded-tools-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/loaded-tools-card.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/todo-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/tool-chip.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/tool-lookup-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/ui-chip-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/ui-chip-card.tsx
  • packages/ui-kit-chat-tools/test/catalog-cards.browser.test.tsx
  • packages/ui-kit-chat-tools/test/new-tool-projection.browser.test.tsx
  • packages/ui-kit-chat-tools/test/page-action-card-title.browser.test.tsx
  • packages/ui-kit-chat-tools/test/page-tool-cards.browser.test.tsx
  • packages/ui-kit-chat-tools/test/registry-card-declarations.browser.test.tsx
  • packages/ui-kit-chat-tools/test/schema-params.test.ts
  • packages/ui-kit-chat/package.json
  • packages/ui-kit-chat/src/index.tsx
  • packages/ui-kit-chat/src/primitives/message/message.tsx
  • packages/ui-kit-chat/src/primitives/tools/schema-params.ts
  • packages/ui-kit-chat/src/primitives/tools/tool-presentation.ts
  • packages/ui-kit-chat/src/store/element-capture.fixtures.ts
  • packages/ui-kit-chat/src/store/tool-context.tsx
  • packages/ui-kit-chat/src/styled/chip.tsx
  • packages/ui-kit-chat/src/styled/collapsible-card.tsx
  • packages/ui-kit-chat/src/styled/element-preview.stories.tsx
  • packages/ui-kit-chat/src/styled/element-preview.tsx
  • packages/ui-kit-chat/src/styled/json-tree.tsx
  • packages/ui-kit-chat/src/styled/tool-card.tsx
  • packages/ui-kit-chat/src/styled/tool-fallback.stories.tsx
  • packages/ui-kit-chat/src/styled/tool-icon.tsx
  • packages/ui-kit-chat/src/styled/tools/inline-row.tsx
  • packages/ui-kit-chat/src/styled/tools/meta-tool-card.stories.tsx
  • packages/ui-kit-chat/src/styled/tools/meta-tool-card.tsx
  • packages/ui-kit-chat/src/styled/tools/note-row.tsx
  • packages/ui-kit-chat/src/styled/tools/permission-card.stories.tsx
  • packages/ui-kit-chat/src/styled/tools/permission-card.tsx
  • packages/ui-kit-chat/src/styled/tools/tool-call-card.tsx
  • packages/ui-kit-chat/test/collapsible-card-shape.browser.test.tsx
  • packages/ui-kit-chat/test/element-preview.browser.test.tsx
  • packages/ui-kit-chat/test/meta-tool-card-error.browser.test.tsx
  • packages/ui-kit-chat/test/schema-params.test.ts
  • packages/ui-kit-chat/test/tool-call-card-dispatch.browser.test.tsx
  • packages/ui-kit-system/src/index.tsx
  • packages/ui-kit-system/src/json-tree-view.tsx
  • packages/ui-kit-system/src/select.tsx
  • packages/uno-preset/src/index.ts
  • packages/uno-preset/src/json-tree.ts
💤 Files with no reviewable changes (13)
  • packages/ui-kit-chat-tools/src/styled/done-card.tsx
  • packages/protocol/src/done-types.ts
  • packages/ui-kit-chat-tools/test/page-action-card-title.browser.test.tsx
  • packages/ui-kit-chat-tools/test/schema-params.test.ts
  • packages/ui-kit-chat-tools/src/styled/ui-chip-card.stories.tsx
  • apps/conciv/src/pane/conciv-ui-card.tsx
  • packages/ui-kit-chat-tools/src/styled/page-action-card.tsx
  • packages/ui-kit-chat-tools/src/styled/ui-chip-card.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/builtin-tool-cards.ts
  • packages/ui-kit-chat-tools/src/styled/page-action-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/done-card.stories.tsx
  • packages/ui-kit-chat-tools/package.json
  • packages/ui-kit-chat-tools/src/primitives/tools/inline-tool.tsx
🚧 Files skipped from review as they are similar to previous changes (159)
  • packages/ui-kit-chat-tools/src/styled/tools/loaded-tools-card.tsx
  • apps/storybook/.storybook/main.ts
  • packages/ui-kit-chat-tools/src/styled/tools/loaded-tools-card.stories.tsx
  • packages/embed/vite.config.ts
  • packages/page/package.json
  • packages/core/src/chat/code-mode.ts
  • packages/core/tsconfig.cards.build.json
  • packages/extensions/page/.gitignore
  • packages/tools/tsconfig.cards.build.json
  • packages/core/src/api/rpc/sessions.ts
  • packages/extensions/page/src/client/js-beautify-html.d.ts
  • packages/ui-kit-chat/src/styled/tools/permission-card.tsx
  • packages/ui-kit-chat/src/styled/tool-fallback.stories.tsx
  • packages/cli/src/tool-command.ts
  • packages/ui-kit-system/src/index.tsx
  • packages/extensions/page/test/tsconfig.json
  • packages/extension/src/define-tool.ts
  • packages/embed/test/mount-externals.test.ts
  • .fallowrc.json
  • packages/extensions/page/src/client/cards/console-card.tsx
  • packages/protocol/tsdown.config.ts
  • packages/tools/src/cards/inline-cards.stories.tsx
  • packages/ui-kit-chat/test/collapsible-card-shape.browser.test.tsx
  • packages/core/test/chat/approving-call-post-result-traffic.it.test.ts
  • apps/storybook/uno.config.ts
  • packages/extensions/whiteboard/src/client/model/comments.tsx
  • packages/page/test/page-dispatcher.browser.test.ts
  • packages/ui-kit-chat-tools/src/styled/tools/tool-chip.stories.tsx
  • packages/core/test/client-tool-gate.test.ts
  • packages/ui-kit-system/src/json-tree-view.tsx
  • packages/extensions/page/src/client/cards/effect-card.tsx
  • packages/core/test/chat/code-mode-reload-fold.it.test.ts
  • packages/ui-kit-chat/src/styled/tools/meta-tool-card.tsx
  • packages/extensions/page/src/client/page-format.ts
  • apps/conciv/uno.config.ts
  • packages/uno-preset/src/json-tree.ts
  • packages/core/src/chat/code-mode-parts.ts
  • apps/conciv/src/pane/session-captures.ts
  • packages/core/src/chat/capabilities.ts
  • packages/ui-kit-chat/src/styled/tool-card.tsx
  • packages/db/drizzle/20260808214400_tool_captures/snapshot.json
  • packages/core/vite.config.ts
  • packages/protocol/src/chat-types.ts
  • packages/extension/src/types.ts
  • packages/extensions/page/src/client/cards/read-value-card.tsx
  • packages/extensions/page/src/client/cards/read-value-card.stories.tsx
  • packages/ui-kit-chat/src/index.tsx
  • packages/extension-testkit/src/card-harness.tsx
  • packages/extensions/page/src/client/cards/edit-live-card.stories.tsx
  • packages/uno-preset/src/index.ts
  • apps/conciv/test/helpers/fake-core.ts
  • packages/db/src/run-queries.ts
  • packages/db/drizzle/20260808214400_tool_captures/migration.sql
  • packages/core/src/app.ts
  • packages/embed/test/helpers/page-plane-host.ts
  • packages/db/test/capture-queries.test.ts
  • packages/extensions/page/tsconfig.json
  • packages/core/uno.config.ts
  • packages/extensions/page/tsconfig.refs.json
  • packages/extensions/page/src/client/cards/act-card.tsx
  • packages/extensions/page/src/client/cards/act-card.stories.tsx
  • packages/protocol/package.json
  • packages/extensions/page/tsconfig.build.json
  • packages/ui-kit-chat/src/styled/tools/permission-card.stories.tsx
  • packages/tools/src/cards.tsx
  • apps/conciv/test/kit-controls.browser.test.tsx
  • apps/conciv/test/helpers/fake-core-router.ts
  • packages/page/test/css-bundle.test.ts
  • packages/ui-kit-chat/src/styled/tools/note-row.tsx
  • packages/tools/src/cards/ui-card.stories.tsx
  • docs/superpowers/specs/2026-08-09-conciv-ui-card-unification-design.md
  • packages/embed/test/embed.it.test.ts
  • packages/protocol/src/page-types.ts
  • packages/extensions/page/src/client/cards/read-bulk-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/inline-tool.stories.tsx
  • packages/tools/src/cards/open-card.tsx
  • packages/ui-kit-chat/test/meta-tool-card-error.browser.test.tsx
  • packages/extensions/test-runner/test/test-card.browser.test.tsx
  • packages/page/src/page-driver.ts
  • packages/core/src/tool-registry.ts
  • packages/core/tsconfig.cards.json
  • packages/extensions/page/tsdown.config.ts
  • packages/extension/test/tool-registry.test.ts
  • packages/tools/tsconfig.cards.json
  • packages/harness-testkit/src/call-tool.ts
  • packages/ui-kit-chat-tools/src/styled/tools/tool-lookup-card.stories.tsx
  • packages/ui-kit-chat/package.json
  • packages/ui-kit-chat/src/styled/chip.tsx
  • packages/ui-kit-chat/src/store/element-capture.fixtures.ts
  • packages/extensions/page/src/client/page-result-views.tsx
  • packages/ui-kit-chat-tools/test/page-tool-cards.browser.test.tsx
  • apps/conciv/package.json
  • packages/core/src/cards/code-run-card.tsx
  • packages/page/src/page-snapshot.ts
  • packages/core/src/cards/code-run-card.stories.tsx
  • packages/extensions/page/src/client/cards/react-card.tsx
  • packages/db/src/index.ts
  • packages/extensions/page/uno.config.ts
  • packages/ui-kit-chat/test/tool-call-card-dispatch.browser.test.tsx
  • packages/extensions/page/test/defs.test.ts
  • apps/conciv/src/pane/tool-view-ctx.ts
  • packages/extension/src/collect-client.ts
  • packages/core/src/api/mcp.ts
  • packages/ui-kit-chat/src/primitives/tools/schema-params.ts
  • packages/page/src/css-bundle.ts
  • packages/core/test/builtin-tool-calls.test.ts
  • packages/tools/vite.config.ts
  • packages/extensions/page/src/client/cards/react-card.stories.tsx
  • packages/core/src/api/execute-schemas.ts
  • packages/ui-kit-chat/src/primitives/tools/tool-presentation.ts
  • packages/protocol/src/tool-view-types.ts
  • packages/protocol/src/element-capture-types.ts
  • packages/ui-kit-chat/src/styled/collapsible-card.tsx
  • packages/ui-kit-chat-tools/src/primitives/tools/now-title.ts
  • packages/contract/src/contract.ts
  • packages/extensions/page/src/client/cards/read-bulk-card.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/file-read-card.stories.tsx
  • packages/extensions/page/src/client/cards/effect-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/discovered-apis-card.stories.tsx
  • packages/tools/package.json
  • packages/ui-kit-chat-tools/src/index.tsx
  • packages/extension/src/tool-registry.ts
  • packages/page/src/element-descriptor.ts
  • packages/ui-kit-chat/src/styled/tools/meta-tool-card.stories.tsx
  • packages/ui-kit-chat/src/styled/tools/tool-call-card.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/inline-tool.tsx
  • packages/ui-kit-chat-tools/test/registry-card-declarations.browser.test.tsx
  • packages/ui-kit-chat-tools/test/new-tool-projection.browser.test.tsx
  • packages/extensions/page/src/client/cards/story.fixtures.ts
  • packages/extensions/page/src/client/page-result-views.stories.tsx
  • packages/ui-kit-chat/src/styled/element-preview.tsx
  • packages/extensions/page/vite.config.ts
  • packages/db/src/schema.ts
  • packages/extensions/page/src/client/cards/shared.tsx
  • packages/page/src/page-tool-dispatcher.ts
  • packages/extensions/page/package.json
  • apps/conciv/test/tool-card-dispatch.browser.test.tsx
  • packages/ui-kit-chat/src/styled/element-preview.stories.tsx
  • packages/embed/uno.config.ts
  • packages/extensions/page/src/client/cards/edit-live-card.tsx
  • packages/extensions/page/src/shared/defs.ts
  • apps/conciv/src/pane/chat-pane.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/todo-card.stories.tsx
  • packages/ui-kit-chat-tools/src/styled/tools/apply-patch-diff.stories.tsx
  • packages/extensions/page/src/client/cards/console-card.stories.tsx
  • packages/ui-kit-chat/test/schema-params.test.ts
  • packages/core/src/cards.tsx
  • packages/ui-kit-chat/src/primitives/message/message.tsx
  • packages/core/src/api/rpc/router.ts
  • packages/core/package.json
  • packages/core/src/page-bus.ts
  • apps/storybook/package.json
  • packages/tools/src/cards/extensions-card.tsx
  • packages/extensions/page/src/client.tsx
  • packages/db/src/capture-queries.ts
  • packages/ui-kit-chat-tools/src/styled/tools/bash-card.stories.tsx
  • packages/core/test/api/page/tool-capture.it.test.ts
  • packages/ui-kit-chat/src/store/tool-context.tsx
  • packages/core/test/chat/code-mode-parts.test.ts

Comment on lines +53 to +60
const stored: SessionCaptures = await kit.rpc.captures.list({sessionId})
const edit = stored.captures.filter((row) => row.capture.descriptor.selectorPath.includes('prose'))
expect(edit.map((row) => row.kind).toSorted()).toEqual(['after', 'before'])
const before = edit.find((row) => row.kind === 'before')
expect(before?.capture.descriptor.accessibleName).toBe('original prose')
expect(JSON.stringify(before?.capture.node)).toContain('theme-light')
expect(JSON.stringify(before?.capture.node)).not.toContain('theme-dark')
expect(JSON.stringify(before?.capture.node)).toContain('data-rr-target')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Assert the post-action capture.

The test proves that both capture kinds exist at Line 55, but it only inspects before at Lines 56-60. A regression that stores the pre-action snapshot twice will pass.

Proposed assertion
 const before = edit.find((row) => row.kind === 'before')
+const after = edit.find((row) => row.kind === 'after')
 expect(before?.capture.descriptor.accessibleName).toBe('original prose')
+expect(after?.capture.descriptor.accessibleName).toBe('rewritten by the agent')
 expect(JSON.stringify(before?.capture.node)).toContain('theme-light')
 expect(JSON.stringify(before?.capture.node)).not.toContain('theme-dark')
+expect(JSON.stringify(after?.capture.node)).toContain('theme-light')
+expect(JSON.stringify(after?.capture.node)).not.toContain('theme-dark')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const stored: SessionCaptures = await kit.rpc.captures.list({sessionId})
const edit = stored.captures.filter((row) => row.capture.descriptor.selectorPath.includes('prose'))
expect(edit.map((row) => row.kind).toSorted()).toEqual(['after', 'before'])
const before = edit.find((row) => row.kind === 'before')
expect(before?.capture.descriptor.accessibleName).toBe('original prose')
expect(JSON.stringify(before?.capture.node)).toContain('theme-light')
expect(JSON.stringify(before?.capture.node)).not.toContain('theme-dark')
expect(JSON.stringify(before?.capture.node)).toContain('data-rr-target')
const stored: SessionCaptures = await kit.rpc.captures.list({sessionId})
const edit = stored.captures.filter((row) => row.capture.descriptor.selectorPath.includes('prose'))
expect(edit.map((row) => row.kind).toSorted()).toEqual(['after', 'before'])
const before = edit.find((row) => row.kind === 'before')
const after = edit.find((row) => row.kind === 'after')
expect(before?.capture.descriptor.accessibleName).toBe('original prose')
expect(after?.capture.descriptor.accessibleName).toBe('rewritten by the agent')
expect(JSON.stringify(before?.capture.node)).toContain('theme-light')
expect(JSON.stringify(before?.capture.node)).not.toContain('theme-dark')
expect(JSON.stringify(after?.capture.node)).toContain('theme-light')
expect(JSON.stringify(after?.capture.node)).not.toContain('theme-dark')
expect(JSON.stringify(before?.capture.node)).toContain('data-rr-target')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/embed/test/element-capture.it.test.ts` around lines 53 - 60, Extend
the capture assertions after the existing before-capture checks to locate the
row with kind "after" and validate its post-action state. Use the same
selectorPath-filtered captures and assert the after capture reflects the
expected updated accessible name and DOM attributes, ensuring a duplicated
pre-action snapshot cannot pass.

Comment on lines +66 to +71
it('never lets a password value reach the stored capture or the tool result', async () => {
const page = await openHostPage()
const result = await kit.callTool('page.fill', {selector: '#secret', value: 'typed by the agent'}, sessionId)
const stored: SessionCaptures = await kit.rpc.captures.list({sessionId})
expect(JSON.stringify(result)).not.toContain(PASSWORD)
expect(JSON.stringify(stored)).not.toContain(PASSWORD)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Verify both password captures before checking redaction.

The assertions only search the full result and session. They pass if page.fill stores no capture or stores only the after row. Filter by the actual tool-call ID, or by the #secret selector, assert before and after, then check every capture for PASSWORD.

Proposed assertions
 const stored: SessionCaptures = await kit.rpc.captures.list({sessionId})
+const secretCaptures = stored.captures.filter((row) =>
+  row.capture.descriptor.selectorPath.includes('secret'),
+)
+expect(secretCaptures.map((row) => row.kind).toSorted()).toEqual(['after', 'before'])
+for (const row of secretCaptures) {
+  expect(JSON.stringify(row.capture)).not.toContain(PASSWORD)
+}
 expect(JSON.stringify(result)).not.toContain(PASSWORD)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('never lets a password value reach the stored capture or the tool result', async () => {
const page = await openHostPage()
const result = await kit.callTool('page.fill', {selector: '#secret', value: 'typed by the agent'}, sessionId)
const stored: SessionCaptures = await kit.rpc.captures.list({sessionId})
expect(JSON.stringify(result)).not.toContain(PASSWORD)
expect(JSON.stringify(stored)).not.toContain(PASSWORD)
it('never lets a password value reach the stored capture or the tool result', async () => {
const page = await openHostPage()
const result = await kit.callTool('page.fill', {selector: '`#secret`', value: 'typed by the agent'}, sessionId)
const stored: SessionCaptures = await kit.rpc.captures.list({sessionId})
const secretCaptures = stored.captures.filter((row) =>
row.capture.descriptor.selectorPath.includes('secret'),
)
expect(secretCaptures.map((row) => row.kind).toSorted()).toEqual(['after', 'before'])
for (const row of secretCaptures) {
expect(JSON.stringify(row.capture)).not.toContain(PASSWORD)
}
expect(JSON.stringify(result)).not.toContain(PASSWORD)
expect(JSON.stringify(stored)).not.toContain(PASSWORD)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/embed/test/element-capture.it.test.ts` around lines 66 - 71,
Strengthen the test “never lets a password value reach the stored capture or the
tool result” by locating captures for the page.fill call using its actual
tool-call ID or the `#secret` selector, asserting that both before and after
captures exist, and then checking every matching capture for PASSWORD redaction
alongside the existing result check.

Comment on lines +84 to +90
it('takes no capture for a read verb', async () => {
const page = await openHostPage()
const before: SessionCaptures = await kit.rpc.captures.list({sessionId})
await kit.callTool('page.text', {selector: '#cta'}, sessionId)
const after: SessionCaptures = await kit.rpc.captures.list({sessionId})
expect(after.captures.length).toBe(before.captures.length)
expect(capturesFor(after, 'never-minted')).toEqual([])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Compare the actual capture state before and after the read.

capturesFor(after, 'never-minted') does not inspect the page.text call at Line 87. It returns an empty array unless a row has that fabricated ID. The count check at Line 89 can also miss one row being replaced by another. Compare the capture rows by stable identity and content, or use the actual tool-call ID.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/embed/test/element-capture.it.test.ts` around lines 84 - 90, Update
the read-verb test around page.text and capturesFor to compare the complete
capture state before and after the call using stable row identity and content,
rather than querying the fabricated "never-minted" ID. Preserve the assertion
that page.text creates no capture and ensure the comparison detects replacements
as well as count changes.

Comment on lines +44 to +46
tagName: 'a',
attributes: {href: `javascript:window.${XSS_FLAG} = true`, onclick: `window.${XSS_FLAG} = true`},
childNodes: [{type: 3, textContent: 'click me', id: 3}],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Trigger the anchor with onclick before the XSS assertion.

The test never activates the anchor at Line 44. A regression that preserves onclick can pass because the handler does not run. Give this anchor an ID, trigger a click, and then assert that window.${XSS_FLAG} remains unset.

Proposed test update
-                attributes: {href: `javascript:window.${XSS_FLAG} = true`, onclick: `window.${XSS_FLAG} = true`},
+                attributes: {
+                  id: 'hostile-click-link',
+                  href: `javascript:window.${XSS_FLAG} = true`,
+                  onclick: `window.${XSS_FLAG} = true`,
+                },
...
   const shadow = replicaShadowRoot()
   expect(shadow.querySelector('iframe')).toBeNull()
+  const clickLink = shadow.getElementById('hostile-click-link')
+  if (clickLink === null) throw new Error('the hostile click link was rebuilt')
+  clickLink.click()
+  expect(Reflect.get(window, XSS_FLAG)).toBeUndefined()
 
   const tabLink = shadow.getElementById('hostile-tab-link')

Also applies to: 197-202

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui-kit-chat/test/element-preview.browser.test.tsx` around lines 44 -
46, Update the anchor fixture in the relevant browser test cases to include a
usable ID, trigger a click on that anchor before checking the XSS flag, and then
assert that window.${XSS_FLAG} remains unset. Apply the same interaction and
assertion sequence to both affected cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants