Skip to content

feat(editor): query workbench with live results and TanStack Query#66

Merged
popemkt merged 7 commits intomainfrom
emdash/query-claude-3w1
Mar 12, 2026
Merged

feat(editor): query workbench with live results and TanStack Query#66
popemkt merged 7 commits intomainfrom
emdash/query-claude-3w1

Conversation

@popemkt
Copy link
Copy Markdown
Owner

@popemkt popemkt commented Mar 12, 2026

Summary

  • Render live query results inline in the outline for nodes with #Query supertag
  • Add inline query summary (human-readable description via QueryLinter)
  • Add "Configure" button that opens the QueryBuilder inline (like Tana's query workbench)
  • Replace useState/useEffect with TanStack Query for cached, auto-invalidating results
  • Invalidate query caches after all outline mutations (create, delete, update, indent, outdent, move)
  • Validate query definitions with Zod schema at server boundary
  • Add updateQueryDefinitionServerFn for persisting query edits
  • Extract query helpers with 24 unit tests
  • Full database seeding via ensure-seeded.server.ts

Test plan

  • 60 unit tests pass (24 new for query helpers)
  • Manual: query nodes show MagnifyingGlass bullet, inline linter summary, and "Configure" button
  • Manual: clicking Configure opens QueryBuilder inline; editing filters persists and refreshes results
  • Manual: creating/deleting/editing nodes causes query results to auto-refresh
  • Manual: result rows are keyboard-accessible (Tab + Enter/Space)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added query node support with magnifying glass icon indicator
    • Query results now display inline with clickable navigation to referenced nodes
    • Introduced query configuration interface for editing and linting queries
    • Results include color-coded supertag badges for quick identification

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 12, 2026

Warning

Rate limit exceeded

@popemkt has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 20 minutes and 26 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3fa9de8d-8a9c-4767-903d-473e45cf6ca5

📥 Commits

Reviewing files that changed from the base of the PR and between a7d19be and 4882a51.

📒 Files selected for processing (1)
  • apps/nxus-editor/src/components/outline/node-block.tsx
📝 Walkthrough

Walkthrough

Adds full query-node support: UI detection and iconography, an inline QueryResults component with server-backed evaluation and edit workbench, new query helpers and tests, seeded DB init for evaluations, cache invalidation wiring, and a new workbench dependency.

Changes

Cohort / File(s) Summary
Bullet Icon
apps/nxus-editor/src/components/outline/bullet.tsx
Adds isQuery?: boolean prop and renders MagnifyingGlass for query nodes; imports icon.
Outline UI — Node & Results
apps/nxus-editor/src/components/outline/node-block.tsx, apps/nxus-editor/src/components/outline/query-results.tsx
Detects query nodes, extracts/filters query definition/fields, threads isQuery to children, adds Configure/workbench toggle, and renders new QueryResults between fields and children.
Query Helpers & Tests
apps/nxus-editor/src/components/outline/query-helpers.ts, apps/nxus-editor/src/components/outline/query-helpers.test.ts
Adds stable query keys, safe stringify, query/node detection, definition extraction, visible-field filtering, and unit tests covering these utilities.
Server: Evaluate & Persist Query
apps/nxus-editor/src/services/outline.server.ts, apps/nxus-editor/src/services/ensure-seeded.server.ts
Adds evaluateQueryServerFn and updateQueryDefinitionServerFn, replaces bootstrap init calls with initDatabaseSeeded(), and provides seeded DB registration helper.
Types
apps/nxus-editor/src/types/outline.ts
Exports QUERY_SYSTEM_ID and QUERY_FIELD_SYSTEM_IDS to identify query supertag and hide internal query fields.
Cache Invalidation & Hooks
apps/nxus-editor/src/hooks/use-outline-sync.ts
Introduces React Query client usage and calls to invalidateQueries (outlineQueryKeys.all) after mutations to refresh query/evaluation caches.
Workspace Dependency
apps/nxus-editor/package.json
Adds @nxus/workbench to dependencies (new workspace dependency).

Sequence Diagram

sequenceDiagram
    participant NodeBlock as NodeBlock (Client)
    participant QueryResults as QueryResults (Component)
    participant Server as evaluateQueryServerFn (Server)
    participant DB as NodeFacade/DB (Data)

    NodeBlock->>NodeBlock: detect isQuery via QUERY_SYSTEM_ID\nextract queryDefinition & visibleFields
    NodeBlock->>QueryResults: render with nodeId, definition & depth
    QueryResults->>QueryResults: compute stable key via safeStringify
    QueryResults->>Server: POST evaluateQueryServerFn(definition)
    Server->>DB: initDatabaseSeeded() -> init & seed\nevaluate query via node facade
    DB-->>Server: return matching nodes + totalCount
    Server-->>QueryResults: return results (id, content, supertags, totalCount)
    QueryResults->>QueryResults: render QueryResultRow list\nshow loading/empty/error/truncation states
    QueryResultRow->>NodeBlock: navigate to node on click
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐇 I sniffed the query, pressed the glassy lens,
Rows hopped out tidy, each with tagged amends.
Badges as carrots, results in a line,
I wiggled my nose and saved the design.
— The Query Rabbit 🔍

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature: adding a query workbench with live results powered by TanStack Query for query nodes in the editor outline.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch emdash/query-claude-3w1
📝 Coding Plan for PR comments
  • Generate coding plan

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 and usage tips.

devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

…idation

- Replace useState/useEffect with useQuery for query result caching
- Add inline QueryLinter summary + "Configure" button on query nodes
- Embed QueryBuilder from @nxus/workbench for inline query editing
- Add updateQueryDefinitionServerFn to persist definition changes
- Add queryClient.invalidateQueries after all outline mutations
- Extract query helpers (isQueryNode, extractQueryDefinition, etc.)
- Add 24 unit tests for query helper functions
- Add ensure-seeded.server.ts for full database seeding

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 7 additional findings in Devin Review.

Open in Devin Review

coderabbitai[bot]

This comment was marked as resolved.

- Replace z.any() with QueryDefinitionSchema for evaluateQueryServerFn
  and updateQueryDefinitionServerFn input validation
- Change QueryResultRow from div to button for keyboard accessibility
- Use span instead of div inside button to avoid invalid nesting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 8 additional findings in Devin Review.

Open in Devin Review

Comment on lines +54 to +67
const handleDefinitionChange = useCallback(
(newDef: QueryDefinition) => {
updateQueryDefinitionServerFn({
data: { nodeId, definition: newDef },
})
.then(() => {
queryClient.invalidateQueries({ queryKey: outlineQueryKeys.all })
})
.catch((err) => {
console.error('[query-workbench] Failed to save definition:', err)
})
},
[nodeId, queryClient],
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Query definition change never updates local state, making inline editing non-functional

handleDefinitionChange in query-results.tsx:54-67 calls updateQueryDefinitionServerFn to persist the new definition to the DB and then invalidates TanStack Query caches. However, it never updates the node's fields in the local Zustand store. The definition prop passed to QueryResults originates from extractQueryDefinition(node) in the parent NodeBlock (node-block.tsx:215), which reads from the Zustand store — so the prop remains stale. Because definitionKey at line 37 is derived from this stale definition, the useQuery cache key doesn't change, and the invalidation merely re-fetches the old query (the queryFn closure at line 46 also captures the old definition). The net effect is that editing a query via the QueryBuilder appears to do nothing until the page is fully reloaded.

Prompt for agents
In apps/nxus-editor/src/components/outline/query-results.tsx, the handleDefinitionChange callback (lines 54-67) needs to also update the node's query_definition field in the local Zustand store so that the definition prop flowing from the parent NodeBlock updates, which in turn changes the TanStack Query cache key and triggers a re-fetch with the new definition.

One approach: import useOutlineStore and call something like useOutlineStore.getState().updateNodeField(nodeId, 'field:query_definition', newDef) inside handleDefinitionChange before or after the server call. If no such store action exists, create one in the outline store that updates the field value for a given node. Alternatively, use optimistic updates with TanStack Query's mutation API so the local query data updates immediately.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
apps/nxus-editor/src/components/outline/query-results.tsx (1)

37-47: ⚠️ Potential issue | 🟠 Major

Normalize definition before gating evaluation and the workbench.

Line 38 still treats '{}' as “no definition”, and the later isQueryDefinition(definition) checks reject partial objects. That blocks default/partial query definitions from both evaluating and opening the inline linter/builder even though the server endpoint already normalizes them with QueryDefinitionSchema.

As per coding guidelines: "Use Zod schemas with parse() for validating external data instead of assuming validity" and "Validate external inputs using Zod schemas before processing"

Also applies to: 78-109

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/nxus-editor/src/components/outline/query-results.tsx` around lines 37 -
47, Normalize and validate the incoming definition using the
QueryDefinitionSchema before any gating or evaluation: call
QueryDefinitionSchema.parse (or safe parse) on definition to produce
parsedDefinition, use parsedDefinition (not the raw safeStringify(definition) or
'{}' checks) for hasDefinition and as the payload to evaluateQueryServerFn and
outlineQueryKeys.evaluation, and replace subsequent
isQueryDefinition(definition) checks with checks against the parsedDefinition so
partial/default definitions that the server accepts are allowed through.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/nxus-editor/src/components/outline/query-results.tsx`:
- Around line 191-200: The button lacks a reliable accessible name when
node.content is empty (it currently relies on title and a zero‑width space);
update the button in the component rendering (the <button> using props like
node.content, depth, onClick) to provide an explicit accessible name by adding
an aria-label such as aria-label={`Go to: ${node.content || 'Untitled result'}`}
(or another clear fallback like "Untitled result") so assistive tech gets a
stable label even when node.content is empty or there are no supertags.
- Around line 54-65: The auto-save handler handleDefinitionChange is calling
updateQueryDefinitionServerFn directly from QueryBuilder onChange (with
showSave={false}), causing concurrent saves to finish out-of-order and overwrite
newer edits; wrap the save logic in a debounce (e.g. 300–800ms) or implement a
simple queued/latest-only save: keep a local incremental requestId/token for
each save, cancel/replace pending debounce timers, call
updateQueryDefinitionServerFn only for the debounced/latest change, and when
each promise resolves check the requestId to ignore stale responses before
calling queryClient.invalidateQueries({ queryKey: outlineQueryKeys.all }). Apply
the same debounce/latest-only pattern to the other save handler referenced in
the diff (the similar save at 108-114).

---

Duplicate comments:
In `@apps/nxus-editor/src/components/outline/query-results.tsx`:
- Around line 37-47: Normalize and validate the incoming definition using the
QueryDefinitionSchema before any gating or evaluation: call
QueryDefinitionSchema.parse (or safe parse) on definition to produce
parsedDefinition, use parsedDefinition (not the raw safeStringify(definition) or
'{}' checks) for hasDefinition and as the payload to evaluateQueryServerFn and
outlineQueryKeys.evaluation, and replace subsequent
isQueryDefinition(definition) checks with checks against the parsedDefinition so
partial/default definitions that the server accepts are allowed through.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d8f0aeba-3cf4-4a2e-9340-d64409858e81

📥 Commits

Reviewing files that changed from the base of the PR and between e39d74c and 0348326.

📒 Files selected for processing (2)
  • apps/nxus-editor/src/components/outline/query-results.tsx
  • apps/nxus-editor/src/services/outline.server.ts

Comment on lines +54 to +65
const handleDefinitionChange = useCallback(
(newDef: QueryDefinition) => {
updateQueryDefinitionServerFn({
data: { nodeId, definition: newDef },
})
.then(() => {
queryClient.invalidateQueries({ queryKey: outlineQueryKeys.all })
})
.catch((err) => {
console.error('[query-workbench] Failed to save definition:', err)
})
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Serialize the auto-saves from QueryBuilder.

onChange is wired straight to updateQueryDefinitionServerFn, and showSave={false} means every edit can trigger a full-definition write. Two quick edits can finish out of order, so an older request can overwrite the newer definition and then invalidate the outline back to stale data. Debounce or queue these saves so only the latest edit persists.

Also applies to: 108-114

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/nxus-editor/src/components/outline/query-results.tsx` around lines 54 -
65, The auto-save handler handleDefinitionChange is calling
updateQueryDefinitionServerFn directly from QueryBuilder onChange (with
showSave={false}), causing concurrent saves to finish out-of-order and overwrite
newer edits; wrap the save logic in a debounce (e.g. 300–800ms) or implement a
simple queued/latest-only save: keep a local incremental requestId/token for
each save, cancel/replace pending debounce timers, call
updateQueryDefinitionServerFn only for the debounced/latest change, and when
each promise resolves check the requestId to ignore stale responses before
calling queryClient.invalidateQueries({ queryKey: outlineQueryKeys.all }). Apply
the same debounce/latest-only pattern to the other save handler referenced in
the diff (the similar save at 108-114).

Comment on lines +191 to +200
<button
type="button"
className={cn(
'flex w-full items-start rounded-sm cursor-pointer text-left',
'hover:bg-foreground/[0.03] transition-colors duration-75',
)}
style={{ paddingLeft: `${(depth + 1) * 24}px` }}
onClick={onClick}
title={`Go to: ${node.content || 'Untitled'}`}
>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add an accessible name for untitled result buttons.

When node.content is empty and there are no supertags, this button's visible text is just a zero-width space. title is not a dependable accessible name, so assistive tech can end up with an unlabeled button here.

♿ Small fix
     <button
       type="button"
+      aria-label={`Go to: ${node.content || 'Untitled'}`}
       className={cn(

Also applies to: 228-228

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/nxus-editor/src/components/outline/query-results.tsx` around lines 191 -
200, The button lacks a reliable accessible name when node.content is empty (it
currently relies on title and a zero‑width space); update the button in the
component rendering (the <button> using props like node.content, depth, onClick)
to provide an explicit accessible name by adding an aria-label such as
aria-label={`Go to: ${node.content || 'Untitled result'}`} (or another clear
fallback like "Untitled result") so assistive tech gets a stable label even when
node.content is empty or there are no supertags.

@popemkt popemkt changed the title feat(editor): render live query results in outline feat(editor): query workbench with live results and TanStack Query Mar 12, 2026
popemkt and others added 2 commits March 12, 2026 23:02
…bench

- Configure button renders as a badge (same h-6 + text-[11px] as supertag
  badges) next to the node content text, only when query is expanded
- QueryLinter now lives inside the QueryBuilder workbench panel (showLinter)
- workbenchOpen state owned by NodeBlock, passed down to QueryResults

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add afterContent slot to NodeContent so the Configure badge renders
right after the editable text, before the supertag badges:
  ● My Query [Configure] #Query

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/nxus-editor/src/components/outline/node-block.tsx (1)

206-220: ⚠️ Potential issue | 🔴 Critical

Critical: useState called after early return violates Rules of Hooks.

The useState(false) at line 212 is called after the conditional return null at line 206. React requires hooks to be called unconditionally and in the same order on every render. When node is undefined, the component returns early and useState is never called, but on the next render when node exists, it will be called—causing hook order mismatch and runtime bugs.

Move workbenchOpen state declaration before the early return, alongside other hooks.

🐛 Proposed fix
   const {
     updateNodeContent,
     createNodeAfter,
     deleteNode,
     indentNode,
     outdentNode,
     moveNodeUp,
     moveNodeDown,
   } = useOutlineSync()

+  const [workbenchOpen, setWorkbenchOpen] = useState(false)
+
   const handleBulletClick = useCallback(

Then remove line 212:

   if (!node) return null

   const isActive = activeNodeId === nodeId
   const isSelected = selectedNodeId === nodeId
   const hasChildren = node.children.length > 0
   const primaryTagColor = node.supertags[0]?.color ?? null
-  const [workbenchOpen, setWorkbenchOpen] = useState(false)
   const isSupertag = node.supertags.some((t) => t.systemId === SUPERTAG_DEFINITION_SYSTEM_ID)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/nxus-editor/src/components/outline/node-block.tsx` around lines 206 -
220, The useState hook for workbenchOpen is declared after an early return when
node is falsy, violating React Hooks rules; move the useState call
(workbenchOpen, setWorkbenchOpen via useState(false)) to before the "if (!node)
return null" check so hooks are invoked unconditionally and in stable order,
then remove the post-return declaration; ensure references to
workbenchOpen/setWorkbenchOpen elsewhere in the component (e.g., in
node-block.tsx) still point to the moved state.
🧹 Nitpick comments (1)
apps/nxus-editor/src/components/outline/query-results.tsx (1)

45-50: Consider validating definition before casting to QueryDefinition.

The definition prop is typed as unknown, but it's cast to QueryDefinition at line 47 without client-side validation. While the server validates via Zod, the client currently trusts the input blindly. If isQueryDefinition(definition) already performs validation (based on usage at line 75), consider using that check here or adding a guard.

Potential safeguard
   const {
     data,
     isLoading,
     error,
   } = useQuery({
     queryKey: outlineQueryKeys.evaluation(definitionKey),
-    queryFn: () => evaluateQueryServerFn({ data: { definition: definition as QueryDefinition } }),
-    enabled: hasDefinition,
+    queryFn: () => evaluateQueryServerFn({ data: { definition: definition as QueryDefinition } }),
+    enabled: hasDefinition && isQueryDefinition(definition),
     staleTime: 30_000,
   })

As per coding guidelines: "Use Zod schemas with parse() for validating external data instead of assuming validity".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/nxus-editor/src/components/outline/query-results.tsx` around lines 45 -
50, The code casts `definition` to `QueryDefinition` when calling
evaluateQueryServerFn inside useQuery; instead validate `definition` first (use
the existing isQueryDefinition(definition) guard or the Zod schema's parse())
and only call evaluateQueryServerFn with a properly typed value. Concretely,
compute a typedDefinition (e.g., if (isQueryDefinition(definition))
typedDefinition = definition; else try schema.parse(definition) or set
enabled=false), update the useQuery `enabled` to depend on that check, and pass
typedDefinition to evaluateQueryServerFn (reference: useQuery,
outlineQueryKeys.evaluation, evaluateQueryServerFn, isQueryDefinition).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/nxus-editor/src/components/outline/node-block.tsx`:
- Around line 206-220: The useState hook for workbenchOpen is declared after an
early return when node is falsy, violating React Hooks rules; move the useState
call (workbenchOpen, setWorkbenchOpen via useState(false)) to before the "if
(!node) return null" check so hooks are invoked unconditionally and in stable
order, then remove the post-return declaration; ensure references to
workbenchOpen/setWorkbenchOpen elsewhere in the component (e.g., in
node-block.tsx) still point to the moved state.

---

Nitpick comments:
In `@apps/nxus-editor/src/components/outline/query-results.tsx`:
- Around line 45-50: The code casts `definition` to `QueryDefinition` when
calling evaluateQueryServerFn inside useQuery; instead validate `definition`
first (use the existing isQueryDefinition(definition) guard or the Zod schema's
parse()) and only call evaluateQueryServerFn with a properly typed value.
Concretely, compute a typedDefinition (e.g., if (isQueryDefinition(definition))
typedDefinition = definition; else try schema.parse(definition) or set
enabled=false), update the useQuery `enabled` to depend on that check, and pass
typedDefinition to evaluateQueryServerFn (reference: useQuery,
outlineQueryKeys.evaluation, evaluateQueryServerFn, isQueryDefinition).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b9d1177b-5ff3-48b0-a3ee-805bc3583697

📥 Commits

Reviewing files that changed from the base of the PR and between 0348326 and a7d19be.

📒 Files selected for processing (2)
  • apps/nxus-editor/src/components/outline/node-block.tsx
  • apps/nxus-editor/src/components/outline/query-results.tsx

popemkt and others added 2 commits March 12, 2026 23:08
20px square icon button (same height as supertag badge inner),
no text label, right after the content text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tent

Place the icon-only Configure button (h-6 w-6, Sliders icon) as a
sibling after NodeContent in the node row. Remove afterContent slot
from NodeContent — not needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 12 additional findings in Devin Review.

Open in Devin Review

const hasFields = node.fields.length > 0
const isExpandable = hasChildren || hasFields
const primaryTagColor = node.supertags[0]?.color ?? null
const [workbenchOpen, setWorkbenchOpen] = useState(false)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 useState hook called after conditional early return violates React Rules of Hooks

The useState(false) call at line 212 is placed after the if (!node) return null early return at line 206. This violates React's Rules of Hooks, which require hooks to be called in the same order on every render. If a node is deleted from the Zustand store while this NodeBlock component is still mounted, the component will re-render with node === undefined, hit the early return, and skip the useState call — causing React to throw "Rendered fewer hooks than expected". The same issue applies to the pre-existing useOutlineStore(useShallow(...)) at line 224, but the newly added useState at line 212 is the hook introduced by this PR.

Fix: move useState before the early return

Move const [workbenchOpen, setWorkbenchOpen] = useState(false) before line 206 (if (!node) return null), alongside the other hook calls (lines 24–47). The useOutlineStore(useShallow(...)) at line 224 should also be moved above the early return for correctness.

Prompt for agents
In apps/nxus-editor/src/components/outline/node-block.tsx, the useState hook at line 212 and the useOutlineStore(useShallow(...)) hook at lines 224-234 are both placed AFTER the conditional early return at line 206 (if (!node) return null). This violates React's Rules of Hooks.

To fix:
1. Move `const [workbenchOpen, setWorkbenchOpen] = useState(false)` from line 212 to somewhere before line 206, e.g. after line 47 (after the useOutlineSync() call) or alongside the other hook declarations around lines 24-36.
2. Also move the `sortedChildren` hook (lines 224-234) to before line 206, since it's also a hook call after a conditional return. This was pre-existing but should be fixed as well.
3. All non-hook computations that depend on `node` (lines 208-220) should remain after the early return guard.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@popemkt popemkt merged commit ed27e2e into main Mar 12, 2026
6 checks passed
@popemkt popemkt deleted the emdash/query-claude-3w1 branch March 12, 2026 16:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant