Skip to content

feat(workspaces): quiz study items, self-describing AI addresses, and shared study UI - #788

Merged
urjitc merged 15 commits into
mainfrom
review/quiz-addresses-split
Aug 15, 2026
Merged

feat(workspaces): quiz study items, self-describing AI addresses, and shared study UI#788
urjitc merged 15 commits into
mainfrom
review/quiz-addresses-split

Conversation

@urjitc

@urjitc urjitc commented Aug 15, 2026

Copy link
Copy Markdown
Member

Adds quizzes as a workspace item type, replaces the AI's per-turn reference registry with self-describing addresses, and brings the flashcard and quiz viewers onto one shared study UI.

Ten commits, each reviewable on its own. The first four are the feature; the last six are the UI and validation work that followed.

What changes

Quiz items. Multiple choice, room for free response later. The model authors question + correctAnswer + distractors + explanation; the server shuffles the options and records which one is correct, so option order can never leak the answer. Answers lock on first pick. Citations resolve to a single question, the way flashcard citations resolve to a card.

Addresses replace wr_ refs. An address is refKey[/unit][.r_revision] — an 8-character per-item key, an optional unit (p5, b_…, a card or question id), and a 6-character content-hash revision. Resolution is stateless, so nothing has to be handed to the model and kept in sync per turn. Reads report returned/total and continue by asking for the next range instead of carrying a cursor.

One study UI. The quiz viewer now uses the flashcard shell — same margins, scrollable body, progress strip, footer. Answer choices are Khan-style rows in one list, picking only selects, and a Submit button grades. Four layout bugs were fixed at the root and measured in the browser rather than eyeballed: unmounting hairlines, a collapsing Hint slot, an editor-only paragraph min-height leaking into read-only prose, and scroll position surviving question changes.

Mermaid diagrams render in documents, flashcards, and quizzes, not just chat. A mermaid block is an ordinary code block, so study items get diagrams without opening their allowlist to widgets, which stay documents-only. PDF export drops them rather than printing authoring syntax.

Entry rich text is validated at the model boundary only. Re-running the allowlist over stored content could only turn one odd entry into an item that will not open.

Review notes

  • Commits are topical, not bisectable — only the tip is guaranteed to build.
  • One accepted risk, flagged rather than fixed: correctOptionId and the explanation reach the client before you answer. Fine for self-study; needs server-side grading if quiz scores ever become a signal worth trusting.
  • The packageManager bump sitting in the working tree is deliberately not here.

Test plan

  • pnpm verify (check + test + build) green on this tree.
  • Quiz and flashcard viewers driven in a real browser: answering, navigating, grading, the progress strip, and both at a 377px-tall viewport to exercise the scroll path.

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

urjitc added 10 commits August 15, 2026 01:34
Multiple-choice quizzes as a first-class item: the model authors
question + correctAnswer + distractors + explanation, the server
shuffles the answer in and stores the option order, and a first-pick-
wins immediate-feedback viewer persists per-user answers. Extracts the
shared ordered-entry edit engine and entry rich-text dialect from
flashcards so both study types run on one mechanism, and generalizes
the study toolbar and creation dialog.

Part 1 of a stacked series; builds together with the address refactor
it was developed alongside.
Every workspace item gets a stable 8-character refKey, minted on
insert and backfilled by migration from a ~47-bit alphanumeric space
with a dedupe pass before the unique index. This is the model-facing
handle the address system resolves statelessly, so nothing per-turn
has to be minted or kept alive for a reference to keep working.

Part 2 of the stacked series.
Deletes the per-turn wr_ reference registry, the four-kind read cursor
union, and the byte-budget latch. Reads follow an outline-and-units
model: every item is numbered units (blocks, pages, cards, questions),
every result names the units it covered plus the total, and
continuation is just the next range. Refs become stateless addresses -
refKey[/unit][.r_revision] - so edits, citations, and re-reads resolve
from the string alone, survive compaction, and need no Durable Object
turn state or transcript reconciliation.

Part 3 of the stacked series.
Entry-range failures carry their real codes and a recovery message,
the range description no longer teaches comma lists to documents, and
the simplification pass lands: one shared Fisher-Yates, alias chains
and the leftover adapter factory removed, dead userId branches and
unused exports deleted.

Part 4 of the stacked series; tree is byte-identical to the verified
add-quiz-item-type branch.
The flashcard and quiz viewers each carried their own copy of the same
pieces: a read-only Tiptap render, the amber "ask AI" button, the
prev/next arrows, and the progress strip. Same look, four chances to
drift.

Extracts them into components/study: StudyRichText, StudyAiActionButton,
StudyNavButton, and StudyProgressStrip, whose segments now carry a tone
so each item type maps its own state onto one strip.
Centred text is hard to read past a line or two, and it forced a
display: inline-block hack on lists to keep them from centring with it.
The prose block still centres inside the face; only the text aligns
left.
Same shell as flashcards — margins, scrollable body, progress strip,
footer — so the two study surfaces read as one product. Answer choices
become Khan-style rows in a single list rather than a card each, picking
one only selects it, and a Submit button in the footer grades.

Four layout bugs went with it, each fixed by a rule rather than an
offset, and each measured in the browser:

- Rows shifted when the outline moved, because the hairlines between
  them unmounted. They now stay in the flow and hide by colour.
- Everything shifted up on submit, because the row holding the Hint
  button collapsed when grading removed it. The button sits in a slot
  that keeps its height.
- Option text sat above its letter, because the prose rule that gives an
  empty paragraph a click target while editing also padded read-only
  renders. Scoped that min-height to editable prose.
- Scroll position survived question changes, dropping you past the next
  stem. The scroller is keyed by question.
The allowlist earns its keep at the model boundary: study viewers render
with the full document schema, so an unchecked widget really would embed
a sandboxed iframe inside a card face. It earns nothing on the way out,
where re-running it over content that already passed can only turn one
odd entry into an item that will not open.

Storage keeps the structural check and drops the allowlist. The parser
factory and its six message strings collapse into two functions taking
the item's label.
Chat could already draw mermaid. Documents, flashcards, and quizzes
could not, which left diagrams out of exactly the surfaces that explain
things. A mermaid block is an ordinary code block, so cards and
questions get diagrams without opening their allowlist to widgets, which
stay documents-only.

AiChatMermaidDiagram becomes a shared MermaidDiagram taking isIncomplete
as a prop, since only chat can see a half-written fence, and caches
rendered SVGs so a card flipped back and forth does not re-render its
artwork. CodeBlockNodeView draws the diagram in place of the source and
selects the whole block on click, the way an image behaves. PDF export
drops mermaid blocks rather than printing authoring syntax.

Tool schemas gain one diagram instruction shared by all three item
types, plus a note that a diagram usually belongs in a quiz stem or
explanation, since one option taller than the rest hints at the answer.
The skeleton still drew one big rounded card, which the quiz stopped
being when the answer rows moved out into a plain list. It now traces
the real shape: a stem line with the Hint slot beside it, four option
rows with their letter badges and hairlines, then the progress strip
and footer.
@cursor

cursor Bot commented Aug 15, 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.

@greptile-apps

greptile-apps Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review (109 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 109 files, which is 9 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3264b56d-6a67-4730-bdfb-130cda381617

📥 Commits

Reviewing files that changed from the base of the PR and between da61bb5 and c9f217a.

⛔ Files ignored due to path filters (1)
  • src/features/workspaces/operations/__snapshots__/workspace-tool-surface.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (109)
  • drizzle-postgres/0004_charming_doomsday.sql
  • drizzle-postgres/0005_dashing_masked_marvel.sql
  • drizzle-postgres/meta/0004_snapshot.json
  • drizzle-postgres/meta/0005_snapshot.json
  • drizzle-postgres/meta/_journal.json
  • eval/datasets/workspace-tools.cases.ts
  • eval/support/harness.ts
  • eval/workspace-tools.eval.ts
  • src/components/code-block/mermaid-diagram.tsx
  • src/db/schema.ts
  • src/features/workspaces/ai/ai-thread-runtime.ts
  • src/features/workspaces/ai/ai-thread.ts
  • src/features/workspaces/ai/workspace-read-file-fallback.ts
  • src/features/workspaces/ai/workspace-references.test.ts
  • src/features/workspaces/ai/workspace-references.ts
  • src/features/workspaces/ai/workspace-tool-result-adapters.test.ts
  • src/features/workspaces/ai/workspace-tool-result-adapters.ts
  • src/features/workspaces/ai/workspace-tools.ts
  • src/features/workspaces/cache-page.test.ts
  • src/features/workspaces/components/WorkspaceCitation.tsx
  • src/features/workspaces/components/WorkspaceContent.tsx
  • src/features/workspaces/components/WorkspaceLayout.tsx
  • src/features/workspaces/components/ai-chat/AiChatMessagePartView.tsx
  • src/features/workspaces/components/ai-chat/AiChatMessageResponse.test.tsx
  • src/features/workspaces/components/ai-chat/AiChatMessageResponse.tsx
  • src/features/workspaces/components/ai-chat/AiChatMessageRow.tsx
  • src/features/workspaces/components/ai-chat/ai-chat-code-block.tsx
  • src/features/workspaces/components/ai-chat/ai-chat-tool-receipts.ts
  • src/features/workspaces/components/flashcards/FlashcardViewer.tsx
  • src/features/workspaces/components/flashcards/flashcard-viewer.css
  • src/features/workspaces/components/quizzes/QuizViewer.tsx
  • src/features/workspaces/components/study/CreateStudyItemDialog.tsx
  • src/features/workspaces/components/study/StudyProgressStrip.tsx
  • src/features/workspaces/components/study/StudyRichText.tsx
  • src/features/workspaces/components/study/StudySessionControls.tsx
  • src/features/workspaces/components/study/StudyToolbar.tsx
  • src/features/workspaces/components/workspace-presentation-model.ts
  • src/features/workspaces/content/entry-rich-text.ts
  • src/features/workspaces/content/ordered-entry-edits.ts
  • src/features/workspaces/content/workspace-content-contract.test.ts
  • src/features/workspaces/content/workspace-content-contract.ts
  • src/features/workspaces/content/workspace-content-cursor.ts
  • src/features/workspaces/content/workspace-content-reader.test.ts
  • src/features/workspaces/content/workspace-content-reader.ts
  • src/features/workspaces/content/workspace-read-model-output.test.ts
  • src/features/workspaces/content/workspace-read-model-output.ts
  • src/features/workspaces/content/workspace-read-references.test.ts
  • src/features/workspaces/content/workspace-read-references.ts
  • src/features/workspaces/contracts.ts
  • src/features/workspaces/documents/code-block-shiki/CodeBlockNodeView.tsx
  • src/features/workspaces/documents/code-block-shiki/code-languages.ts
  • src/features/workspaces/documents/code-block-shiki/highlighter.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-html-chunk.ts
  • src/features/workspaces/documents/document-session.ts
  • src/features/workspaces/documents/tiptap-schema.ts
  • src/features/workspaces/export/workspace-document-pdf-html.test.ts
  • src/features/workspaces/export/workspace-document-pdf-html.ts
  • src/features/workspaces/export/workspace-export-archive.test.ts
  • src/features/workspaces/export/workspace-export-archive.ts
  • src/features/workspaces/export/workspace-export.ts
  • src/features/workspaces/flashcards/flashcard-content.test.ts
  • src/features/workspaces/flashcards/flashcard-content.ts
  • src/features/workspaces/flashcards/flashcard-edits.test.ts
  • src/features/workspaces/flashcards/flashcard-edits.ts
  • src/features/workspaces/flashcards/flashcard-functions.ts
  • src/features/workspaces/flashcards/flashcard-persistence.ts
  • src/features/workspaces/flashcards/flashcard-study-session.ts
  • src/features/workspaces/flashcards/flashcard-study-state.ts
  • src/features/workspaces/locations/workspace-location-context.test.tsx
  • src/features/workspaces/locations/workspace-location-context.tsx
  • src/features/workspaces/locations/workspace-location.test.ts
  • src/features/workspaces/locations/workspace-location.ts
  • src/features/workspaces/model/item-display.ts
  • src/features/workspaces/model/workspace-ai-context-snapshot.test.ts
  • src/features/workspaces/model/workspace-page.test.ts
  • src/features/workspaces/model/workspace-page.ts
  • src/features/workspaces/operations/create-items.ts
  • src/features/workspaces/operations/document-citations.ts
  • src/features/workspaces/operations/edit-item.ts
  • src/features/workspaces/operations/link-items.test.ts
  • src/features/workspaces/operations/read-items.ts
  • src/features/workspaces/operations/workspace-access-context.ts
  • src/features/workspaces/operations/workspace-operation-failure-codes.ts
  • src/features/workspaces/operations/workspace-tool-definitions.ts
  • src/features/workspaces/operations/workspace-tool-schemas.ts
  • src/features/workspaces/operations/workspace-tool-surface.test.ts
  • src/features/workspaces/persistence/workspace-files.ts
  • src/features/workspaces/persistence/workspace-items.ts
  • src/features/workspaces/persistence/workspace-postgres-support.ts
  • src/features/workspaces/quizzes/quiz-content.test.ts
  • src/features/workspaces/quizzes/quiz-content.ts
  • src/features/workspaces/quizzes/quiz-edits.test.ts
  • src/features/workspaces/quizzes/quiz-edits.ts
  • src/features/workspaces/quizzes/quiz-functions.ts
  • src/features/workspaces/quizzes/quiz-persistence.ts
  • src/features/workspaces/quizzes/quiz-queries.ts
  • src/features/workspaces/quizzes/quiz-study-persistence.ts
  • src/features/workspaces/quizzes/quiz-study-session.ts
  • src/features/workspaces/quizzes/quiz-study-state.test.ts
  • src/features/workspaces/quizzes/quiz-study-state.ts
  • src/features/workspaces/workspace-item-registry.ts
  • src/lib/binary.ts
  • src/lib/shuffle.ts
  • src/styles.css

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


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.

@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.

23 issues found across 109 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/documents/document-session.ts">

<violation number="1" location="src/features/workspaces/documents/document-session.ts:357">
P3: `readHtmlChunk` no longer uses the snapshot revision, but `getReferencedDocumentSnapshot()` still computes and returns `stateVector` for every read. Remove that field and its `Y.encodeStateVector` computation to avoid dead work on each document read.</violation>
</file>

<file name="src/features/workspaces/content/entry-rich-text.ts">

<violation number="1" location="src/features/workspaces/content/entry-rich-text.ts:15">
P2: When an entry field is syntactically nonempty but renders no content, such as `<p></p>`, this schema accepts it and `parseEntryRichTextHtml` stores a blank rich-text document. That permits blank quiz answers, stems, explanations, or flashcard sides; validate the normalized document for meaningful content at the model boundary (and in the parser used by direct callers).</violation>
</file>

<file name="src/features/workspaces/quizzes/quiz-queries.ts">

<violation number="1" location="src/features/workspaces/quizzes/quiz-queries.ts:16">
P3: This module duplicates the complete study-query lifecycle from `flashcard-queries.ts`, so fixes to cancellation, optimistic rollback, or cache-version handling must be made twice. Extract a shared study-cache helper and pass the item-specific query and state functions into it.</violation>

<violation number="2" location="src/features/workspaces/quizzes/quiz-queries.ts:58">
P2: When an older `updatedAt` variant remains cached and a quiz mutation fails, the optimistic answer is rolled back only for the current variant. The older variant can show the rejected answer when reopened until its refetch completes; snapshot and restore every key modified optimistically, or update only the exact key and invalidate the others.</violation>
</file>

<file name="src/features/workspaces/quizzes/quiz-persistence.ts">

<violation number="1" location="src/features/workspaces/quizzes/quiz-persistence.ts:18">
P3: This file duplicates the complete structured-item persistence flow from `flashcard-persistence.ts`, so future fixes to locking, revisions, or notifications can diverge between flashcards and quizzes. Extract the shared transaction and notification logic into a generic structured-item persistence helper, leaving only type-specific parsing and serialization in each module.</violation>
</file>

<file name="src/features/workspaces/documents/code-block-shiki/CodeBlockNodeView.tsx">

<violation number="1" location="src/features/workspaces/documents/code-block-shiki/CodeBlockNodeView.tsx:25">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The PR adds mermaid diagram rendering in documents, flashcards, and quizzes, but the new document rendering path has no automated tests. Rule 1 requires behavior changes to be exercised by tests when a regression-style assertion is practical. Add a test that verifies a mermaid `codeBlock` renders `<MermaidDiagram>` (or the `.workspace-document-diagram` wrapper) instead of the usual `<NodeViewWrapper className="workspace-document-code-block">` path.</violation>

<violation number="2" location="src/features/workspaces/documents/code-block-shiki/CodeBlockNodeView.tsx:54">
P2: Clicking a Mermaid block selects it but does not make it draggable, so users cannot move the block as the new image-like behavior promises. Mark the code-block node as draggable or add equivalent drag handling.</violation>
</file>

<file name="src/features/workspaces/model/workspace-page.ts">

<violation number="1" location="src/features/workspaces/model/workspace-page.ts:43">
P2: The optimistic `refKey` is a random client-minted placeholder that deliberately differs from the server-minted key (`createWorkspaceItemRefKey()` at workspace-items.ts:241 mints an independent value, and the onSuccess/realtime upsert replaces the item by id). During the window before that upsert lands, any client-built address that copies this placeholder `refKey` (e.g. a citation or reference authored against the freshly created item, resolved via `workspace-location-context.tsx:69` or server-side `getWorkspaceItemByRefKey`) is persisted with a key that never resolves after the placeholder is discarded. Because the refKey is the durable model-facing handle, consider minting the real key on the client reject path or deferring refKey-dependent reads until the server-minted key arrives, rather than exposing a placeholder that can be captured into durable content.</violation>
</file>

<file name="src/features/workspaces/components/study/StudySessionControls.tsx">

<violation number="1" location="src/features/workspaces/components/study/StudySessionControls.tsx:32">
P3: The unconditional `onKeyDown={(event) => event.stopPropagation()}` on StudyAiActionButton swallows every key press from the Hint/Explain button, so when it holds focus the parent `<section>`'s ArrowLeft/ArrowRight navigation (QuizViewer.tsx:305 and FlashcardViewer.tsx:460) stops working. This is inconsistent with StudyNavButton, which intentionally lets arrows bubble to the same section handler, and it breaks the arrow-key shortcuts advertised in the nav button title. Drop the onKeyDown handler — `onClick` already stops the click from reaching the flashcard flip handler, and the flip-on-space path is separately guarded by `closest("button, a")` — so arrow navigation behaves identically regardless of which control has focus.</violation>
</file>

<file name="src/features/workspaces/components/quizzes/QuizViewer.tsx">

<violation number="1" location="src/features/workspaces/components/quizzes/QuizViewer.tsx:192">
P2: When switching to Missed after all questions are correct, `currentQuestion` becomes undefined and this effect returns without clearing the last view state. AI context continues reporting the previous question while the viewer shows “No missed questions”; clear this view instance when no current question exists.</violation>
</file>

<file name="src/features/workspaces/locations/workspace-location.ts">

<violation number="1" location="src/features/workspaces/locations/workspace-location.ts:52">
P2: The shared entry schema allows flashcard locations to carry `q_` IDs and quiz locations to carry `c_` IDs. Enforce the `c_` prefix for flashcards and `q_` for quiz questions while retaining UUID support for legacy content, so malformed persisted content and cross-type citations cannot validate.</violation>
</file>

<file name="src/features/workspaces/components/study/StudyRichText.tsx">

<violation number="1" location="src/features/workspaces/components/study/StudyRichText.tsx:27">
P2: Read-only study content still exposes the code-block language selector, and selecting a language mutates the editor because `CodeBlockNodeView` does not guard `updateAttributes` with `editor.isEditable`. Hide or disable that selector for read-only editors so study content remains read-only.</violation>
</file>

<file name="src/features/workspaces/documents/document-html-chunk.ts">

<violation number="1" location="src/features/workspaces/documents/document-html-chunk.ts:42">
P2: When `maxBlocks` is zero or negative, `readDocumentHtmlChunk` returns an empty ready chunk whose `nextOffset` repeats the current offset. Validate that `maxBlocks` is a positive integer and reject invalid values before entering the loop.</violation>
</file>

<file name="src/features/workspaces/components/study/StudyProgressStrip.tsx">

<violation number="1" location="src/features/workspaces/components/study/StudyProgressStrip.tsx:49">
P2: Keyboard users cannot tab to another progress segment because every non-current button has `tabIndex={-1}`, but the strip does not implement roving arrow-key focus. Make all segment buttons tabbable with a visible focus style, or move focus to the newly selected button when navigating with the keyboard.</violation>
</file>

<file name="eval/datasets/workspace-tools.cases.ts">

<violation number="1" location="eval/datasets/workspace-tools.cases.ts:41">
P2: The case name claims 'without leaking the answer key into the stems', but the eval cannot enforce that. The harness stubs tool execution and grades only tool selection and schema validity (`scoreExpectedTools`/`scoreNoForbiddenTools`/`scoreToolInputsValid`), so this case passes as long as the model calls `workspace_create_items` once and avoids `workspace_delete_items` — it never checks that 3 questions were created or that the correct answer isn't embedded among the stems. The other cases whose concern can't be verified deterministically (read-only denial, general question) set `qualityRubric`; this one has none. Either rename the case to what it actually measures (e.g. 'creates a quiz and does not delete items') or add a `qualityRubric` that grades the answer for the 3-question count and no answer-key leakage.</violation>
</file>

<file name="src/features/workspaces/export/workspace-document-pdf-html.ts">

<violation number="1" location="src/features/workspaces/export/workspace-document-pdf-html.ts:119">
P2: When a Mermaid block is the only content inside a block container, this removes only `<pre>` and leaves the empty parent visible in the PDF. Prune empty ancestors after removal, or remove the Mermaid node before HTML serialization so the diagram leaves no layout artifact.</violation>
</file>

<file name="src/features/workspaces/quizzes/quiz-content.ts">

<violation number="1" location="src/features/workspaces/quizzes/quiz-content.ts:144">
P2: A `replace_text` edit can make one option exactly duplicate another, but `parseQuizSetContent` accepts the resulting question because it never enforces the distinct-option invariant. The quiz then stores indistinguishable choices; enforce `assertDistinctOptions` for every edited question before persistence and reject duplicate stored content at this boundary.</violation>
</file>

<file name="src/components/code-block/mermaid-diagram.tsx">

<violation number="1" location="src/components/code-block/mermaid-diagram.tsx:208">
P2: When a cached SVG fails `<img>` decoding, the cache retains the broken image and later remounts skip Mermaid rendering because `cachedImage` is present. Remove the matching cache entry when `onImageError` fires so the next mount can retry and show the normal error fallback.</violation>
</file>

<file name="src/features/workspaces/operations/document-citations.ts">

<violation number="1" location="src/features/workspaces/operations/document-citations.ts:39">
P2: When the model emits a valid-looking but nonexistent card or question ID, this line treats the address as resolved and persists a broken citation. Verify the unit exists in the item's content before adding its location, or leave the citation unresolved so `parseDocumentAiHtml` removes it.</violation>
</file>

<file name="src/features/workspaces/content/workspace-content-reader.ts">

<violation number="1" location="src/features/workspaces/content/workspace-content-reader.ts:117">
P2: When a file read hits the batch budget, this failure omits `type: "file"`, so `recordWorkspaceFileReadOutcomes` drops the event. Preserve the file type on budget failures so file-read observability continues to capture them.</violation>

<violation number="2" location="src/features/workspaces/content/workspace-content-reader.ts:135">
P2: An explicit `entries` read can return more than the 200,000-character batch ceiling because this code accounts for the current result only after reading it. Reject an oversized result before adding it to `results` and `readyResults`, while preserving `read_budget_exceeded` for subsequent requests.</violation>

<violation number="3" location="src/features/workspaces/content/workspace-content-reader.ts:506">
P2: Document `entries` reads do not enforce the documented 20-entry maximum, so a range such as `1-100` can return an unexpectedly large response. Reject contiguous ranges containing more than 20 blocks before calling `readHtmlChunk`.</violation>
</file>

<file name="src/features/workspaces/content/workspace-content-contract.ts">

<violation number="1" location="src/features/workspaces/content/workspace-content-contract.ts:152">
P2: Malformed quiz results with zero or multiple correct options pass this output contract despite the documented invariant. Enforce that exactly one option has `correct: true` so invalid quiz data is rejected at the boundary.</violation>
</file>

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

Re-trigger cubic

} from "#/features/workspaces/documents/code-block-shiki/highlighter";

export function CodeBlockNodeView({ node, updateAttributes }: ReactNodeViewProps) {
export function CodeBlockNodeView({

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: Custom agent: Flag AI Slop and Fabricated Changes

The PR adds mermaid diagram rendering in documents, flashcards, and quizzes, but the new document rendering path has no automated tests. Rule 1 requires behavior changes to be exercised by tests when a regression-style assertion is practical. Add a test that verifies a mermaid codeBlock renders <MermaidDiagram> (or the .workspace-document-diagram wrapper) instead of the usual <NodeViewWrapper className="workspace-document-code-block"> path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/code-block-shiki/CodeBlockNodeView.tsx, line 25:

<comment>The PR adds mermaid diagram rendering in documents, flashcards, and quizzes, but the new document rendering path has no automated tests. Rule 1 requires behavior changes to be exercised by tests when a regression-style assertion is practical. Add a test that verifies a mermaid `codeBlock` renders `<MermaidDiagram>` (or the `.workspace-document-diagram` wrapper) instead of the usual `<NodeViewWrapper className="workspace-document-code-block">` path.</comment>

<file context>
@@ -13,18 +13,56 @@ import {
 } from "#/features/workspaces/documents/code-block-shiki/highlighter";
 
-export function CodeBlockNodeView({ node, updateAttributes }: ReactNodeViewProps) {
+export function CodeBlockNodeView({
+	editor,
+	getPos,
</file context>

itemId: workspaceLocationItemIdSchema,
kind: z.literal("flashcard"),
cardId: z.uuid(),
cardId: workspaceEntryIdSchema,

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: The shared entry schema allows flashcard locations to carry q_ IDs and quiz locations to carry c_ IDs. Enforce the c_ prefix for flashcards and q_ for quiz questions while retaining UUID support for legacy content, so malformed persisted content and cross-type citations cannot validate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/locations/workspace-location.ts, line 52:

<comment>The shared entry schema allows flashcard locations to carry `q_` IDs and quiz locations to carry `c_` IDs. Enforce the `c_` prefix for flashcards and `q_` for quiz questions while retaining UUID support for legacy content, so malformed persisted content and cross-type citations cannot validate.</comment>

<file context>
@@ -38,135 +49,103 @@ export const workspaceLocationSchema = z.discriminatedUnion("kind", [
 		itemId: workspaceLocationItemIdSchema,
 		kind: z.literal("flashcard"),
-		cardId: z.uuid(),
+		cardId: workspaceEntryIdSchema,
+		version: z.literal(1),
+	}),
</file context>

Comment thread src/features/workspaces/operations/read-items.ts Outdated
content: content as unknown as JSONContent,
editable: false,
immediatelyRender: false,
extensions: getTiptapDocumentBaseExtensions(),

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 study content still exposes the code-block language selector, and selecting a language mutates the editor because CodeBlockNodeView does not guard updateAttributes with editor.isEditable. Hide or disable that selector for read-only editors so study content remains read-only.

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

<comment>Read-only study content still exposes the code-block language selector, and selecting a language mutates the editor because `CodeBlockNodeView` does not guard `updateAttributes` with `editor.isEditable`. Hide or disable that selector for read-only editors so study content remains read-only.</comment>

<file context>
@@ -0,0 +1,36 @@
+			content: content as unknown as JSONContent,
+			editable: false,
+			immediatelyRender: false,
+			extensions: getTiptapDocumentBaseExtensions(),
+			editorProps: {
+				attributes: { class: cn("workspace-document-prose outline-none", proseClassName) },
</file context>

let characters = 0;
let endOffset = offset;
while (endOffset < document.childCount) {
if (maxBlocks !== undefined && endOffset - offset >= maxBlocks) break;

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: When maxBlocks is zero or negative, readDocumentHtmlChunk returns an empty ready chunk whose nextOffset repeats the current offset. Validate that maxBlocks is a positive integer and reject invalid values before entering the loop.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/document-html-chunk.ts, line 42:

<comment>When `maxBlocks` is zero or negative, `readDocumentHtmlChunk` returns an empty ready chunk whose `nextOffset` repeats the current offset. Validate that `maxBlocks` is a positive integer and reject invalid values before entering the loop.</comment>

<file context>
@@ -38,6 +39,7 @@ export async function readDocumentHtmlChunk(
 	let characters = 0;
 	let endOffset = offset;
 	while (endOffset < document.childCount) {
+		if (maxBlocks !== undefined && endOffset - offset >= maxBlocks) break;
 		const block = await serializeTiptapNodeToAiHtml(document.child(endOffset));
 		const separatorCharacters = content.length > 0 ? 1 : 0;
</file context>
Suggested change
if (maxBlocks !== undefined && endOffset - offset >= maxBlocks) break;
if (maxBlocks !== undefined && (!Number.isInteger(maxBlocks) || maxBlocks <= 0)) {
return undefined;
}
if (maxBlocks !== undefined && endOffset - offset >= maxBlocks) break;

Comment thread src/features/workspaces/operations/workspace-tool-schemas.ts
if (input.expectedRevision && input.expectedRevision !== revision) {
return { status: "content_changed" };
}
const { document } = await this.getReferencedDocumentSnapshot();

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: readHtmlChunk no longer uses the snapshot revision, but getReferencedDocumentSnapshot() still computes and returns stateVector for every read. Remove that field and its Y.encodeStateVector computation to avoid dead work on each document read.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/document-session.ts, line 357:

<comment>`readHtmlChunk` no longer uses the snapshot revision, but `getReferencedDocumentSnapshot()` still computes and returns `stateVector` for every read. Remove that field and its `Y.encodeStateVector` computation to avoid dead work on each document read.</comment>

<file context>
@@ -354,14 +354,10 @@ export class DocumentSession extends YServer {
-		if (input.expectedRevision && input.expectedRevision !== revision) {
-			return { status: "content_changed" };
-		}
+		const { document } = await this.getReferencedDocumentSnapshot();
 
-		const chunk = await readDocumentHtmlChunk(document, input.offset);
</file context>


type QuizViewerData = Awaited<ReturnType<typeof getQuizViewerFn>>;

export function quizViewerQueryOptions(input: {

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: This module duplicates the complete study-query lifecycle from flashcard-queries.ts, so fixes to cancellation, optimistic rollback, or cache-version handling must be made twice. Extract a shared study-cache helper and pass the item-specific query and state functions into it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/quizzes/quiz-queries.ts, line 16:

<comment>This module duplicates the complete study-query lifecycle from `flashcard-queries.ts`, so fixes to cancellation, optimistic rollback, or cache-version handling must be made twice. Extract a shared study-cache helper and pass the item-specific query and state functions into it.</comment>

<file context>
@@ -0,0 +1,110 @@
+
+type QuizViewerData = Awaited<ReturnType<typeof getQuizViewerFn>>;
+
+export function quizViewerQueryOptions(input: {
+	itemId: string;
+	updatedAt: string;
</file context>

} from "#/features/workspaces/persistence/workspace-postgres-support";
import { notifyWorkspaceRoom } from "#/features/workspaces/realtime/workspace-room-notifier";

export async function readQuizSet(input: { itemId: string; workspaceId: string }) {

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: This file duplicates the complete structured-item persistence flow from flashcard-persistence.ts, so future fixes to locking, revisions, or notifications can diverge between flashcards and quizzes. Extract the shared transaction and notification logic into a generic structured-item persistence helper, leaving only type-specific parsing and serialization in each module.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/quizzes/quiz-persistence.ts, line 18:

<comment>This file duplicates the complete structured-item persistence flow from `flashcard-persistence.ts`, so future fixes to locking, revisions, or notifications can diverge between flashcards and quizzes. Extract the shared transaction and notification logic into a generic structured-item persistence helper, leaving only type-specific parsing and serialization in each module.</comment>

<file context>
@@ -0,0 +1,96 @@
+} from "#/features/workspaces/persistence/workspace-postgres-support";
+import { notifyWorkspaceRoom } from "#/features/workspaces/realtime/workspace-room-notifier";
+
+export async function readQuizSet(input: { itemId: string; workspaceId: string }) {
+	return await withDb(async (db) => {
+		const [row] = await db
</file context>

event.stopPropagation();
onSend();
}}
onKeyDown={(event) => event.stopPropagation()}

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: The unconditional onKeyDown={(event) => event.stopPropagation()} on StudyAiActionButton swallows every key press from the Hint/Explain button, so when it holds focus the parent <section>'s ArrowLeft/ArrowRight navigation (QuizViewer.tsx:305 and FlashcardViewer.tsx:460) stops working. This is inconsistent with StudyNavButton, which intentionally lets arrows bubble to the same section handler, and it breaks the arrow-key shortcuts advertised in the nav button title. Drop the onKeyDown handler — onClick already stops the click from reaching the flashcard flip handler, and the flip-on-space path is separately guarded by closest("button, a") — so arrow navigation behaves identically regardless of which control has focus.

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

<comment>The unconditional `onKeyDown={(event) => event.stopPropagation()}` on StudyAiActionButton swallows every key press from the Hint/Explain button, so when it holds focus the parent `<section>`'s ArrowLeft/ArrowRight navigation (QuizViewer.tsx:305 and FlashcardViewer.tsx:460) stops working. This is inconsistent with StudyNavButton, which intentionally lets arrows bubble to the same section handler, and it breaks the arrow-key shortcuts advertised in the nav button title. Drop the onKeyDown handler — `onClick` already stops the click from reaching the flashcard flip handler, and the flip-on-space path is separately guarded by `closest("button, a")` — so arrow navigation behaves identically regardless of which control has focus.</comment>

<file context>
@@ -0,0 +1,66 @@
+				event.stopPropagation();
+				onSend();
+			}}
+			onKeyDown={(event) => event.stopPropagation()}
+		>
+			<Lightbulb />
</file context>

@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: 4164e7e571

ℹ️ 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 +135 to +138
returnedCharacters +=
"content" in read
? read.content.length
: JSON.stringify("cards" in read ? read.cards : read.questions).length;

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 Enforce the read limit before returning each result

When a request selects a large payload—such as 20 text-heavy PDF pages or quiz entries—the code adds its size only after the pre-read budget check and returns it unconditionally. A single result can therefore exceed the intended 200,000-character ceiling by an arbitrary amount and overflow the model context; calculate the prospective total here and return read_budget_exceeded instead of appending an over-budget result.

Useful? React with 👍 / 👎.

Comment on lines +53 to 54
const location = resolveAddress(citationProps.node?.properties?.ref);
return location ? <WorkspaceCitation location={location} /> : null;

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 Preserve resolution of citations from existing chat history

When rendering any assistant message saved before this upgrade, its citation refs use the wr_XXXXXXXX format and the message's hidden data-workspace-references part contains the durable location. resolveAddress accepts only the new eight-character item addresses, while this change removes the code that read those persisted records, so every historical citation now resolves to undefined and disappears from the transcript despite its target still being available.

Useful? React with 👍 / 👎.

@urjitc

urjitc commented Aug 15, 2026

Copy link
Copy Markdown
Member Author

@greptile-apps review

urjitc added 4 commits August 15, 2026 15:29
Both viewers key the current entry's subtree so navigation remounts it,
and a remount that held the focused element drops focus to <body>, dead
to the section's keydown handler. The section now reclaims focus when a
navigation strands it there.
The node view assumed its host was the editable document editor; study
viewers now render it read-only, where the language dropdown is editing
chrome with nothing to edit.
getWorkspaceItemByRefKey read the whole workspace page per call, and
both callers called it in a loop — N citations or ref reads cost N
serial page reads. It is now getWorkspaceItemRefKeyIndex: one page
read, however many keys the operation resolves.
The prose listed the union's members and went stale the moment quizzes
joined; the schema already names every type, so the description no
longer repeats them.

@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).

You’re at about 93% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

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/documents/code-block-shiki/CodeBlockNodeView.tsx">

<violation number="1" location="src/features/workspaces/documents/code-block-shiki/CodeBlockNodeView.tsx:104">
P2: When `language` is null, the read-only branch displays `Code` instead of the existing `Plain text` label. Preserve the plain-text fallback and label styling in this branch so read-only and editable code blocks remain consistent.</violation>
</file>

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

Re-trigger cubic

</CodeBlockLanguageSelectorContent>
</CodeBlockLanguageSelector>
) : (
getCodeLanguageLabel(language)

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: When language is null, the read-only branch displays Code instead of the existing Plain text label. Preserve the plain-text fallback and label styling in this branch so read-only and editable code blocks remain consistent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/code-block-shiki/CodeBlockNodeView.tsx, line 104:

<comment>When `language` is null, the read-only branch displays `Code` instead of the existing `Plain text` label. Preserve the plain-text fallback and label styling in this branch so read-only and editable code blocks remain consistent.</comment>

<file context>
@@ -71,32 +71,38 @@ export function CodeBlockNodeView({
+							</CodeBlockLanguageSelectorContent>
+						</CodeBlockLanguageSelector>
+					) : (
+						getCodeLanguageLabel(language)
+					)}
 				</CodeBlockTitle>
</file context>
Suggested change
getCodeLanguageLabel(language)
<span className="truncate font-medium">{language ? getCodeLanguageLabel(language) : "Plain text"}</span>

…-split

# Conflicts:
#	src/features/workspaces/operations/__snapshots__/workspace-tool-surface.test.ts.snap
#	src/features/workspaces/operations/create-items.ts
#	src/features/workspaces/operations/workspace-tool-definitions.ts
#	src/features/workspaces/operations/workspace-tool-schemas.ts
@github-actions

Copy link
Copy Markdown

React Doctor found 3 new issues in 2 files · 3 warnings · score 85 / 100 (Great) · 1 fixed · vs main

3 warnings

src/features/workspaces/components/flashcards/FlashcardViewer.tsx

  • ⚠️ L186 useMemo before an early return rerender-memo-before-early-return

src/features/workspaces/components/quizzes/QuizViewer.tsx

  • ⚠️ L85 Large component is hard to read and change no-giant-component
  • ⚠️ L169 useMemo before an early return rerender-memo-before-early-return

Reviewed by React Doctor for commit c9f217a. See inline comments for fixes.

@@ -178,9 +185,17 @@ function FlashcardStudySession({
}, [resetProgress, startSession]);
const toolbar = useMemo(

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/rerender-memo-before-early-return (warning)

This rebuilds the JSX whenever its dependencies change even on renders that take the early return, so move the JSX into a child component rendered after the early return to skip it

Fix → Move the JSX into a child component rendered after the early return, so renders that take the early return never build it

Docs

);
}

function QuizStudySession({

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/no-giant-component (warning)

Component "QuizStudySession" is over 300 lines long, which is hard to read & change. Split it into a few smaller components.

Fix → Pull each section into its own component so the parent is easier to read, test, and change.

Docs

startSession("all", false);
resetProgress();
}, [resetProgress, startSession]);
const toolbar = useMemo(

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/rerender-memo-before-early-return (warning)

This rebuilds the JSX whenever its dependencies change even on renders that take the early return, so move the JSX into a child component rendered after the early return to skip it

Fix → Move the JSX into a child component rendered after the early return, so renders that take the early return never build it

Docs

@urjitc
urjitc merged commit a4b3502 into main Aug 15, 2026
11 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Dev Board Aug 15, 2026
@urjitc
urjitc deleted the review/quiz-addresses-split branch August 15, 2026 19:44
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.

1 participant