feat(tool-cards): render the element page/extension tools acted on - #348
feat(tool-cards): render the element page/extension tools acted on#348omridevk wants to merge 22 commits into
Conversation
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>
📝 WalkthroughWalkthroughThe 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. ChangesTool cards and frozen captures
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
⚔️ Resolve merge conflicts 💡
🧪 Generate unit tests (beta)
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
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Comment |
There was a problem hiding this comment.
Pull request overview
Adds metadata-driven tool cards so declared page and extension tools no longer fall back to raw tool rendering.
Changes:
- Adds
MetaToolCardand layered card dispatch with approval support. - Expands registry metadata with schemas, errors, and approval details.
- Removes obsolete
conciv_pagecards 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.
| 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 |
| <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')) |
…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>
There was a problem hiding this comment.
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-containerimplementation 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 necessarilyundefined, so an existingpage.*call temporarily renders throughToolFallback(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
sis an abbreviated identifier, which this repository’s TypeScript conventions prohibit. Rename it to a self-explanatory name such asselectorand 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-containertag 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>insideCollapsible.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>
)}
…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: 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>
There was a problem hiding this comment.
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
bodycan persist an unbounded rrweb tree; every session load then returns all such trees throughcaptures.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;takeElementCapturethen 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.
makeCssBundleDeduperremembers 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 PageCaptureBundlehas only onecssBundle, but this loop can capture different stylesheets before and after an edit. The second new hash overwrites the first bundle while thebeforecapture still references its old hash, andToolCaptureViewthen 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-chatentry 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.
| payload: text('payload', {mode: 'json'}).$type<ElementCapture>().notNull(), | ||
| createdAt: integer('created_at').notNull(), | ||
| }, | ||
| (table) => [primaryKey({name: 'tool_captures_pk', columns: [table.toolCallId, table.kind]})], |
| delete attributes[name] | ||
| continue | ||
| } | ||
| if (URL_ATTRIBUTES.has(lowered) && typeof value === 'string' && isJavascriptUrl(value)) delete attributes[name] | ||
| } |
There was a problem hiding this comment.
Actionable comments posted: 15
🧹 Nitpick comments (8)
packages/ui-kit-chat/test/element-preview.browser.test.tsx (1)
180-203: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winExercise each hostile execution path.
This test does not click either link. It does not assert that
onerrorandonclickattributes are removed. It only checks the malformedjava\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 thatXSS_FLAGremains 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 winAdd a non-unique
session_idindex and generate the migration.
sessionCapturesanddeleteSessionCapturesfilter bytool_captures.session_id. The composite primary key(tool_call_id, kind)cannot support these lookups, so SQLite scans the table. Declareindex('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 | 🔵 TrivialConsider indexing
tool_captures.session_id.The snapshot declares only the composite primary key
(tool_call_id, kind). Capture reload and session cleanup filter bysession_id, which requires a full table scan without an index. Add an index inpackages/db/src/schema.tsand regenerate the migration if those access paths exist. The same applies tocss_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 winAlso 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 onerrorand theiframe srcdocscript 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 winAssert the tool output and the latency the test name claims.
JSON.stringify(outcome)contains'target'even if the outcome only echoes the inputelementId. The test also states "resolves quickly", but only the 8 s timeout bounds the duration, and the noise stream lasts about 900 ms. Assert theremovedfield 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 winInline only
@conciv/core/cards.
@conciv/coreresolves to the server entry, while the widget uses the browser-safe@conciv/core/cardsentry. The current prefix also inlines@conciv/core/appand 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 valueRemove the
as constassertion.
MIRROR_VERBSis 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 prohibitsas.🤖 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 winThe inherited
captureprop is silently ignored.
ToolCallCardPropsextendsOmit<ToolCardProps, 'addResult'>, so it still declares an optionalcapturefield. Line 35 always resolves the capture fromprops.ctx.captureFor. A caller that passescapturedirectly gets no effect and no type error.Either prefer the explicit prop, or omit
capturefrom 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (185)
.fallowrc.jsonapps/conciv/package.jsonapps/conciv/src/pane/chat-pane.tsxapps/conciv/src/pane/conciv-ui-card.tsxapps/conciv/src/pane/session-captures.tsapps/conciv/src/pane/tool-view-ctx.tsapps/conciv/test/helpers/fake-core-router.tsapps/conciv/test/helpers/fake-core.tsapps/conciv/test/kit-controls.browser.test.tsxapps/conciv/test/tool-card-dispatch.browser.test.tsxapps/conciv/uno.config.tsapps/storybook/.storybook/main.tsapps/storybook/package.jsonapps/storybook/uno.config.tsdocs/superpowers/specs/2026-08-08-page-tool-cards-design.mddocs/superpowers/specs/2026-08-09-conciv-ui-card-unification-design.mdpackages/cli/src/tool-command.tspackages/contract/src/contract.tspackages/core/package.jsonpackages/core/src/api/execute-schemas.tspackages/core/src/api/mcp.tspackages/core/src/api/rpc/router.tspackages/core/src/api/rpc/sessions.tspackages/core/src/app.tspackages/core/src/cards.tsxpackages/core/src/cards/code-run-card.stories.tsxpackages/core/src/cards/code-run-card.tsxpackages/core/src/chat/capabilities.tspackages/core/src/chat/code-mode-parts.tspackages/core/src/chat/code-mode.tspackages/core/src/page-bus.tspackages/core/src/tool-registry.tspackages/core/test/api/page/tool-capture.it.test.tspackages/core/test/builtin-tool-calls.test.tspackages/core/test/chat/approving-call-post-result-traffic.it.test.tspackages/core/test/chat/code-mode-parts.test.tspackages/core/test/chat/code-mode-reload-fold.it.test.tspackages/core/test/client-tool-gate.test.tspackages/core/tsconfig.cards.build.jsonpackages/core/tsconfig.cards.jsonpackages/core/uno.config.tspackages/core/vite.config.tspackages/db/drizzle/20260808214400_tool_captures/migration.sqlpackages/db/drizzle/20260808214400_tool_captures/snapshot.jsonpackages/db/src/capture-queries.tspackages/db/src/index.tspackages/db/src/run-queries.tspackages/db/src/schema.tspackages/db/test/capture-queries.test.tspackages/embed/test/element-capture.it.test.tspackages/embed/test/embed.it.test.tspackages/embed/test/helpers/page-plane-host.tspackages/embed/test/mount-externals.test.tspackages/embed/uno.config.tspackages/embed/vite.config.tspackages/extension-testkit/src/card-harness.tsxpackages/extension/src/collect-client.tspackages/extension/src/define-tool.tspackages/extension/src/tool-registry.tspackages/extension/src/types.tspackages/extension/test/tool-registry.test.tspackages/extensions/page/.gitignorepackages/extensions/page/package.jsonpackages/extensions/page/src/client.tsxpackages/extensions/page/src/client/bodies.tspackages/extensions/page/src/client/cards/act-card.stories.tsxpackages/extensions/page/src/client/cards/act-card.tsxpackages/extensions/page/src/client/cards/console-card.stories.tsxpackages/extensions/page/src/client/cards/console-card.tsxpackages/extensions/page/src/client/cards/edit-live-card.stories.tsxpackages/extensions/page/src/client/cards/edit-live-card.tsxpackages/extensions/page/src/client/cards/effect-card.stories.tsxpackages/extensions/page/src/client/cards/effect-card.tsxpackages/extensions/page/src/client/cards/react-card.stories.tsxpackages/extensions/page/src/client/cards/react-card.tsxpackages/extensions/page/src/client/cards/read-bulk-card.stories.tsxpackages/extensions/page/src/client/cards/read-bulk-card.tsxpackages/extensions/page/src/client/cards/read-value-card.stories.tsxpackages/extensions/page/src/client/cards/read-value-card.tsxpackages/extensions/page/src/client/cards/shared.tsxpackages/extensions/page/src/client/cards/story.fixtures.tspackages/extensions/page/src/client/js-beautify-html.d.tspackages/extensions/page/src/client/page-format.tspackages/extensions/page/src/client/page-result-views.stories.tsxpackages/extensions/page/src/client/page-result-views.tsxpackages/extensions/page/src/shared/defs.tspackages/extensions/page/test/defs.test.tspackages/extensions/page/test/tsconfig.jsonpackages/extensions/page/tsconfig.build.jsonpackages/extensions/page/tsconfig.jsonpackages/extensions/page/tsconfig.refs.jsonpackages/extensions/page/tsdown.config.tspackages/extensions/page/uno.config.tspackages/extensions/page/vite.config.tspackages/extensions/test-runner/test/test-card.browser.test.tsxpackages/extensions/whiteboard/src/client/model/comments.tsxpackages/harness-testkit/src/call-tool.tspackages/page/package.jsonpackages/page/src/css-bundle.tspackages/page/src/element-capture.tspackages/page/src/element-descriptor.tspackages/page/src/page-driver.tspackages/page/src/page-snapshot.tspackages/page/src/page-tool-dispatcher.tspackages/page/test/css-bundle.test.tspackages/page/test/element-capture.browser.test.tspackages/page/test/page-dispatcher.browser.test.tspackages/protocol/package.jsonpackages/protocol/src/chat-types.tspackages/protocol/src/done-types.tspackages/protocol/src/element-capture-types.tspackages/protocol/src/page-types.tspackages/protocol/src/tool-view-types.tspackages/protocol/tsdown.config.tspackages/tools/package.jsonpackages/tools/src/cards.tsxpackages/tools/src/cards/extensions-card.tsxpackages/tools/src/cards/inline-cards.stories.tsxpackages/tools/src/cards/open-card.tsxpackages/tools/src/cards/ui-card.stories.tsxpackages/tools/src/cards/ui-card.tsxpackages/tools/tsconfig.cards.build.jsonpackages/tools/tsconfig.cards.jsonpackages/tools/uno.config.tspackages/tools/vite.config.tspackages/ui-kit-chat-tools/package.jsonpackages/ui-kit-chat-tools/src/index.tsxpackages/ui-kit-chat-tools/src/primitives/tools/inline-tool.tsxpackages/ui-kit-chat-tools/src/primitives/tools/now-title.tspackages/ui-kit-chat-tools/src/styled/done-card.stories.tsxpackages/ui-kit-chat-tools/src/styled/done-card.tsxpackages/ui-kit-chat-tools/src/styled/page-action-card.stories.tsxpackages/ui-kit-chat-tools/src/styled/page-action-card.tsxpackages/ui-kit-chat-tools/src/styled/tools/apply-patch-diff.stories.tsxpackages/ui-kit-chat-tools/src/styled/tools/bash-card.stories.tsxpackages/ui-kit-chat-tools/src/styled/tools/builtin-tool-cards.tspackages/ui-kit-chat-tools/src/styled/tools/discovered-apis-card.stories.tsxpackages/ui-kit-chat-tools/src/styled/tools/file-read-card.stories.tsxpackages/ui-kit-chat-tools/src/styled/tools/inline-tool.stories.tsxpackages/ui-kit-chat-tools/src/styled/tools/inline-tool.tsxpackages/ui-kit-chat-tools/src/styled/tools/loaded-tools-card.stories.tsxpackages/ui-kit-chat-tools/src/styled/tools/loaded-tools-card.tsxpackages/ui-kit-chat-tools/src/styled/tools/todo-card.stories.tsxpackages/ui-kit-chat-tools/src/styled/tools/tool-chip.stories.tsxpackages/ui-kit-chat-tools/src/styled/tools/tool-lookup-card.stories.tsxpackages/ui-kit-chat-tools/src/styled/ui-chip-card.stories.tsxpackages/ui-kit-chat-tools/src/styled/ui-chip-card.tsxpackages/ui-kit-chat-tools/test/catalog-cards.browser.test.tsxpackages/ui-kit-chat-tools/test/new-tool-projection.browser.test.tsxpackages/ui-kit-chat-tools/test/page-action-card-title.browser.test.tsxpackages/ui-kit-chat-tools/test/page-tool-cards.browser.test.tsxpackages/ui-kit-chat-tools/test/registry-card-declarations.browser.test.tsxpackages/ui-kit-chat-tools/test/schema-params.test.tspackages/ui-kit-chat/package.jsonpackages/ui-kit-chat/src/index.tsxpackages/ui-kit-chat/src/primitives/message/message.tsxpackages/ui-kit-chat/src/primitives/tools/schema-params.tspackages/ui-kit-chat/src/primitives/tools/tool-presentation.tspackages/ui-kit-chat/src/store/element-capture.fixtures.tspackages/ui-kit-chat/src/store/tool-context.tsxpackages/ui-kit-chat/src/styled/chip.tsxpackages/ui-kit-chat/src/styled/collapsible-card.tsxpackages/ui-kit-chat/src/styled/element-preview.stories.tsxpackages/ui-kit-chat/src/styled/element-preview.tsxpackages/ui-kit-chat/src/styled/json-tree.tsxpackages/ui-kit-chat/src/styled/tool-card.tsxpackages/ui-kit-chat/src/styled/tool-fallback.stories.tsxpackages/ui-kit-chat/src/styled/tool-icon.tsxpackages/ui-kit-chat/src/styled/tools/inline-row.tsxpackages/ui-kit-chat/src/styled/tools/meta-tool-card.stories.tsxpackages/ui-kit-chat/src/styled/tools/meta-tool-card.tsxpackages/ui-kit-chat/src/styled/tools/note-row.tsxpackages/ui-kit-chat/src/styled/tools/permission-card.stories.tsxpackages/ui-kit-chat/src/styled/tools/permission-card.tsxpackages/ui-kit-chat/src/styled/tools/tool-call-card.tsxpackages/ui-kit-chat/test/collapsible-card-shape.browser.test.tsxpackages/ui-kit-chat/test/element-preview.browser.test.tsxpackages/ui-kit-chat/test/meta-tool-card-error.browser.test.tsxpackages/ui-kit-chat/test/schema-params.test.tspackages/ui-kit-chat/test/tool-call-card-dispatch.browser.test.tsxpackages/ui-kit-system/src/index.tsxpackages/ui-kit-system/src/json-tree-view.tsxpackages/ui-kit-system/src/select.tsxpackages/uno-preset/src/index.tspackages/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
| "e2e/vite-solid/**", | ||
| "e2e/vite-vanilla/**", | ||
| "**/vitest.config.ts", | ||
| "**/vite.config.ts", |
There was a problem hiding this comment.
📐 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: | ||
|
|
||
| ``` |
There was a problem hiding this comment.
📐 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
- ```
+ ```textAlso 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
| 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} |
There was a problem hiding this comment.
🔒 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
| 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}, | ||
| }) |
There was a problem hiding this comment.
🗄️ 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/srcRepository: 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' | sortRepository: 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 listRepository: 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))
PYRepository: 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.
| 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) |
There was a problem hiding this comment.
🩺 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 | sortRepository: 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.tsRepository: 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/testRepository: 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 �. 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.
| 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 |
There was a problem hiding this comment.
🩺 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.tsRepository: 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 -240Repository: 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.tsxRepository: 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') }"
doneRepository: 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)}')
PYRepository: 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)}')
PYRepository: 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.
| 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 |
There was a problem hiding this comment.
🗄️ 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.
| fallback={ | ||
| <CardFrame class={local.class}> | ||
| <StaticRow header={local.header} /> | ||
| </CardFrame> |
There was a problem hiding this comment.
🎯 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.
There was a problem hiding this comment.
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/hrefURLs, and arbitrary page CSS can containurl()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, sinceinertonly 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.erroris 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'serrors, 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
buildChatToolscreates theToolRequestatpackages/core/src/chat/runtime.ts:102without atoolCallId. Only code-mode calls currently add one, so directpage.*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
bodyor 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.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
packages/embed/test/element-capture.it.test.ts (1)
19-28: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winCreate a fresh session for each test.
sessionIdis created once inbeforeAll, 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 inbeforeEach/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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (185)
.fallowrc.jsonapps/conciv/package.jsonapps/conciv/src/pane/chat-pane.tsxapps/conciv/src/pane/conciv-ui-card.tsxapps/conciv/src/pane/session-captures.tsapps/conciv/src/pane/tool-view-ctx.tsapps/conciv/test/helpers/fake-core-router.tsapps/conciv/test/helpers/fake-core.tsapps/conciv/test/kit-controls.browser.test.tsxapps/conciv/test/tool-card-dispatch.browser.test.tsxapps/conciv/uno.config.tsapps/storybook/.storybook/main.tsapps/storybook/package.jsonapps/storybook/uno.config.tsdocs/superpowers/specs/2026-08-08-page-tool-cards-design.mddocs/superpowers/specs/2026-08-09-conciv-ui-card-unification-design.mdpackages/cli/src/tool-command.tspackages/contract/src/contract.tspackages/core/package.jsonpackages/core/src/api/execute-schemas.tspackages/core/src/api/mcp.tspackages/core/src/api/rpc/router.tspackages/core/src/api/rpc/sessions.tspackages/core/src/app.tspackages/core/src/cards.tsxpackages/core/src/cards/code-run-card.stories.tsxpackages/core/src/cards/code-run-card.tsxpackages/core/src/chat/capabilities.tspackages/core/src/chat/code-mode-parts.tspackages/core/src/chat/code-mode.tspackages/core/src/page-bus.tspackages/core/src/tool-registry.tspackages/core/test/api/page/tool-capture.it.test.tspackages/core/test/builtin-tool-calls.test.tspackages/core/test/chat/approving-call-post-result-traffic.it.test.tspackages/core/test/chat/code-mode-parts.test.tspackages/core/test/chat/code-mode-reload-fold.it.test.tspackages/core/test/client-tool-gate.test.tspackages/core/tsconfig.cards.build.jsonpackages/core/tsconfig.cards.jsonpackages/core/uno.config.tspackages/core/vite.config.tspackages/db/drizzle/20260808214400_tool_captures/migration.sqlpackages/db/drizzle/20260808214400_tool_captures/snapshot.jsonpackages/db/src/capture-queries.tspackages/db/src/index.tspackages/db/src/run-queries.tspackages/db/src/schema.tspackages/db/test/capture-queries.test.tspackages/embed/test/element-capture.it.test.tspackages/embed/test/embed.it.test.tspackages/embed/test/helpers/page-plane-host.tspackages/embed/test/mount-externals.test.tspackages/embed/uno.config.tspackages/embed/vite.config.tspackages/extension-testkit/src/card-harness.tsxpackages/extension/src/collect-client.tspackages/extension/src/define-tool.tspackages/extension/src/tool-registry.tspackages/extension/src/types.tspackages/extension/test/tool-registry.test.tspackages/extensions/page/.gitignorepackages/extensions/page/package.jsonpackages/extensions/page/src/client.tsxpackages/extensions/page/src/client/bodies.tspackages/extensions/page/src/client/cards/act-card.stories.tsxpackages/extensions/page/src/client/cards/act-card.tsxpackages/extensions/page/src/client/cards/console-card.stories.tsxpackages/extensions/page/src/client/cards/console-card.tsxpackages/extensions/page/src/client/cards/edit-live-card.stories.tsxpackages/extensions/page/src/client/cards/edit-live-card.tsxpackages/extensions/page/src/client/cards/effect-card.stories.tsxpackages/extensions/page/src/client/cards/effect-card.tsxpackages/extensions/page/src/client/cards/react-card.stories.tsxpackages/extensions/page/src/client/cards/react-card.tsxpackages/extensions/page/src/client/cards/read-bulk-card.stories.tsxpackages/extensions/page/src/client/cards/read-bulk-card.tsxpackages/extensions/page/src/client/cards/read-value-card.stories.tsxpackages/extensions/page/src/client/cards/read-value-card.tsxpackages/extensions/page/src/client/cards/shared.tsxpackages/extensions/page/src/client/cards/story.fixtures.tspackages/extensions/page/src/client/js-beautify-html.d.tspackages/extensions/page/src/client/page-format.tspackages/extensions/page/src/client/page-result-views.stories.tsxpackages/extensions/page/src/client/page-result-views.tsxpackages/extensions/page/src/shared/defs.tspackages/extensions/page/test/defs.test.tspackages/extensions/page/test/tsconfig.jsonpackages/extensions/page/tsconfig.build.jsonpackages/extensions/page/tsconfig.jsonpackages/extensions/page/tsconfig.refs.jsonpackages/extensions/page/tsdown.config.tspackages/extensions/page/uno.config.tspackages/extensions/page/vite.config.tspackages/extensions/test-runner/test/test-card.browser.test.tsxpackages/extensions/whiteboard/src/client/model/comments.tsxpackages/harness-testkit/src/call-tool.tspackages/page/package.jsonpackages/page/src/css-bundle.tspackages/page/src/element-capture.tspackages/page/src/element-descriptor.tspackages/page/src/page-driver.tspackages/page/src/page-snapshot.tspackages/page/src/page-tool-dispatcher.tspackages/page/test/css-bundle.test.tspackages/page/test/element-capture.browser.test.tspackages/page/test/page-dispatcher.browser.test.tspackages/protocol/package.jsonpackages/protocol/src/chat-types.tspackages/protocol/src/done-types.tspackages/protocol/src/element-capture-types.tspackages/protocol/src/page-types.tspackages/protocol/src/tool-view-types.tspackages/protocol/tsdown.config.tspackages/tools/package.jsonpackages/tools/src/cards.tsxpackages/tools/src/cards/extensions-card.tsxpackages/tools/src/cards/inline-cards.stories.tsxpackages/tools/src/cards/open-card.tsxpackages/tools/src/cards/ui-card.stories.tsxpackages/tools/src/cards/ui-card.tsxpackages/tools/tsconfig.cards.build.jsonpackages/tools/tsconfig.cards.jsonpackages/tools/uno.config.tspackages/tools/vite.config.tspackages/ui-kit-chat-tools/package.jsonpackages/ui-kit-chat-tools/src/index.tsxpackages/ui-kit-chat-tools/src/primitives/tools/inline-tool.tsxpackages/ui-kit-chat-tools/src/primitives/tools/now-title.tspackages/ui-kit-chat-tools/src/styled/done-card.stories.tsxpackages/ui-kit-chat-tools/src/styled/done-card.tsxpackages/ui-kit-chat-tools/src/styled/page-action-card.stories.tsxpackages/ui-kit-chat-tools/src/styled/page-action-card.tsxpackages/ui-kit-chat-tools/src/styled/tools/apply-patch-diff.stories.tsxpackages/ui-kit-chat-tools/src/styled/tools/bash-card.stories.tsxpackages/ui-kit-chat-tools/src/styled/tools/builtin-tool-cards.tspackages/ui-kit-chat-tools/src/styled/tools/discovered-apis-card.stories.tsxpackages/ui-kit-chat-tools/src/styled/tools/file-read-card.stories.tsxpackages/ui-kit-chat-tools/src/styled/tools/inline-tool.stories.tsxpackages/ui-kit-chat-tools/src/styled/tools/inline-tool.tsxpackages/ui-kit-chat-tools/src/styled/tools/loaded-tools-card.stories.tsxpackages/ui-kit-chat-tools/src/styled/tools/loaded-tools-card.tsxpackages/ui-kit-chat-tools/src/styled/tools/todo-card.stories.tsxpackages/ui-kit-chat-tools/src/styled/tools/tool-chip.stories.tsxpackages/ui-kit-chat-tools/src/styled/tools/tool-lookup-card.stories.tsxpackages/ui-kit-chat-tools/src/styled/ui-chip-card.stories.tsxpackages/ui-kit-chat-tools/src/styled/ui-chip-card.tsxpackages/ui-kit-chat-tools/test/catalog-cards.browser.test.tsxpackages/ui-kit-chat-tools/test/new-tool-projection.browser.test.tsxpackages/ui-kit-chat-tools/test/page-action-card-title.browser.test.tsxpackages/ui-kit-chat-tools/test/page-tool-cards.browser.test.tsxpackages/ui-kit-chat-tools/test/registry-card-declarations.browser.test.tsxpackages/ui-kit-chat-tools/test/schema-params.test.tspackages/ui-kit-chat/package.jsonpackages/ui-kit-chat/src/index.tsxpackages/ui-kit-chat/src/primitives/message/message.tsxpackages/ui-kit-chat/src/primitives/tools/schema-params.tspackages/ui-kit-chat/src/primitives/tools/tool-presentation.tspackages/ui-kit-chat/src/store/element-capture.fixtures.tspackages/ui-kit-chat/src/store/tool-context.tsxpackages/ui-kit-chat/src/styled/chip.tsxpackages/ui-kit-chat/src/styled/collapsible-card.tsxpackages/ui-kit-chat/src/styled/element-preview.stories.tsxpackages/ui-kit-chat/src/styled/element-preview.tsxpackages/ui-kit-chat/src/styled/json-tree.tsxpackages/ui-kit-chat/src/styled/tool-card.tsxpackages/ui-kit-chat/src/styled/tool-fallback.stories.tsxpackages/ui-kit-chat/src/styled/tool-icon.tsxpackages/ui-kit-chat/src/styled/tools/inline-row.tsxpackages/ui-kit-chat/src/styled/tools/meta-tool-card.stories.tsxpackages/ui-kit-chat/src/styled/tools/meta-tool-card.tsxpackages/ui-kit-chat/src/styled/tools/note-row.tsxpackages/ui-kit-chat/src/styled/tools/permission-card.stories.tsxpackages/ui-kit-chat/src/styled/tools/permission-card.tsxpackages/ui-kit-chat/src/styled/tools/tool-call-card.tsxpackages/ui-kit-chat/test/collapsible-card-shape.browser.test.tsxpackages/ui-kit-chat/test/element-preview.browser.test.tsxpackages/ui-kit-chat/test/meta-tool-card-error.browser.test.tsxpackages/ui-kit-chat/test/schema-params.test.tspackages/ui-kit-chat/test/tool-call-card-dispatch.browser.test.tsxpackages/ui-kit-system/src/index.tsxpackages/ui-kit-system/src/json-tree-view.tsxpackages/ui-kit-system/src/select.tsxpackages/uno-preset/src/index.tspackages/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
| 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') |
There was a problem hiding this comment.
🗄️ 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.
| 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.
| 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) |
There was a problem hiding this comment.
🔒 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.
| 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.
| 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([]) |
There was a problem hiding this comment.
🎯 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.
| tagName: 'a', | ||
| attributes: {href: `javascript:window.${XSS_FLAG} = true`, onclick: `window.${XSS_FLAG} = true`}, | ||
| childNodes: [{type: 3, textContent: 'click me', id: 3}], |
There was a problem hiding this comment.
🔒 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.
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, becausepageActionToolmatchedconciv_page— a tool name #284 replaced with 37page.<verb>tools.ToolCallCardmatches onpart.name, nothing matched, so everything fell through toToolFallback.Landed so far
Dispatch.
ToolCallCardresolves extension card → builtin card →MetaToolCard→ToolFallback. The extension layer already existed end to end (ToolBuilder.render→collectToolRenderers→ chat pane) and had no callers; layer 3 is new.ToolFallbacknow only serves tools that declare no meta at all — i.e. foreign harness tools.The wire.
ToolViewMetagainedcategory,hint,positional,approval,inputSchema,outputSchema,errors, so a card can be built from a declaration alone.input/outputwere renamed toinputSchema/outputSchemaso the widget's structural assignment needs no adapter.MetaToolCardin@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.schemaParamsandtool-iconmoved 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 readserror.codeand 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/pageActionTooldeleted. Result views survive aspage-result-views.tsxprimitives (A11yNodeList,PageHtmlBlock,PageValueChip) for the per-verb cards.PermissionCardrenders again for tools reaching the first three layers. TheShowstays —ToolFallbackdraws its own approval buttons and would otherwise double the prompt.nowTitlereadslabel.runningfrom 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.ToolAccentdeleted — dead, and its members never matched thecategoryvalues 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:
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.Treerenders 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 inpresetConcivemitting plain nested CSS.transformerVariantGroupwas 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
@conciv/page(rrweb node + ancestor skeleton + shared CSS bundle, password masking at serialize time)toolCapturesstorage, so captures reach the transcript UI and never the modelElementCapturein protocol and<ElementPreview>in ui-kit-chat.render()conciv_ui/conciv_open/conciv_extensionscards topackages/tools,execute_typescriptto coreGates
19/19 test tasks (99 ui-kit-chat, 199 storybook), typecheck 92/92, lint 0 errors, format clean,
fallow auditverdict pass.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes