Skip to content

feat(documents): add interactive widgets to documents - #725

Merged
urjitc merged 32 commits into
mainfrom
codex/widgets-in-documents
Aug 4, 2026
Merged

feat(documents): add interactive widgets to documents#725
urjitc merged 32 commits into
mainfrom
codex/widgets-in-documents

Conversation

@urjitc

@urjitc urjitc commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

  • Add interactive widget blocks directly inside ThinkEx documents.
  • Render AI-authored widget HTML in a sandboxed, theme-aware, auto-sizing frame.
  • Add a document toolbar flow that stages a widget request in the AI composer.
  • Give document block reads and edits one consistent editRef contract.

Why

Documents could explain an idea but could not let the reader interact with it. This adds small simulations, calculators, adjustable visuals, quizzes, and similar tools without introducing a separate workspace item type.

The AI guidance stays conservative: ordinary prose and tables remain the default, Mermaid remains available for requested static diagrams in chat, and widgets are reserved for explicit, interactive, or document-specific visual requests.

Changes

  • Add a Tiptap widget node whose source is stored as document text content.
  • Run widget source in an opaque-origin iframe with allow-scripts only, a restrictive CSP, no network access, and no parent storage or DOM access.
  • Sync app design tokens across light and dark themes without resetting live widget state.
  • Measure widget content and bound its inline height, with internal scrolling for tall widgets.
  • Show a centered error state and let users stage the runtime error in the AI composer for repair.
  • Add the document toolbar dialog and focus the composer after its prompt is filled.
  • Keep widgets inside document reads as compact placeholders, with full block reads available on demand.
  • Rename model-facing targets to editRef, return canonical block content and its current target together, and use that same content for replace_text.
  • Remove widget-only replacement and overwrite-restoration paths.
  • Add widget authoring guidance, tool examples, eval coverage, preview/search handling, and bundled KaTeX browser assets.
  • Keep Mermaid in chat, but only prompt for it when the user asks for a diagram or visual explanation.
  • Remove widget fullscreen and keep the document surface compact.

Testing

  • pnpm check
  • NODE_OPTIONS="--no-experimental-webstorage" pnpm test — 66 files and 293 tests passed
  • Focused widget, document edit, content read, and tool-surface tests — 64 tests passed
  • React Doctor changed-scope review — 100/100

The local shell is on unsupported Node 25, while the repository requires Node 24. Node 25 exposes an experimental broken global localStorage unless web storage is disabled; the full suite passes with that Node 25-only workaround.

Review Notes

Start with:

  1. src/features/workspaces/documents/tiptap-schema.ts
  2. src/features/workspaces/documents/document-widget-extension.ts
  3. src/features/workspaces/components/widget/WorkspaceWidgetSandbox.tsx
  4. src/features/workspaces/components/widget/workspace-widget-sandbox-document.ts
  5. src/features/workspaces/documents/document-ai-edits.ts

The main security-sensitive area is the iframe sandbox/CSP and its small theme, height, and error message channel. Generated KaTeX browser files are copied during prepare and intentionally ignored by Git.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added AI-assisted widget creation and editing for workspace documents.
    • Added sandboxed interactive widgets with theme support, responsive sizing, runtime error handling, and optional AI fixes.
    • Added block-level document reading and precise edit references for safer targeted updates.
    • Added collapsible document sections and improved widget previews.
    • Improved AI composer prompt staging, focus handling, and responsive chat behavior.
  • Bug Fixes
    • Improved math, chemistry, currency, Mermaid, and HTML formatting guidance.
    • Improved workspace citations, editing validation, and widget error recovery.
  • Tests
    • Expanded coverage for widgets, document editing, formatting, references, and composer behavior.

urjitc added 17 commits August 3, 2026 23:36
A widget is an AI-authored, self-contained HTML fragment that runs in an
opaque-origin iframe (allow-scripts, no allow-same-origin) and lives as a block
inside a document. A document holding nothing but a widget is the standalone
case, so there is no separate item type to read, edit, create and render.

Reads elide a widget's source to an empty placeholder carrying its ref and
title, because one widget runs to kilobytes and would crowd the prose out of a
chunk. workspace_read_items gains mode "block" to fetch that source back in
full. Elision alone would be data loss, since overwrite echoes a read back by
design, so the parser takes a resolveWidgetSource hook and an empty widget with
a known ref keeps what is already there.

replace_text joins the document edit vocabulary with a ref, so it targets text
inside any block: a widget's source or a paragraph's markup. It fails rather
than replacing every occurrence when a find matches more than once. replace_all
is renamed overwrite so it cannot be read as "replace all instances of a find".

Also here, because the surfaces had drifted apart:

- Documents degrade unsupported markup instead of rejecting the write. A probe
  of realistic model output found 12 of 20 snippets refused outright, each
  costing a whole document to save formatting we can flatten. ProseMirror's
  DOMParser already normalises HTML, which removed the hand-rolled allowlist.
- Math, chemistry and units are one contract across documents and widgets, both
  being HTML. Chat keeps the $-delimited Markdown form. sub/sup become math
  rather than failing, and money is written plainly.
- KaTeX is served from the app's own origin and injected only when a widget
  contains math.

Widget source stays out of search and previews: the markdown projection drops
it, and a preview shows the widget's title instead.
Return canonical block content with a fresh editRef and use the same representation for
replace_text across prose and widgets. Remove widget-specific overwrite restoration and align
tool schemas, prompts, evals, and focused coverage with the contract.
Keep the Tiptap extension in a non-component module so the React node view remains compatible
with state-preserving Fast Refresh.
Make the DOM environment used by widget and existing component tests an explicit dependency.
Default to prose and tables, reserve Mermaid for requested visual explanations, and create
widgets only for explicit, interactive, or document-specific visual requests.
@cursor

cursor Bot commented Aug 4, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@capy-ai

capy-ai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Capy auto-review is paused for this organization because the usage-cycle auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

React Doctor found 2 new issues in 1 file · 2 warnings · score 87 / 100 (Great) · 1 fixed · vs main

2 warnings

eval/support/harness.ts

  • ⚠️ L80 await inside a loop async-await-in-loop
  • ⚠️ L147 Chained array iterations js-combine-iterations

Reviewed by React Doctor for commit 6292f7f. See inline comments for fixes.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@urjitc, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 15 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 269b78e8-b1aa-4fdf-a302-94512af5208c

📥 Commits

Reviewing files that changed from the base of the PR and between 7087704 and 6292f7f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • .gitignore
  • eval/datasets/workspace-tools.cases.ts
  • eval/support/harness.ts
  • eval/support/scorers.ts
  • eval/workspace-tools.eval.ts
  • package.json
  • scripts/copy-widget-libs.mjs
  • src/features/workspaces/ai/skills/widget-authoring/SKILL.md
  • src/features/workspaces/ai/skills/widget-authoring/references/canvas.md
  • src/features/workspaces/ai/skills/widget-authoring/references/starter.md
  • src/features/workspaces/components/widget/WorkspaceAddWidgetDialog.tsx
  • src/features/workspaces/components/widget/WorkspaceWidgetSandbox.test.tsx
  • src/features/workspaces/components/widget/WorkspaceWidgetSandbox.tsx
  • src/features/workspaces/components/widget/workspace-widget-sandbox-document.test.ts
  • src/features/workspaces/components/widget/workspace-widget-sandbox-document.ts
  • src/features/workspaces/composer/workspace-composer-actions.ts
  • src/features/workspaces/content/workspace-content-contract.ts
  • src/features/workspaces/documents/document-ai-edits.test.ts
  • src/features/workspaces/documents/document-ai-edits.ts
  • src/features/workspaces/documents/document-ai-html.test.ts
  • src/features/workspaces/operations/workspace-tool-definitions.ts
  • src/features/workspaces/operations/workspace-tool-schemas.ts
📝 Walkthrough

Walkthrough

The PR adds sandboxed HTML widgets to workspace documents, block-level reads and edits using editRef, staged AI prompts, responsive composer focus handling, shared workspace paths, updated AI instructions, and evaluation checks for content and edit provenance.

Changes

Workspace document blocks and edits

Layer / File(s) Summary
Block references, reads, and edits
src/features/workspaces/content/*, src/features/workspaces/documents/*
Documents now use persistent block IDs and data-edit-ref values. Workspace reads can return block snapshots. Targeted edits validate fresh references, support replace_text, and retain overwrite for whole-document edits.
Widget document model and previews
src/features/workspaces/documents/tiptap-schema.ts, src/features/workspaces/documents/document-preview-text.ts
Documents support widget blocks and collapsible details sections. Widget source is elided from AI HTML and search projections, while widget titles can appear in previews.

Widget rendering and authoring

Layer / File(s) Summary
Sandboxed widget runtime
src/features/workspaces/components/widget/*, src/features/workspaces/documents/document-widget-*
Widgets render in sandboxed iframes with restrictive CSP, theme synchronization, conditional KaTeX loading, bounded height reporting, session validation, and runtime error handling.
Widget controls and repair prompts
src/features/workspaces/components/document-editor/*, src/features/workspaces/components/widget/WorkspaceAddWidgetDialog.tsx, src/features/workspaces/composer/*, src/features/workspaces/components/ai-chat/*
Toolbars can open an Add widget dialog. Widget errors can stage AI-fix prompts. Staged prompts reveal the responsive composer and request textarea focus.

AI instructions and evaluations

Layer / File(s) Summary
Workspace AI contracts
src/features/workspaces/ai/*, src/features/workspaces/operations/*
AI guidance now covers widget HTML, math and chemistry formatting, block reads, exact edit references, replace_text, and overwrite. Bundled skills are exposed through AIThread.getSkills().
Evaluation harness and cases
eval/datasets/workspace-tools.cases.ts, eval/support/*, eval/workspace-tools.eval.ts
Evaluations use the production workspace prompt, mutation-aware tools, path- and block-aware fixtures, prior-read edit references, and regex-based content checks.

Workspace wiring and generated assets

Layer / File(s) Summary
Workspace paths, toolbar context, and assets
src/features/workspaces/model/*, src/features/workspaces/components/WorkspaceContent.tsx, src/features/workspaces/components/WorkspaceItemToolbarSlot.tsx, scripts/copy-widget-libs.mjs, package.json, .gitignore, src/styles.css
Document paths are resolved from workspace trees and passed through editor registrations. The prepare script copies KaTeX browser assets. Widget and details-section styles are added, and generated assets are ignored.

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

Sequence Diagram(s)

sequenceDiagram
  participant AIThread
  participant WorkspaceContentReader
  participant DocumentSession
  participant DocumentAiEdits
  AIThread->>WorkspaceContentReader: request block read with editRef
  WorkspaceContentReader->>DocumentSession: readBlock(editRef)
  DocumentSession-->>WorkspaceContentReader: block HTML and current editRef
  WorkspaceContentReader-->>AIThread: block content
  AIThread->>DocumentAiEdits: submit targeted edit
  DocumentAiEdits->>DocumentAiEdits: validate prior-read editRef
  DocumentAiEdits-->>AIThread: edit result
Loading

Possibly related PRs

Suggested labels: capy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding interactive widgets to documents.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/widgets-in-documents

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bc7136b6a2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +109 to +112
"media-src data: blob:",
"connect-src 'none'",
"object-src 'none'",
"base-uri 'none'",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict sandbox resources to the KaTeX asset path

When widget HTML contains a same-origin <script src>, stylesheet, @import, or font URL, these source expressions permit it because ${origin} authorizes the entire application origin; sandbox="allow-scripts" isolates the frame's origin but does not disable its subresource requests. This breaks the stated no-network contract for untrusted widgets and lets them request or execute arbitrary same-origin resources rather than only the vendored KaTeX files. Scope each directive to ${origin}/widget-libs/katex/ (or serve the required assets through another narrowly allowlisted origin/path).

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This change adds AI-authored interactive widgets to collaborative documents and updates document reads and edits around editRef.

Browser execution confirmed that widget code can navigate its sandbox to an external URL despite the generated connection policy. The sandbox must reject untrusted document navigation before this change is merged.

Confidence Score: 4/5

Not safe to merge until the widget iframe prevents untrusted external navigation.

The reproduced external navigation path in the widget sandbox remains an open security issue.

Files Needing Attention: src/features/workspaces/components/widget/WorkspaceWidgetSandbox.tsx and the generated sandbox document CSP.

Security Review

The widget sandbox isolates scripts from the parent document and storage, but it does not fully enforce its no-network intent. An authored widget can call window.location.assign() with an arbitrary external URL, which causes a document request even when connect-src 'none' is active. That permits URL-based data disclosure and replaces the widget interface with external content.

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex produced a finding-comment-proof for a posted P1 finding, accompanied by videos and images that illustrate the sandboxed widget navigating toward an authored external destination.
  • T-Rex produced a second finding-comment-proof for another P1 finding.
  • T-Rex produced a general-contract-validation-proof that shows the baseline widget remains in its initial sandbox and that a changed recording captured an external navigation to a redacted URL, with observedRequestToExternalDestination: true and a GET to /external-destination.
  • Artifacts were created to support the proofs, including video captures, baseline images, a navigation log, and the Playwright proof source for widget navigation.
  • The artifacts include a baseline image of the sandbox and a frame captured immediately before the widget's external navigation to help review the embedding boundary.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (1)

  1. General comment

    P1 Authored widget code can navigate its sandbox to an external URL

    • Bug
      • At src/features/workspaces/components/widget/WorkspaceWidgetSandbox.tsx:142, an authored widget runs with sandbox="allow-scripts". Its JavaScript can assign its own window.location to an external URL, causing a real document request despite the generated CSP's connect-src 'none'.
    • Cause
      • The iframe sandbox permits scripts and does not prevent the frame's own navigation. connect-src restricts fetch/XHR/WebSocket-style connections, not document navigation; the CSP has no effective navigation restriction.
    • Fix
      • Preserve the no-network contract by using an isolation/embedding design that prevents or rejects untrusted iframe navigations, rather than relying on connect-src. Add a Chromium regression test that asserts an authored window.location assignment cannot issue an external document request.

    T-Rex Ran code and verified through T-Rex

Fix All in Cursor

Reviews (1): Last reviewed commit: "fix(ai): avoid unnecessary widget genera..." | Re-trigger Greptile

<iframe
ref={iframeRef}
title="Widget preview"
sandbox="allow-scripts"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security Widget navigation bypasses network isolation

An authored widget can assign window.location to an external URL. With sandbox="allow-scripts", the iframe can navigate itself; the generated connect-src 'none' policy restricts connection APIs but not document navigation. This makes a real external request and replaces the widget document, allowing widget-controlled data to be included in a navigation URL despite the intended no-network isolation. Prevent or reject untrusted frame navigation at the embedding boundary and add a browser regression test for window.location assignment.

Artifacts

▶ Recording of the check

  • Chromium rendered the generated widget iframe without a navigation payload; the widget remains in its initial document, establishing the comparison baseline.

Baseline sandboxed widget before location assignment

  • Poster frame from the baseline recording shows the authored widget before any location assignment, establishing the unchanged state.

▶ Recording of the check

  • Chromium rendered the same generated sandbox with an authored window.location assignment and then requested the external destination, proving navigation bypasses the no-network intent.

Widget immediately before its authored external navigation

  • Poster frame from the navigation run shows the exact authored widget loaded with the delayed location payload, documenting the changed execution condition.

Successful Chromium navigation proof output

  • Output from the executed Playwright proof records exit code 0, sandbox allow-scripts, connect-src none, and the external GET request; the no-network claim is violated.

Playwright proof source for widget navigation

  • The executed source builds the repository's actual widget sandbox document, renders it in Chromium, and records baseline and location-assignment behavior; it reproduces the finding.

View artifacts

T-Rex Ran code and verified through T-Rex

Fix in Cursor

sessionId,
...theme,
};
iframe?.contentWindow?.postMessage(message, "*");

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (8)
src/features/workspaces/documents/document-ai-edits.ts (1)

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

Block-ID inheritance is duplicated across the replace_text and replace branches.

Lines 256-264 and lines 270-277 build the same "first node inherits the target block ID" list. Extract one helper to keep both paths identical if the rule changes.

♻️ Proposed refactor
+function withInheritedBlockId(target: ProseMirrorNode, children: ProseMirrorNode[]) {
+	const targetBlockId = readTiptapNodeBlockId(target);
+	const firstNode = children[0];
+	return targetBlockId && firstNode
+		? [withTiptapNodeAiRef(firstNode, targetBlockId), ...children.slice(1)]
+		: children;
+}

Then call it from both branches:

-		const targetBlockId = readTiptapNodeBlockId(target);
-		const firstNode = parsed.children[0];
-		children.splice(
-			targetIndex,
-			1,
-			...(targetBlockId && firstNode
-				? [withTiptapNodeAiRef(firstNode, targetBlockId), ...parsed.children.slice(1)]
-				: parsed.children),
-		);
+		children.splice(targetIndex, 1, ...withInheritedBlockId(target, parsed.children));
🤖 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 `@src/features/workspaces/documents/document-ai-edits.ts` around lines 241 -
277, Extract the duplicated block-ID inheritance logic into a shared helper near
the edit-processing code, accepting the parsed children and target block ID and
returning the adjusted node list. Use this helper in both the replace_text
branch and the replace branch, preserving inheritance only when a target block
ID and first node exist.
src/features/workspaces/documents/document-ai-html.test.ts (1)

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

Use a pattern-valid editRef in this fixture.

b_modelchosen1 does not match documentBlockIdPattern, so the test passes even if the strip loop and parseHTML: () => null are both removed; readTiptapNodeBlockId would reject the value anyway. Use a well-formed value so the test proves that a model cannot choose a usable ref.

💚 Proposed fix
-				parseDocumentAiHtml('<p data-edit-ref="b_modelchosen1">Hello</p>'),
+				parseDocumentAiHtml(
+					'<p data-edit-ref="b_modelchosen1x.r_0123456789">Hello</p>',
+				),
 			).document,
 		);
 
-		expect(html).not.toContain("b_modelchosen1");
+		expect(html).not.toContain("b_modelchosen1x");
🤖 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 `@src/features/workspaces/documents/document-ai-html.test.ts` around lines 42 -
51, Update the fixture in “ignores editRefs supplied in model-authored HTML” to
use an editRef matching documentBlockIdPattern, while preserving the assertion
that the original value is absent and a generated valid ref is present. Ensure
the test specifically exercises rejection of a structurally valid model-supplied
ref.
src/features/workspaces/documents/document-ai-html.ts (1)

225-239: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Nested inline markup inside <sub>/<sup> becomes literal LaTeX text.

element.textContent flattens child elements. For x<sup><em>n</em></sup> the value stays n, which is correct, but for content that includes LaTeX-significant characters, such as <sub>a_b</sub> or <sub>{x}</sub>, the generated data-latex becomes {}_{a_b} or {}_{{x}}. KaTeX renders those differently from the intended text. throwOnError: false prevents a hard failure, so the impact is limited to a wrong render.

Consider escaping LaTeX control characters (\, {, }, _, ^, $, %, &, #, ~) before building the attribute.

🤖 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 `@src/features/workspaces/documents/document-ai-html.ts` around lines 225 -
239, Update the sub/sup conversion loop around element.textContent and
data-latex so latex text is escaped before interpolation. Escape backslashes and
LaTeX-significant characters (\ , {, }, _, ^, $, %, &, #, ~) while preserving
the existing subscript/superscript wrapper and removal behavior for empty
content.
src/features/workspaces/components/widget/workspace-widget-sandbox-document.test.ts (2)

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

Assert the CSP directives that carry the security guarantee.

The test name says the document blocks remote assets, but the assertions cover only img-src and media-src. The directives that actually contain an untrusted widget are untested: default-src 'none', connect-src 'none', object-src 'none', base-uri 'none', and form-action 'none'.

connect-src 'none' is the control that stops a malicious widget from exfiltrating anything it can read. Pin it, so a later edit to getWidgetSandboxCsp cannot quietly relax it.

The assertion on line 47 checks for a string the builder never emits in any branch, so it cannot fail. Replace it with the positive directive checks.

♻️ Proposed assertions
 		expect(document).toContain("img-src data: blob:");
 		expect(document).toContain("media-src data: blob:");
-		expect(document).not.toContain("img-src data: https:");
+		expect(document).toContain("default-src 'none'");
+		expect(document).toContain("connect-src 'none'");
+		expect(document).toContain("object-src 'none'");
+		expect(document).toContain("base-uri 'none'");
+		expect(document).toContain("form-action 'none'");
🤖 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
`@src/features/workspaces/components/widget/workspace-widget-sandbox-document.test.ts`
around lines 45 - 47, Update the CSP assertions in the workspace sandbox
document test to positively verify default-src 'none', connect-src 'none',
object-src 'none', base-uri 'none', and form-action 'none', alongside the
existing relevant directives. Remove the ineffective negative img-src https
assertion and ensure the test pins the directives emitted by
getWidgetSandboxCsp.

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

Add a case for a mismatched source.

The source check is the first guard in isWidgetSandboxFrameMessage, and every existing case passes WIDGET_SANDBOX_FRAME_SOURCE. A message from an unrelated frame or from the host channel is the realistic rejection path, and it has no test.

♻️ Proposed case
 		expect(
 			isWidgetSandboxFrameMessage({
 				source: WIDGET_SANDBOX_FRAME_SOURCE,
 				kind: "ready",
 				sessionId: 0,
 			}),
 		).toBe(false);
+		expect(isWidgetSandboxFrameMessage({ kind: "ready", sessionId: 1, source: "other" })).toBe(
+			false,
+		);
🤖 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
`@src/features/workspaces/components/widget/workspace-widget-sandbox-document.test.ts`
around lines 99 - 105, Add a test case for isWidgetSandboxFrameMessage where
source is a different, unrelated value while the other message fields remain
valid, and assert that it returns false. Keep the existing valid-source
rejection cases unchanged.
src/features/workspaces/components/widget/workspace-widget-sandbox-document.ts (1)

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

Restrict emitted token names to WIDGET_SANDBOX_TOKENS.

tokens is typed Record<string, string>, so any key and value reach the frame <style> block verbatim through ${name}: ${value};. The only current caller, readWidgetSandboxTheme, iterates the WIDGET_SANDBOX_TOKENS constant and reads first-party computed styles, so nothing attacker-influenced reaches this concatenation today. The exported signature does not enforce that, and the test on line 13 already passes an ad-hoc object.

Filter against the allowlist here so the guarantee lives with the code that builds the document.

♻️ Proposed allowlist filter
-	const tokenDeclarations = Object.entries(tokens)
-		.filter(([, value]) => value.trim().length > 0)
-		.map(([name, value]) => `${name}: ${value};`)
-		.join("");
+	let tokenDeclarations = "";
+	for (const name of WIDGET_SANDBOX_TOKENS) {
+		const value = tokens[name]?.trim();
+		if (value) {
+			tokenDeclarations += `${name}: ${value};`;
+		}
+	}
🤖 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
`@src/features/workspaces/components/widget/workspace-widget-sandbox-document.ts`
around lines 132 - 135, Update the tokenDeclarations construction to emit only
entries whose names are included in the WIDGET_SANDBOX_TOKENS allowlist, while
preserving the existing non-empty value filtering and declaration formatting.
Apply this restriction in the document-building code rather than relying on
readWidgetSandboxTheme callers.
src/features/workspaces/content/workspace-read-references.ts (1)

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

Add coverage for the new type: "block" result.

Both conditions now key off result.type !== "file". This is the correct narrowing: a type: "block" ready result has no location field, so the previous type !== "document" check would have reached result.location.returned on line 41 and thrown. The guard is also correctly ordered, because assetKind exists only on the file variant.

The test file has no case that feeds a type: "block" result into createWorkspaceReadReferences or createWorkspaceReadItemsModelOutput. Add one so a future reordering of these conditions fails loudly instead of dereferencing a missing location.

Also applies to: 135-135

🤖 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 `@src/features/workspaces/content/workspace-read-references.ts` at line 32, Add
a test covering a ready result with type "block" through
createWorkspaceReadReferences or createWorkspaceReadItemsModelOutput, asserting
it completes without dereferencing location and produces the expected output.
Keep the existing file/PDF behavior unchanged, and ensure the test would fail if
the type guard were reordered or reverted.
src/features/workspaces/content/workspace-content-contract.ts (1)

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

Add an upper length bound to editRef.

cursor on line 51 bounds model-supplied strings with .max(4_096). editRef has only .min(1). An editRef is a short block reference, so a small maximum keeps the validation surface consistent and rejects oversized payloads at the schema boundary.

♻️ Proposed bound
 		editRef: z
 			.string()
 			.min(1)
+			.max(256)
 			.describe(
 				"editRef of one block from an earlier document read. The result returns the block in full with its current editRef.",
 			),
🤖 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 `@src/features/workspaces/content/workspace-content-contract.ts` around lines
56 - 61, Update the editRef schema in the workspace content contract to add a
small maximum length consistent with its short block-reference purpose and the
existing cursor bound. Preserve the current non-empty validation and
description.
🤖 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 `@eval/datasets/workspace-tools.cases.ts`:
- Around line 22-25: Strengthen the validation patterns in
eval/datasets/workspace-tools.cases.ts:22-25, 59-64, and 161-161. Update
DOCUMENT_MATH_MARKUP to require data-type and data-latex on the same math
element; update the widget-block check to require an encoded HTML tag such as
&lt;style&gt; or &lt;script&gt;; and update the currency check to require plain
$30, $60, and $90 while still rejecting escaped dollar signs.

In `@eval/support/scorers.ts`:
- Around line 73-89: Update eval/support/harness.ts lines 55-85 to record each
workspace_read_items fixture result and expose the editRefs it delivered to
scoring; update scoreTargetedEditProvenance in eval/support/scorers.ts lines
73-89 to accept an editRef only when it is both valid and present in the
recorded prior-read results, preserving the existing targeted, fabricated, and
overwrite checks.

In `@src/features/workspaces/components/ai-chat/useConsumeComposerPrompt.ts`:
- Around line 29-49: The useEffect hook can run twice in React Strict Mode with
the same pendingPrompt value, causing the prompt to be appended twice before
being cleared. Move the clearPrompt(workspaceId) call inside the
requestAnimationFrame callback (after the textarea focus and selection logic) to
ensure the prompt is only cleared after the append to setInput is scheduled and
guaranteed to execute, preventing duplicate appends when the effect re-runs with
the same dependencies.

In `@src/features/workspaces/components/widget/WorkspaceWidgetSandbox.tsx`:
- Around line 96-99: Update the error handling in WorkspaceWidgetSandbox so
errors reported after the iframe has completed its ready handshake are displayed
as a dismissible overlay or banner while keeping the live iframe mounted; only
replace the iframe with the full error panel when the frame never becomes ready.
Use the existing ready state or readySessionIdRef to distinguish these cases,
and clear the displayed runtime error when dismissed.

In `@src/features/workspaces/content/workspace-content-reader.ts`:
- Around line 137-177: In the readWorkspaceItem function, add a validation check
for file items before calling readFile to reject requests with mode: "block".
When input.item.type is "file" and input.request.mode is "block", return a
failure result with code "invalid_selection" (matching the pattern in
readDocumentBlock) instead of proceeding to readFile, which does not support
block mode.

In `@src/features/workspaces/model/use-workspace-item-path.ts`:
- Around line 35-45: Update readWorkspaceItemPath to resolve the requested item
from the page.items map before calling getWorkspaceAiContextItemPath, using the
cached object as the path’s base item and ensuring ancestor traversal resolves
cached entries by parentId. Preserve the null return when no cached page exists,
and handle a missing cached item without using the stale captured item.

In `@src/styles.css`:
- Line 509: Add an empty line immediately after the explanatory comment and
before the padding-inline declaration in the relevant CSS rule, leaving the
declaration unchanged.

---

Nitpick comments:
In
`@src/features/workspaces/components/widget/workspace-widget-sandbox-document.test.ts`:
- Around line 45-47: Update the CSP assertions in the workspace sandbox document
test to positively verify default-src 'none', connect-src 'none', object-src
'none', base-uri 'none', and form-action 'none', alongside the existing relevant
directives. Remove the ineffective negative img-src https assertion and ensure
the test pins the directives emitted by getWidgetSandboxCsp.
- Around line 99-105: Add a test case for isWidgetSandboxFrameMessage where
source is a different, unrelated value while the other message fields remain
valid, and assert that it returns false. Keep the existing valid-source
rejection cases unchanged.

In
`@src/features/workspaces/components/widget/workspace-widget-sandbox-document.ts`:
- Around line 132-135: Update the tokenDeclarations construction to emit only
entries whose names are included in the WIDGET_SANDBOX_TOKENS allowlist, while
preserving the existing non-empty value filtering and declaration formatting.
Apply this restriction in the document-building code rather than relying on
readWidgetSandboxTheme callers.

In `@src/features/workspaces/content/workspace-content-contract.ts`:
- Around line 56-61: Update the editRef schema in the workspace content contract
to add a small maximum length consistent with its short block-reference purpose
and the existing cursor bound. Preserve the current non-empty validation and
description.

In `@src/features/workspaces/content/workspace-read-references.ts`:
- Line 32: Add a test covering a ready result with type "block" through
createWorkspaceReadReferences or createWorkspaceReadItemsModelOutput, asserting
it completes without dereferencing location and produces the expected output.
Keep the existing file/PDF behavior unchanged, and ensure the test would fail if
the type guard were reordered or reverted.

In `@src/features/workspaces/documents/document-ai-edits.ts`:
- Around line 241-277: Extract the duplicated block-ID inheritance logic into a
shared helper near the edit-processing code, accepting the parsed children and
target block ID and returning the adjusted node list. Use this helper in both
the replace_text branch and the replace branch, preserving inheritance only when
a target block ID and first node exist.

In `@src/features/workspaces/documents/document-ai-html.test.ts`:
- Around line 42-51: Update the fixture in “ignores editRefs supplied in
model-authored HTML” to use an editRef matching documentBlockIdPattern, while
preserving the assertion that the original value is absent and a generated valid
ref is present. Ensure the test specifically exercises rejection of a
structurally valid model-supplied ref.

In `@src/features/workspaces/documents/document-ai-html.ts`:
- Around line 225-239: Update the sub/sup conversion loop around
element.textContent and data-latex so latex text is escaped before
interpolation. Escape backslashes and LaTeX-significant characters (\ , {, }, _,
^, $, %, &, #, ~) while preserving the existing subscript/superscript wrapper
and removal behavior for empty content.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a700fb0f-693d-41c4-b01a-3f425163e5d7

📥 Commits

Reviewing files that changed from the base of the PR and between b234418 and bc7136b.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • src/features/workspaces/operations/__snapshots__/workspace-tool-surface.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (47)
  • .gitignore
  • eval/datasets/workspace-tools.cases.ts
  • eval/support/harness.ts
  • eval/support/scorers.ts
  • eval/workspace-tools.eval.ts
  • package.json
  • scripts/copy-widget-libs.mjs
  • src/features/workspaces/ai/ai-thread-soul-prompt.ts
  • src/features/workspaces/ai/ai-thread.ts
  • src/features/workspaces/ai/skills/widget-authoring/SKILL.md
  • src/features/workspaces/ai/workspace-citations.test.ts
  • src/features/workspaces/components/WorkspaceItemToolbarSlot.tsx
  • src/features/workspaces/components/ai-chat/AiChatPromptInput.tsx
  • src/features/workspaces/components/ai-chat/ai-chat-tool-receipts.ts
  • src/features/workspaces/components/ai-chat/useConsumeComposerPrompt.ts
  • src/features/workspaces/components/document-editor/DocumentEditorSurface.tsx
  • src/features/workspaces/components/document-editor/DocumentToolbar.tsx
  • src/features/workspaces/components/widget/WorkspaceAddWidgetDialog.tsx
  • src/features/workspaces/components/widget/WorkspaceWidgetSandbox.test.tsx
  • src/features/workspaces/components/widget/WorkspaceWidgetSandbox.tsx
  • src/features/workspaces/components/widget/workspace-widget-sandbox-document.test.ts
  • src/features/workspaces/components/widget/workspace-widget-sandbox-document.ts
  • src/features/workspaces/composer/workspace-composer-actions.ts
  • src/features/workspaces/content/workspace-content-contract.test.ts
  • src/features/workspaces/content/workspace-content-contract.ts
  • src/features/workspaces/content/workspace-content-reader.test.ts
  • src/features/workspaces/content/workspace-content-reader.ts
  • src/features/workspaces/content/workspace-read-references.test.ts
  • src/features/workspaces/content/workspace-read-references.ts
  • src/features/workspaces/documents/document-ai-edits.test.ts
  • src/features/workspaces/documents/document-ai-edits.ts
  • src/features/workspaces/documents/document-ai-html.test.ts
  • src/features/workspaces/documents/document-ai-html.ts
  • src/features/workspaces/documents/document-ai-html.worker.test.ts
  • src/features/workspaces/documents/document-preview-text.test.ts
  • src/features/workspaces/documents/document-preview-text.ts
  • src/features/workspaces/documents/document-session.ts
  • src/features/workspaces/documents/document-widget-extension.ts
  • src/features/workspaces/documents/document-widget-node.tsx
  • src/features/workspaces/documents/tiptap-extensions.ts
  • src/features/workspaces/documents/tiptap-schema.ts
  • src/features/workspaces/model/use-workspace-item-path.ts
  • src/features/workspaces/model/workspace-ai-context-reference.ts
  • src/features/workspaces/operations/workspace-tool-definitions.ts
  • src/features/workspaces/operations/workspace-tool-schemas.ts
  • src/features/workspaces/state/workspace-ai-composer-draft-store.ts
  • src/styles.css

Comment thread eval/datasets/workspace-tools.cases.ts Outdated
Comment thread eval/support/scorers.ts Outdated
Comment thread src/features/workspaces/components/ai-chat/useConsumeComposerPrompt.ts Outdated
Comment thread src/features/workspaces/content/workspace-content-reader.ts Outdated
Comment on lines +35 to +45
export function readWorkspaceItemPath(
queryClient: QueryClient,
workspaceId: string,
item: WorkspaceItem,
) {
const page = queryClient.getQueryData(workspacePageQueryOptions(workspaceId).queryKey);
if (!page) {
return null;
}

return getWorkspaceAiContextItemPath(item, new Map(page.items.map((entry) => [entry.id, entry])));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the resolver and all imperative helper callers.
rg -n -C 12 \
  '\bgetWorkspaceAiContextItemPath\s*\(|\breadWorkspaceItemPath\s*\(' \
  --type=ts --type=tsx .

Repository: ThinkEx-OSS/thinkex

Length of output: 189


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo 'Matching files:'
git ls-files | grep -E '\.(ts|tsx)$' | sed -n '1,50p'

echo
echo 'Search for path helpers (if grep supports -E):'
rg -n -C 12 '\bgetWorkspaceAiContextItemPath\s*\(|\breadWorkspaceItemPath\s*\(' \
  --glob '*.ts' --glob '*.tsx' . \
  || true

Repository: ThinkEx-OSS/thinkex

Length of output: 13030


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo 'use-workspace-item-path.ts:'
cat -n src/features/workspaces/model/use-workspace-item-path.ts

echo
echo 'workspace-ai-context-reference.ts:'
cat -n src/features/workspaces/model/workspace-ai-context-reference.ts

echo
echo 'WorkspaceItem type references:'
rg -n -C 4 'interface WorkspaceItem|type WorkspaceItem|WorkspaceItem =|interface WorkspaceBreadcrumb' --glob '*.ts' --glob '*.tsx' src

Repository: ThinkEx-OSS/thinkex

Length of output: 25101


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo 'tree.ts:'
cat -n src/features/workspaces/model/tree.ts

echo
echo 'types.ts relevant WorkspaceItem schema/type:'
cat -n src/features/workspaces/model/types.ts | sed -n '1,220p'

Repository: ThinkEx-OSS/thinkex

Length of output: 4781


Resolve the item from the cached page before building the path.

getWorkspaceBreadcrumbItems(item, itemsById) walks item.parentId up through itemsById, but both the final item and ancestor paths are built from item’s name and parentId. A stale captured object can therefore produce an out-of-date AI context path. Use the cached item for the base segment, and also resolve any renamed/moved ancestors from page.items.

🤖 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 `@src/features/workspaces/model/use-workspace-item-path.ts` around lines 35 -
45, Update readWorkspaceItemPath to resolve the requested item from the
page.items map before calling getWorkspaceAiContextItemPath, using the cached
object as the path’s base item and ensuring ancestor traversal resolves cached
entries by parentId. Preserve the null return when no cached page exists, and
handle a missing cached item without using the stale captured item.

Comment thread src/styles.css
* the editor.
*/
--workspace-document-measure: 80ch;
padding-inline: max(1rem, calc((100% - var(--workspace-document-measure)) / 2));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Add the required empty line before padding-inline.

Stylelint reports declaration-empty-line-before at Line 509. Add an empty line after the explanatory comment.

Proposed fix
 		 */
+
 		--workspace-document-measure: 80ch;
 		padding-inline: max(1rem, calc((100% - var(--workspace-document-measure)) / 2));
🧰 Tools
🪛 Stylelint (17.14.1)

[error] 509-509: Expected empty line before declaration (declaration-empty-line-before)

(declaration-empty-line-before)

🤖 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 `@src/styles.css` at line 509, Add an empty line immediately after the
explanatory comment and before the padding-inline declaration in the relevant
CSS rule, leaving the declaration unchanged.

Source: Linters/SAST tools

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

3 issues found across 49 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/components/widget/WorkspaceAddWidgetDialog.tsx">

<violation number="1" location="src/features/workspaces/components/widget/WorkspaceAddWidgetDialog.tsx:76">
P2: On mobile, submitting “Add with AI” stages the request but leaves the chat hidden, so the user sees no composer and the new widget flow appears to do nothing until they open Chat manually. Use a mobile-aware reveal action that selects fullscreen on mobile (or update the shared staging helper to handle mobile).</violation>
</file>

<file name="src/features/workspaces/components/widget/WorkspaceWidgetSandbox.tsx">

<violation number="1" location="src/features/workspaces/components/widget/WorkspaceWidgetSandbox.tsx:141">
P3: Documents with multiple widgets are indistinguishable to screen-reader users because every embedded frame is announced as “Widget preview.” Passing the node’s widget label into this component and incorporating it into the iframe title would preserve the accessible context.</violation>

<violation number="2" location="src/features/workspaces/components/widget/WorkspaceWidgetSandbox.tsx:143">
P1: Widgets can still make network requests despite the documented no-network boundary: the sandbox does not restrict resource loading, and the generated CSP permits every app-origin script URL. Narrow the CSP to the exact bundled KaTeX asset paths (and add an explicit navigation restriction) so widget-controlled URLs cannot send data to the app or another server.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

ref={iframeRef}
title="Widget preview"
sandbox="allow-scripts"
srcDoc={srcDoc ?? undefined}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Widgets can still make network requests despite the documented no-network boundary: the sandbox does not restrict resource loading, and the generated CSP permits every app-origin script URL. Narrow the CSP to the exact bundled KaTeX asset paths (and add an explicit navigation restriction) so widget-controlled URLs cannot send data to the app or another server.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/widget/WorkspaceWidgetSandbox.tsx, line 143:

<comment>Widgets can still make network requests despite the documented no-network boundary: the sandbox does not restrict resource loading, and the generated CSP permits every app-origin script URL. Narrow the CSP to the exact bundled KaTeX asset paths (and add an explicit navigation restriction) so widget-controlled URLs cannot send data to the app or another server.</comment>

<file context>
@@ -0,0 +1,172 @@
+					ref={iframeRef}
+					title="Widget preview"
+					sandbox="allow-scripts"
+					srcDoc={srcDoc ?? undefined}
+					className="h-full w-full border-0 bg-background"
+				/>
</file context>

Comment thread src/features/workspaces/model/use-workspace-item-path.ts Outdated
stageComposerPrompt(
workspaceId,
`Add an interactive widget to ${documentPath}: ${description}`,
{ revealChat: true },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: On mobile, submitting “Add with AI” stages the request but leaves the chat hidden, so the user sees no composer and the new widget flow appears to do nothing until they open Chat manually. Use a mobile-aware reveal action that selects fullscreen on mobile (or update the shared staging helper to handle mobile).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/widget/WorkspaceAddWidgetDialog.tsx, line 76:

<comment>On mobile, submitting “Add with AI” stages the request but leaves the chat hidden, so the user sees no composer and the new widget flow appears to do nothing until they open Chat manually. Use a mobile-aware reveal action that selects fullscreen on mobile (or update the shared staging helper to handle mobile).</comment>

<file context>
@@ -0,0 +1,111 @@
+					stageComposerPrompt(
+						workspaceId,
+						`Add an interactive widget to ${documentPath}: ${description}`,
+						{ revealChat: true },
+					);
+					onOpenChange(false);
</file context>

Comment thread src/features/workspaces/operations/workspace-tool-schemas.ts Outdated
Comment thread eval/support/harness.ts
Comment thread eval/datasets/workspace-tools.cases.ts Outdated
Comment thread eval/datasets/workspace-tools.cases.ts Outdated
Comment thread src/features/workspaces/model/use-workspace-item-path.ts Outdated
Comment thread src/features/workspaces/content/workspace-content-reader.ts Outdated
Comment thread src/features/workspaces/components/ai-chat/useConsumeComposerPrompt.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

2 issues found across 51 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/components/ai-chat/AiChatPromptInput.tsx">

<violation number="1" location="src/features/workspaces/components/ai-chat/AiChatPromptInput.tsx:161">
P2: Staging with `revealChat: false` can consume the focus request while the composer is hidden: the mounted textarea is focused inside the `invisible` chat wrapper, then `clearFocusRequest` runs. Focus only when the composer is visible, or avoid creating the request when chat will remain hidden, so a later chat open can still focus the staged text.</violation>
</file>

<file name="src/features/workspaces/components/widget/WorkspaceWidgetSandbox.test.tsx">

<violation number="1" location="src/features/workspaces/components/widget/WorkspaceWidgetSandbox.test.tsx:141">
P3: Add coverage for the onAskAiToFix path: render the sandbox with onAskAiToFix, trigger a runtime error, and assert the "Ask AI to fix" button calls the callback with the error message. This error-repair affordance is a headline feature of the PR and is currently the only behavior in the error view with no test.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/features/workspaces/content/workspace-content-contract.ts Outdated
Comment thread eval/support/harness.ts Outdated
textarea.focus();
const caret = textarea.value.length;
textarea.setSelectionRange(caret, caret);
clearFocusRequest(activeThreadId, focusRequest);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Staging with revealChat: false can consume the focus request while the composer is hidden: the mounted textarea is focused inside the invisible chat wrapper, then clearFocusRequest runs. Focus only when the composer is visible, or avoid creating the request when chat will remain hidden, so a later chat open can still focus the staged text.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/ai-chat/AiChatPromptInput.tsx, line 161:

<comment>Staging with `revealChat: false` can consume the focus request while the composer is hidden: the mounted textarea is focused inside the `invisible` chat wrapper, then `clearFocusRequest` runs. Focus only when the composer is visible, or avoid creating the request when chat will remain hidden, so a later chat open can still focus the staged text.</comment>

<file context>
@@ -130,6 +144,25 @@ export default function AiChatPromptInput({
+			textarea.focus();
+			const caret = textarea.value.length;
+			textarea.setSelectionRange(caret, caret);
+			clearFocusRequest(activeThreadId, focusRequest);
+		});
+
</file context>

Comment thread eval/support/scorers.ts
Comment thread src/features/workspaces/components/widget/workspace-widget-sandbox-document.ts Outdated
Comment thread eval/datasets/workspace-tools.cases.ts Outdated
expect(frame?.style.height).toBe("120px");
});

it("lets the error view size itself instead of keeping the widget height", async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: Add coverage for the onAskAiToFix path: render the sandbox with onAskAiToFix, trigger a runtime error, and assert the "Ask AI to fix" button calls the callback with the error message. This error-repair affordance is a headline feature of the PR and is currently the only behavior in the error view with no test.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/widget/WorkspaceWidgetSandbox.test.tsx, line 141:

<comment>Add coverage for the onAskAiToFix path: render the sandbox with onAskAiToFix, trigger a runtime error, and assert the "Ask AI to fix" button calls the callback with the error message. This error-repair affordance is a headline feature of the PR and is currently the only behavior in the error view with no test.</comment>

<file context>
@@ -0,0 +1,216 @@
+		expect(frame?.style.height).toBe("120px");
+	});
+
+	it("lets the error view size itself instead of keeping the widget height", async () => {
+		await act(async () => root.render(<WorkspaceWidgetSandbox html="<p>broken</p>" />));
+
</file context>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0 issues found across 5 files (changes from recent commits).

Requires human review: Auto-approval blocked by 10 unresolved issues from previous reviews.

Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 9 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="eval/support/harness.ts">

<violation number="1" location="eval/support/harness.ts:205">
P2: A schema-valid edit path such as `toString` can make the targeted-edit evaluator throw instead of reporting a failed provenance check, because the plain object exposes inherited properties. Build the snapshot with a null prototype (or use an own-property lookup) before grading path-keyed refs.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread eval/support/harness.ts
name: part.toolName,
known: Boolean(definition),
input: part.input,
priorReadEditRefsByPath: Object.fromEntries(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: A schema-valid edit path such as toString can make the targeted-edit evaluator throw instead of reporting a failed provenance check, because the plain object exposes inherited properties. Build the snapshot with a null prototype (or use an own-property lookup) before grading path-keyed refs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At eval/support/harness.ts, line 205:

<comment>A schema-valid edit path such as `toString` can make the targeted-edit evaluator throw instead of reporting a failed provenance check, because the plain object exposes inherited properties. Build the snapshot with a null prototype (or use an own-property lookup) before grading path-keyed refs.</comment>

<file context>
@@ -178,7 +202,9 @@ export async function runWorkspaceAgent(input: WorkspaceAgentInput): Promise<Wor
 				name: part.toolName,
 				input: part.input,
-				priorReadEditRefs: [...priorReadEditRefs],
+				priorReadEditRefsByPath: Object.fromEntries(
+					[...priorReadEditRefsByPath].map(([path, refs]) => [path, [...refs]]),
+				),
</file context>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/components/widget/workspace-widget-sandbox-document.ts">

<violation number="1" location="src/features/workspaces/components/widget/workspace-widget-sandbox-document.ts:248">
P2: Removing `data-latex` leaves the previously rendered formula displayed because the observer handles only targets that still have the attribute. The transition should clear or restore the node's non-math content when the attribute is removed so interactive widgets cannot show stale math.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

new MutationObserver(function(records){
records.forEach(function(record){
if(record.type==="attributes"){
if(record.target.matches&&record.target.matches("[data-latex]")){

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Removing data-latex leaves the previously rendered formula displayed because the observer handles only targets that still have the attribute. The transition should clear or restore the node's non-math content when the attribute is removed so interactive widgets cannot show stale math.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/widget/workspace-widget-sandbox-document.ts, line 248:

<comment>Removing `data-latex` leaves the previously rendered formula displayed because the observer handles only targets that still have the attribute. The transition should clear or restore the node's non-math content when the attribute is removed so interactive widgets cannot show stale math.</comment>

<file context>
@@ -222,23 +222,45 @@ body{padding:clamp(10px,2.5%,16px);}
+      new MutationObserver(function(records){
+        records.forEach(function(record){
+          if(record.type==="attributes"){
+            if(record.target.matches&&record.target.matches("[data-latex]")){
+              delete record.target.dataset.widgetMathRendered;
+              try{renderLatexNode(record.target);}catch(_){}
</file context>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 11 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/components/widget/WorkspaceWidgetSandbox.tsx">

<violation number="1" location="src/features/workspaces/components/widget/WorkspaceWidgetSandbox.tsx:160">
P2: Read-only viewers lose recovery for non-ready widget errors: when `preserveFrame` is false and `onAskAiToFix` is omitted, this condition removes the iframe and renders no action. Retaining a retry/remount path for consumers without an AI-fix callback would let transient frame failures recover.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

<div className="max-h-32 w-full max-w-lg overflow-auto whitespace-pre-wrap rounded-md bg-muted px-4 py-3 font-mono text-muted-foreground text-xs leading-relaxed">
{error.message}
</div>
{error.preserveFrame || onAskAiToFix ? (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Read-only viewers lose recovery for non-ready widget errors: when preserveFrame is false and onAskAiToFix is omitted, this condition removes the iframe and renders no action. Retaining a retry/remount path for consumers without an AI-fix callback would let transient frame failures recover.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/widget/WorkspaceWidgetSandbox.tsx, line 160:

<comment>Read-only viewers lose recovery for non-ready widget errors: when `preserveFrame` is false and `onAskAiToFix` is omitted, this condition removes the iframe and renders no action. Retaining a retry/remount path for consumers without an AI-fix callback would let transient frame failures recover.</comment>

<file context>
@@ -162,26 +157,25 @@ export function WorkspaceWidgetSandbox({
-							</Button>
-						) : null}
-					</div>
+					{error.preserveFrame || onAskAiToFix ? (
+						<div className="flex items-center gap-2">
+							{error.preserveFrame ? (
</file context>

Comment thread eval/support/harness.ts
const blockId = blockIds[index];
if (!blockId) throw new Error("Eval standup fixture has an unexpected block count.");
const node = withTiptapNodeAiRef(document.child(index), blockId);
const snapshot = await createDocumentAiBlockSnapshot(node);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/async-await-in-loop (warning)

This makes the for-loop slow because each await runs one after another, so collect the independent calls & run them together with await Promise.all(items.map(...))

Fix → Collect the items, then use await Promise.all(items.map(...)) so independent work runs at the same time

Docs

Comment thread eval/support/harness.ts
execute: async () => evalToolFixture(definition.name),
}),
]),
workspaceToolDefinitions

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/js-combine-iterations (warning)

This loops over your list twice because .filter().map() makes two passes, so do it in one pass with .reduce() or a for...of loop

Fix → Combine .map().filter() style chains into one pass with .reduce() or a for...of loop, so you only loop over the list once

Docs

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0 issues found across 8 files (changes from recent commits).

Requires human review: Auto-approval blocked by 8 unresolved issues from previous reviews.

Re-trigger cubic

@urjitc
urjitc merged commit 4832c6f into main Aug 4, 2026
12 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Dev Board Aug 4, 2026
@urjitc
urjitc deleted the codex/widgets-in-documents branch August 5, 2026 06:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants