feat(console): add V2 workspace and goal stack surfaces - #78
Conversation
📝 WalkthroughWalkthroughAdds shared workspace and plan-state contracts, secure Console API routes, Workspace Substrate and Goal Stack surfaces, layout migration, readiness degradation reporting, and comprehensive unit and Playwright coverage. ChangesShared projection contracts
Server integration and validation
Workspace Substrate
Goal Stack
Readiness degradation
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant GoalStackView
participant PlanRoute
participant HarnessMCP
GoalStackView->>PlanRoute: Poll plan snapshot and events
PlanRoute->>HarnessMCP: Inspect plan and fetch changes
HarnessMCP-->>PlanRoute: Return normalized MCP data
PlanRoute-->>GoalStackView: Return snapshot, events, cursor, and capabilities
GoalStackView->>PlanRoute: Submit plan mutation
PlanRoute->>HarnessMCP: Dispatch approval or affordance action
HarnessMCP-->>PlanRoute: Return action result or refusal
PlanRoute-->>GoalStackView: Return success or refusal response
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
7cde5b5 to
e2e8a48
Compare
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
There was a problem hiding this comment.
Pull request overview
This PR expands the canonical V2 Console with two new primary surfaces—Workspace substrate and Goal Stack—backed by shared, framework-free projection modules in packages/theorem-acp, plus same-origin API routes that enforce identity/path constraints when talking to upstream GraphQL and Harness MCP.
Changes:
- Add shared “projection” modules for Workspace GraphQL and Harness Plan streams (
workspace-state,plan-state,plan-program) and export them from@commonplace/theorem-acp. - Introduce new Console surfaces: a virtualized Workspace substrate browser (with local-history diff) and a Goal Stack Plan canvas (React Flow + dagre + drag-and-drop capability queueing).
- Wire same-origin routes for
/api/workspace(operation allowlist + path allowlist) and/api/harness/plan(inspect/poll + mutate + save-as-program via Harness MCP), plus broaden tests/e2e coverage.
Reviewed changes
Copilot reviewed 41 out of 57 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Locks new Console dependencies (dnd-kit, Radix Popover) and related resolution changes. |
| packages/theorem-acp/src/workspace-state.ts | Adds framework-free Workspace GraphQL client + tree projection helpers. |
| packages/theorem-acp/src/plan-state.ts | Adds framework-free Plan polling normalization + event folding + subscription helper. |
| packages/theorem-acp/src/plan-program.ts | Adds conversion from plan snapshot to programmable graph definition. |
| packages/theorem-acp/package.json | Exposes new theorem-acp entrypoints (plan/workspace modules). |
| apps/console/src/views/workspace/WorkspaceSubstrateView.tsx | New Workspace substrate surface (tree, readiness, find, file history). |
| apps/console/src/views/workspace/WorkspaceHistoryDiff.tsx | CodeMirror Merge-based diff viewer for workspace file revisions. |
| apps/console/src/views/ThreadView.tsx | Displays “ask degraded” banner on assistant messages via message metadata. |
| apps/console/src/views/registry.tsx | Registers new Workspace and Goal Stack view descriptors in the Console registry. |
| apps/console/src/views/goal-stack/ToolPalette.tsx | Capability palette using cmdk + dnd-kit draggable affordances. |
| apps/console/src/views/goal-stack/ProgressEdge.tsx | Custom React Flow edge rendering progress/running state. |
| apps/console/src/views/goal-stack/PlanTaskNode.tsx | Plan task node renderer + dnd-kit drop target for affordance queueing. |
| apps/console/src/views/goal-stack/PlanPermissionPrompt.tsx | cmdk Dialog prompt for destructive/approval-required plan nodes. |
| apps/console/src/views/goal-stack/plan-layout.ts | dagre-based layout + edge progress/state projection. |
| apps/console/src/views/goal-stack/NodeInspector.tsx | Inspector panel for selected plan node + mutation actions. |
| apps/console/src/views/goal-stack/GoalStackView.tsx | Goal Stack surface: plan subscription, canvas rendering, DnD queueing, mutations. |
| apps/console/src/motion/motion.css | Adds edge-running keyframes + reduced-motion handling for Goal Stack edges. |
| apps/console/src/motion/motion-tokens.ts | Documents Goal Stack motion behavior in the interaction inventory. |
| apps/console/src/lib/workspace-state.test.ts | Unit tests for workspace projection + GraphQL query shape and readiness logic. |
| apps/console/src/lib/workspace-seed.ts | IA seed update: Workspace substrate tab + new Goal Stack surface in primary stripe. |
| apps/console/src/lib/workspace-route.test.ts | Tests operation allowlist and path allowlist enforcement for /api/workspace. |
| apps/console/src/lib/thread-store.ts | Adds readiness-based “ask degraded” labeling to assistant messages. |
| apps/console/src/lib/thread-store.test.ts | Adjusts tests to account for additional readiness fetch during send. |
| apps/console/src/lib/thread-readiness.test.ts | Tests askDegradation readiness-to-banner projection. |
| apps/console/src/lib/server/harness-mcp.ts | Adds server helper to call Harness MCP with identity-bound arguments. |
| apps/console/src/lib/plan-route.test.ts | Tests mapping of typed Plan refusals to HTTP 409 responses. |
| apps/console/src/lib/harness-mcp.test.ts | Tests identity-bound argument overwrite behavior. |
| apps/console/src/lib/harness-mcp-core.ts | Implements identityBoundArguments helper used by MCP calls. |
| apps/console/src/lib/goal-stack-contract.test.ts | Tests plan projection normalization + event folding + capability annotations. |
| apps/console/src/lib/console-host.ts | Migrates IA revision 3 seed: adds workspace substrate tab and updates stripe orders. |
| apps/console/src/lib/console-host.test.ts | Updates host tests for new Goal Stack surface and updated Workspace editor tabs. |
| apps/console/src/components/shell/SearchField.tsx | Tightens onOpenChange typing for cmdk dialog handler. |
| apps/console/src/components/shell/IntuiShell.tsx | Adds Goal Stack icon mapping + updates surface hotkey comment (Alt+1..6). |
| apps/console/src/components/ConsoleApp.tsx | Emits degradation metadata into assistant-ui message objects. |
| apps/console/src/app/api/workspace/route.ts | New same-origin GraphQL proxy with operation allowlist + path validation. |
| apps/console/src/app/api/harness/plan/route.ts | New Plan poll/mutation route backed by Harness MCP + “save as program”. |
| apps/console/package.json | Adds new deps and switches dev/build scripts to force webpack. |
| apps/console/e2e/workspace-goal-stack.spec.ts | Playwright coverage for Workspace substrate + Goal Stack + live-route smoke gating. |
| apps/console/e2e/console-ia.spec.ts | Updates IA e2e expectations for 6 surfaces + Workspace substrate seed behavior. |
| apps/console/e2e/cards.spec.ts | Updates surface nav count expectation (now 6). |
| apps/console/CLAUDE.md | Documents workspace import allowlist requirements + adds Goal Stack/Workspace substrate sourcing rows. |
| apps/console/AGENTS.md | Mirrors constitution updates (workspace allowlists + new sourcing rows). |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| onClick={() => { | ||
| window.localStorage.removeItem(PROJECT_STORAGE_KEY); | ||
| setProjectId(''); | ||
| setSurface(null); | ||
| setHistory(null); | ||
| setFindHits([]); | ||
| lastFindRef.current = null; | ||
| }} |
| for (const entry of content) { | ||
| const text = record(entry)?.text; | ||
| if (typeof text !== 'string') continue; | ||
| try { | ||
| const parsed = record(JSON.parse(text)); | ||
| if (parsed) return parsed; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| return null; |
| const edgePairs = tasks.flatMap((task) => [ | ||
| ...task.dependencies | ||
| .filter((source) => visible.has(source)) | ||
| .map((source) => ({ source, target: task.id, relation: 'dependency' as const })), | ||
| ...task.supersedes | ||
| .filter((source) => visible.has(source)) | ||
| .map((source) => ({ source, target: task.id, relation: 'supersedes' as const })), | ||
| ]); |
| edges: edgePairs.map((edge) => { | ||
| const task = tasks.find((candidate) => candidate.id === edge.target); | ||
| const state = edge.relation === 'supersedes' | ||
| ? 'superseded' | ||
| : edgeState(task?.status ?? 'pending'); | ||
| return { | ||
| id: `${edge.relation}:${edge.source}:${edge.target}`, | ||
| source: edge.source, | ||
| target: edge.target, | ||
| type: 'goalProgress', | ||
| data: { | ||
| progress: edgeProgress(edge.relation === 'supersedes' ? 'superseded' : task?.status ?? 'pending'), | ||
| state, | ||
| relation: edge.relation, | ||
| }, | ||
| }; | ||
| }), |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 98bc32cfd4
ℹ️ 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".
| children { | ||
| id kind name path excluded | ||
| children { id kind name path excluded } |
There was a problem hiding this comment.
Request descendants beyond the fourth tree level
The canonical query forwarded by this route stops selecting children after four nested directory levels. GraphQL therefore omits every deeper descendant before workspaceTreeRows receives the tree, so normal repository paths such as apps/console/src/views/workspace/... cannot be expanded or displayed in the new Workspace surface. The client defines the same bounded selection in packages/theorem-acp/src/workspace-state.ts, but this route replaces it with this canonical query, so increasing only the client selection would not fix the production request.
Useful? React with 👍 / 👎.
| ...task.supersedes | ||
| .filter((source) => visible.has(source)) | ||
| .map((source) => ({ source, target: task.id, relation: 'supersedes' as const })), |
There was a problem hiding this comment.
Render inverse supersession links in the plan DAG
When a Plan task reports its replacement using superseded_by, the renderer drops that relationship because it only produces supersession edges from task.supersedes. The normalizer preserves both fields, and the added end-to-end fixture itself supplies task-old-release.superseded_by: ['task-release']; consequently that prior generation appears as an isolated node rather than being connected to its successor. Add inverse edges from the current task to its supersededBy tasks (while respecting visible) so either canonical representation renders the lineage.
Useful? React with 👍 / 👎.
| function programNode(task: PlanTask, dependentIds: string[]): Record<string, unknown> { | ||
| const destructive = task.queuedAffordances.some((affordance) => affordance.annotations.destructive); | ||
| const base = { | ||
| id: task.id, | ||
| block_id: `block:plan-task:${task.alias}`, | ||
| contract: blockContract(`contract:plan-task:${task.alias}`, task.title, task.description, destructive), |
There was a problem hiding this comment.
Mark non-read-only affordances as side-effecting
For any ordinary write affordance annotated readOnly: false and destructive: false, this sets has_side_effects to false even though the affordance can mutate external state. The generated program explicitly uses approval: { mode: 'require_side_effects' }, so those writes will be materialized without the approval protection that mode is intended to enforce. Derive side effects from !annotations.readOnly rather than only from the destructive subset.
Useful? React with 👍 / 👎.
| return { | ||
| ...base, | ||
| kind: 'stochastic', | ||
| affordance_id: task.queuedAffordances[0]?.ref ?? `plan-task:${task.alias}`, |
There was a problem hiding this comment.
Preserve every queued affordance in saved programs
The Goal Stack permits repeatedly dropping capabilities onto a task and renders queuedAffordances as an array, but program export serializes only index zero into affordance_id. Any second and later queued capability is silently omitted from the materialized graph; additionally, the preceding verify and review return path omits even the first one. Split queued affordances into executable nodes or reject unsupported multi-affordance tasks before reporting a successful save.
Useful? React with 👍 / 👎.
| window.localStorage.removeItem(PROJECT_STORAGE_KEY); | ||
| setProjectId(''); | ||
| setSurface(null); | ||
| setHistory(null); | ||
| setFindHits([]); | ||
| lastFindRef.current = null; |
There was a problem hiding this comment.
Clear the selected entity when changing projects
After selecting an entity in project A, clicking Change project only clears the surface, history, and Find results. selected is retained, so before or after opening project B the inspector continues to display A's path and labels it as inside the current project membrane. Reset the selected entity, and ideally the expansion state, when discarding the active project.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/console/src/components/shell/IntuiShell.tsx (1)
326-343: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winFix keyboard shortcut detection for Alt and Shift modifiers.
Checking
event.keyagainst a numeric string (e.g.,'1') fails when modifier keys alter the character output. For example, pressingShift + 1setsevent.keyto!, and pressingAlt + 1on macOS setsevent.keyto¡. This breaks navigation for macOS users and prevents all users from toggling companions withAlt+Shift+1..3.Use
event.code(e.g.,'Digit1') instead, which represents the physical key pressed regardless of active modifiers or keyboard layout.⌨️ Proposed fix using `event.code`
const onKeyDown = (event: KeyboardEvent) => { if (!event.altKey || event.ctrlKey || event.metaKey) return; if (event.shiftKey) { companions.forEach((region, index) => { - if (event.key === String(index + 1)) { + if (event.code === `Digit${index + 1}`) { event.preventDefault(); toggle(region); } }); return; } primarySurfaces.forEach((surface, index) => { - if (event.key === String(index + 1)) { + if (event.code === `Digit${index + 1}`) { event.preventDefault(); void host.activateSurface(surface.id); } }); };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/console/src/components/shell/IntuiShell.tsx` around lines 326 - 343, Update the onKeyDown shortcut detection to compare event.code with the corresponding physical digit key codes, such as Digit1, instead of comparing event.key to numeric strings. Apply this change to both the companions toggle branch and the primarySurfaces activation branch while preserving the existing modifier checks and actions.
🧹 Nitpick comments (4)
apps/console/src/views/goal-stack/GoalStackView.tsx (1)
184-184: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winMemoize the per-render node/selection mapping.
nodes.map((node) => ({ ...node, selected: ... }))allocates a brand-new array (and new object per node) on every render, including every poll tick, even when neithernodesnorselectedTaskIdchanged meaningfully for most nodes. Since custom node types are matched by reference/shallow-prop diffing in@xyflow/react, this can force everyPlanTaskNodeto re-render on each poll tick regardless of graph size.♻️ Suggested fix
+ const renderedNodes = useMemo( + () => nodes.map((node) => ({ ...node, selected: node.id === selectedTaskId })), + [nodes, selectedTaskId], + ); ... <ReactFlow - nodes={nodes.map((node) => ({ ...node, selected: node.id === selectedTaskId }))} + nodes={renderedNodes}🤖 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 `@apps/console/src/views/goal-stack/GoalStackView.tsx` at line 184, Memoize the mapped nodes passed to the graph in GoalStackView, using nodes and selectedTaskId as dependencies so the array and node objects are reused when inputs are unchanged. Preserve the existing selected comparison and pass the memoized result to the nodes prop.apps/console/src/views/goal-stack/plan-layout.ts (1)
72-88: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winO(n)
tasks.findper edge inside the edge-mapping loop.
tasks.find((candidate) => candidate.id === edge.target)runs once per edge, giving O(n*m) work on every layout pass, which recomputes every poll tick (1.5s). Building aMap<string, PlanTask>once and looking up by id would be O(1) per edge.♻️ Suggested fix
+ const taskById = new Map(tasks.map((task) => [task.id, task] as const)); return { ... edges: edgePairs.map((edge) => { - const task = tasks.find((candidate) => candidate.id === edge.target); + const task = taskById.get(edge.target);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/console/src/views/goal-stack/plan-layout.ts` around lines 72 - 88, Replace the per-edge tasks.find lookup in the edge-mapping logic with a single Map keyed by task id, created before mapping edgePairs. Use constant-time map lookups for each edge while preserving the existing fallback status and edge state/progress behavior.apps/console/src/views/workspace/WorkspaceSubstrateView.tsx (1)
260-270: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate "Readiness by capability" list markup.
The same capability list JSX is repeated verbatim in the popover and the "Entity contract" panel. Extracting a small
ReadinessCapabilityListcomponent would remove the duplication and keep both surfaces in sync automatically.♻️ Proposed extraction
+function ReadinessCapabilityList({ readiness }: { readiness: WorkspaceReadiness | null }) { + return ( + <ul className="mt-2 grid gap-1"> + {readiness?.capabilities.map((capability) => ( + <li key={capability.capability} className="flex gap-2 border-b border-ij-divider py-1"> + <span>{capability.capability}</span> + <span className="ml-auto text-ij-ink-info"> + {capability.missing.length ? capability.missing.join(', ') : capability.state} + </span> + </li> + ))} + </ul> + ); +}Then replace both inline
<ul>...</ul>blocks with<ReadinessCapabilityList readiness={readiness} />.Also applies to: 462-473
🤖 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 `@apps/console/src/views/workspace/WorkspaceSubstrateView.tsx` around lines 260 - 270, Extract the duplicated capability-list JSX into a shared ReadinessCapabilityList component that accepts readiness, preserving the existing rendering and styling. Replace both inline capability <ul> blocks in the popover and Entity contract panel with ReadinessCapabilityList readiness={readiness}, keeping both surfaces synchronized.apps/console/src/app/api/harness/plan/route.ts (1)
17-26: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winParallelize the three independent Harness MCP calls in the polling path.
snapshot,events, andmanifestare all independent (eventsonly needscursor/planId;manifestonly needsplanId), yet they're awaited sequentially. Since this is a polling endpoint, three sequential round trips per poll tick adds unnecessary latency thatPromise.allwould avoid.⚡ Proposed fix to parallelize independent calls
- const snapshot = await callHarnessMcp('plan', { action: 'inspect', plan_id: planId }); - if (!snapshot.ok) return snapshot.response; - const events = await callHarnessMcp('plan', { - action: 'what_changed', - plan_id: planId, - anchor: `graph:${cursor}`, - }); - const manifest = includeManifest - ? await callHarnessMcp('plan', { action: 'capability_manifest', plan_id: planId }) - : null; + const [snapshot, events, manifest] = await Promise.all([ + callHarnessMcp('plan', { action: 'inspect', plan_id: planId }), + callHarnessMcp('plan', { + action: 'what_changed', + plan_id: planId, + anchor: `graph:${cursor}`, + }), + includeManifest + ? callHarnessMcp('plan', { action: 'capability_manifest', plan_id: planId }) + : Promise.resolve(null), + ]); + if (!snapshot.ok) return snapshot.response;🤖 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 `@apps/console/src/app/api/harness/plan/route.ts` around lines 17 - 26, Parallelize the independent Harness MCP requests in the polling flow by starting snapshot, events, and the conditional manifest call together and awaiting them with Promise.all. Preserve the existing snapshot.ok early return and manifest null behavior, while retaining the existing request parameters and response handling.
🤖 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 `@apps/console/src/motion/motion.css`:
- Around line 15-27: Move the 0.9s and 0.22s durations from .goal-edge-running
and .goal-edge-progress into motion-tokens.ts, exposing them as CSS custom
properties consistent with the existing DUR.* pattern, then reference those
variables in motion.css. Also confirm that the stroke-dashoffset animations for
these SVG progress edges are an approved exception to the transform/opacity-only
motion policy.
In `@apps/console/src/views/goal-stack/PlanPermissionPrompt.tsx`:
- Line 27: Update the dialog container’s contentClassName to use outline-hidden
instead of outline-none, preserving the existing positioning and sizing classes
while retaining the accessible focus fallback in forced-colors mode.
In `@apps/console/src/views/goal-stack/ToolPalette.tsx`:
- Around line 32-66: Make capability queuing keyboard-operable: in
apps/console/src/views/goal-stack/ToolPalette.tsx:32-66, update
DraggableCapability’s CommandItem with explicit focusability and
dnd-kit-compatible role, aria-roledescription, and aria-label; in
apps/console/src/views/goal-stack/PlanTaskNode.tsx:9-35, make no direct change
but confirm the droppable works with keyboard coordinate collision detection; in
apps/console/src/views/goal-stack/NodeInspector.tsx:30-48, add a
keyboard-operable “Add affordance” control that opens a capability picker; and
in apps/console/src/views/goal-stack/GoalStackView.tsx:113-123, connect that
action to the existing mutate('queue_affordance', ...) flow used by onDragEnd.
In `@apps/console/src/views/workspace/WorkspaceSubstrateView.tsx`:
- Around line 253-259: Replace the raw Tailwind shadow-xl class on
Popover.Content with the approved Int UI register/theme-engine elevation token,
preserving the existing layout and semantic styling classes. Ensure the
resulting popover uses a gated elevation value compatible with the register and
contrast merge gates.
- Around line 226-240: Update the “Change project” reset handler to clear the
expanded tree state by resetting the expanded collection, alongside
setProjectId, setSurface, setHistory, and setFindHits. Ensure stale expansion
IDs cannot affect the new project’s auto-expand behavior.
- Around line 66-99: Remove expanded.size from the polling useEffect dependency
list so tree expand/collapse actions do not restart or abort polling. Use a ref
to guard the initial root auto-expansion instead of reading expanded.size, and
preserve the existing projectId-driven polling lifecycle and cadence.
In `@packages/theorem-acp/src/workspace-state.ts`:
- Around line 89-116: The fixed-depth NODE_FIELDS selection truncates
descendants and causes workspaceTreeRows() to misidentify deeper nodes as
leaves. Replace the hard-coded nested selection used by fetchWorkspaceSurface()
with the workspace tree’s supported on-demand child-fetch mechanism, preserving
the existing root snapshot and readiness data while allowing expansion to
retrieve deeper children.
---
Outside diff comments:
In `@apps/console/src/components/shell/IntuiShell.tsx`:
- Around line 326-343: Update the onKeyDown shortcut detection to compare
event.code with the corresponding physical digit key codes, such as Digit1,
instead of comparing event.key to numeric strings. Apply this change to both the
companions toggle branch and the primarySurfaces activation branch while
preserving the existing modifier checks and actions.
---
Nitpick comments:
In `@apps/console/src/app/api/harness/plan/route.ts`:
- Around line 17-26: Parallelize the independent Harness MCP requests in the
polling flow by starting snapshot, events, and the conditional manifest call
together and awaiting them with Promise.all. Preserve the existing snapshot.ok
early return and manifest null behavior, while retaining the existing request
parameters and response handling.
In `@apps/console/src/views/goal-stack/GoalStackView.tsx`:
- Line 184: Memoize the mapped nodes passed to the graph in GoalStackView, using
nodes and selectedTaskId as dependencies so the array and node objects are
reused when inputs are unchanged. Preserve the existing selected comparison and
pass the memoized result to the nodes prop.
In `@apps/console/src/views/goal-stack/plan-layout.ts`:
- Around line 72-88: Replace the per-edge tasks.find lookup in the edge-mapping
logic with a single Map keyed by task id, created before mapping edgePairs. Use
constant-time map lookups for each edge while preserving the existing fallback
status and edge state/progress behavior.
In `@apps/console/src/views/workspace/WorkspaceSubstrateView.tsx`:
- Around line 260-270: Extract the duplicated capability-list JSX into a shared
ReadinessCapabilityList component that accepts readiness, preserving the
existing rendering and styling. Replace both inline capability <ul> blocks in
the popover and Entity contract panel with ReadinessCapabilityList
readiness={readiness}, keeping both surfaces synchronized.
🪄 Autofix (Beta)
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: f2192b4e-f8bf-4f4f-8447-88965cdc90b3
⛔ Files ignored due to path filters (16)
apps/console/e2e/appearance.spec.ts-snapshots/workspace-1280-light-darwin.pngis excluded by!**/*.pngapps/console/e2e/appearance.spec.ts-snapshots/workspace-1280-light-linux.pngis excluded by!**/*.pngapps/console/e2e/appearance.spec.ts-snapshots/workspace-1440-light-darwin.pngis excluded by!**/*.pngapps/console/e2e/appearance.spec.ts-snapshots/workspace-1440-light-linux.pngis excluded by!**/*.pngapps/console/e2e/console-ia.spec.ts-snapshots/chat-empty-darwin.pngis excluded by!**/*.pngapps/console/e2e/console-ia.spec.ts-snapshots/chat-plan-darwin.pngis excluded by!**/*.pngapps/console/e2e/console-ia.spec.ts-snapshots/chat-streaming-darwin.pngis excluded by!**/*.pngapps/console/e2e/console-ia.spec.ts-snapshots/files-projection-darwin.pngis excluded by!**/*.pngapps/console/e2e/console-ia.spec.ts-snapshots/files-projection-linux.pngis excluded by!**/*.pngapps/console/e2e/console-ia.spec.ts-snapshots/stripe-groups-darwin.pngis excluded by!**/*.pngapps/console/e2e/console-ia.spec.ts-snapshots/stripe-groups-linux.pngis excluded by!**/*.pngapps/console/e2e/workspace-goal-stack.spec.ts-snapshots/goal-stack-v2-1440-dark-darwin.pngis excluded by!**/*.pngapps/console/e2e/workspace-goal-stack.spec.ts-snapshots/goal-stack-v2-1440-dark-linux.pngis excluded by!**/*.pngapps/console/e2e/workspace-goal-stack.spec.ts-snapshots/workspace-substrate-v2-1440-dark-darwin.pngis excluded by!**/*.pngapps/console/e2e/workspace-goal-stack.spec.ts-snapshots/workspace-substrate-v2-1440-dark-linux.pngis excluded by!**/*.pngpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (41)
apps/console/AGENTS.mdapps/console/CLAUDE.mdapps/console/e2e/cards.spec.tsapps/console/e2e/console-ia.spec.tsapps/console/e2e/workspace-goal-stack.spec.tsapps/console/package.jsonapps/console/src/app/api/harness/plan/route.tsapps/console/src/app/api/workspace/route.tsapps/console/src/components/ConsoleApp.tsxapps/console/src/components/shell/IntuiShell.tsxapps/console/src/components/shell/SearchField.tsxapps/console/src/lib/console-host.test.tsapps/console/src/lib/console-host.tsapps/console/src/lib/goal-stack-contract.test.tsapps/console/src/lib/harness-mcp-core.tsapps/console/src/lib/harness-mcp.test.tsapps/console/src/lib/plan-route.test.tsapps/console/src/lib/server/harness-mcp.tsapps/console/src/lib/thread-readiness.test.tsapps/console/src/lib/thread-store.test.tsapps/console/src/lib/thread-store.tsapps/console/src/lib/workspace-route.test.tsapps/console/src/lib/workspace-seed.tsapps/console/src/lib/workspace-state.test.tsapps/console/src/motion/motion-tokens.tsapps/console/src/motion/motion.cssapps/console/src/views/ThreadView.tsxapps/console/src/views/goal-stack/GoalStackView.tsxapps/console/src/views/goal-stack/NodeInspector.tsxapps/console/src/views/goal-stack/PlanPermissionPrompt.tsxapps/console/src/views/goal-stack/PlanTaskNode.tsxapps/console/src/views/goal-stack/ProgressEdge.tsxapps/console/src/views/goal-stack/ToolPalette.tsxapps/console/src/views/goal-stack/plan-layout.tsapps/console/src/views/registry.tsxapps/console/src/views/workspace/WorkspaceHistoryDiff.tsxapps/console/src/views/workspace/WorkspaceSubstrateView.tsxpackages/theorem-acp/package.jsonpackages/theorem-acp/src/plan-program.tspackages/theorem-acp/src/plan-state.tspackages/theorem-acp/src/workspace-state.ts
| @keyframes goal-edge-march { | ||
| to { | ||
| stroke-dashoffset: -0.08; | ||
| } | ||
| } | ||
|
|
||
| .goal-edge-running { | ||
| animation: goal-edge-march 0.9s linear infinite; | ||
| } | ||
|
|
||
| .goal-edge-progress { | ||
| transition: stroke-dashoffset 0.22s var(--ij-ease); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Literal durations hardcoded outside motion-tokens.ts.
0.9s and 0.22s are written directly into this stylesheet. As per coding guidelines, "Do not use porcelain --cr-* tokens, beige styling, raw hex values outside register files, arbitrary-value Tailwind classes, raw palette utilities, *.module.css, or literal durations outside src/motion/motion-tokens.ts." Define these durations as tokens in motion-tokens.ts (exposed as CSS custom properties, matching the existing DUR.* pattern referenced elsewhere in the inventory) and reference them here instead of the literal values. Separately, both animated properties are stroke-dashoffset rather than transform/opacity; worth confirming this is an intentional, reviewed exception for SVG progress edges given the broader motion policy favors transform/opacity-only animation.
🤖 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 `@apps/console/src/motion/motion.css` around lines 15 - 27, Move the 0.9s and
0.22s durations from .goal-edge-running and .goal-edge-progress into
motion-tokens.ts, exposing them as CSS custom properties consistent with the
existing DUR.* pattern, then reference those variables in motion.css. Also
confirm that the stroke-dashoffset animations for these SVG progress edges are
an approved exception to the transform/opacity-only motion policy.
Source: Coding guidelines
| label="Approval required" | ||
| shouldFilter={false} | ||
| overlayClassName="fixed inset-0 z-50 bg-ij-ground opacity-75" | ||
| contentClassName="fixed inset-x-0 top-1/3 z-50 mx-auto w-144 max-w-full outline-none" |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '--- file ---\n'
sed -n '1,120p' apps/console/src/views/goal-stack/PlanPermissionPrompt.tsx
printf '\n--- tailwind/package references ---\n'
rg -n '"tailwindcss"|outline-hidden|outline-none' package.json apps/console package.json . -g 'package.json' -g '*.css' -g '*.ts' -g '*.tsx' -g '*.js' -g '*.mjs' | sed -n '1,200p'Repository: Travis-Gilbert/CommonPlace
Length of output: 255
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- apps/console/src/views/goal-stack/PlanPermissionPrompt.tsx ---'
sed -n '1,140p' apps/console/src/views/goal-stack/PlanPermissionPrompt.tsx
echo
echo '--- tailwind references ---'
rg -n 'outline-hidden|outline-none|tailwindcss' \
package.json pnpm-lock.yaml package-lock.json yarn.lock \
apps/console package.json . \
-g 'package.json' -g '*.css' -g '*.ts' -g '*.tsx' -g '*.js' -g '*.mjs' -g '*.json' -g 'pnpm-lock.yaml' -g 'package-lock.json' -g 'yarn.lock' \
| sed -n '1,200p'Repository: Travis-Gilbert/CommonPlace
Length of output: 22463
🌐 Web query:
Tailwind CSS v4 outline-hidden forced-colors outline-none docs
💡 Result:
In Tailwind CSS v4, the outline-none and outline-hidden utilities serve distinct purposes to clarify behavior regarding accessibility and forced colors mode [1][2]. ### Key Differences * outline-none: In v4, this utility now functions as a direct mapping to the CSS property outline-style: none [1][2]. It removes the outline completely, which can make elements inaccessible to users who rely on focus rings, especially in forced colors mode [3]. * outline-hidden: This utility is the replacement for the v3 behavior of outline-none [1][2]. It applies a 2px transparent outline with a 2px offset (outline: 2px solid transparent; outline-offset: 2px;) [4]. This approach is preferred for accessibility because it maintains a focus indicator that remains visible to the browser’s rendering engine in forced colors mode, even though it appears invisible in standard view [4][3]. ### Transition from v3 to v4 When upgrading from v3 to v4, you should generally replace existing instances of outline-none with outline-hidden to maintain the original accessibility behavior [1][2]. Use outline-none only when you specifically intend to remove the outline entirely without providing a transparent fallback for forced colors mode [2]. For technical implementation details, consult the official Tailwind CSS documentation on outline styles [4] and the upgrade guide [1].
Citations:
- 1: https://tailwindcss.com/docs/upgrade-guide
- 2: https://v3.tailwindcss.com/docs/v4-beta
- 3: Replace
outline-nonewithoutline-hidden, add newoutline-nonetailwindlabs/tailwindcss#14926 - 4: https://tailwindcss.com/docs/outline-style
Prefer outline-hidden on the dialog container. outline-none strips the focus outline entirely in forced-colors mode; outline-hidden keeps the accessible fallback while hiding it visually.
🤖 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 `@apps/console/src/views/goal-stack/PlanPermissionPrompt.tsx` at line 27,
Update the dialog container’s contentClassName to use outline-hidden instead of
outline-none, preserving the existing positioning and sizing classes while
retaining the accessible focus fallback in forced-colors mode.
| function DraggableCapability({ capability }: { capability: PlanCapability }) { | ||
| const { | ||
| attributes, | ||
| isDragging, | ||
| listeners, | ||
| setNodeRef, | ||
| } = useDraggable({ | ||
| id: `goal-capability:${capability.id}`, | ||
| data: { capability }, | ||
| }); | ||
| return ( | ||
| <CommandItem | ||
| ref={setNodeRef} | ||
| value={`${capability.title} ${capability.description} ${capability.serverOrigin}`} | ||
| {...attributes} | ||
| {...listeners} | ||
| className="mb-1 grid cursor-grab gap-1 rounded-ij-arc border border-ij-seam-raised bg-ij-raised p-2 data-[selected=true]:bg-ij-selection" | ||
| style={{ opacity: isDragging ? 0.4 : 1 }} | ||
| > | ||
| <span className="flex items-center gap-2"> | ||
| <strong className="truncate">{capability.title}</strong> | ||
| <span className="ml-auto font-ij-mono text-ij-ink-info">{capability.serverOrigin}</span> | ||
| </span> | ||
| <span className="line-clamp-2 text-ij-ink-info">{capability.description}</span> | ||
| <span className="flex gap-1"> | ||
| <span className="rounded-ij-arc-underline bg-ij-selection-inactive px-1"> | ||
| {capability.annotations.readOnly ? 'read-only' : 'writes'} | ||
| </span> | ||
| {capability.annotations.destructive ? ( | ||
| <span className="rounded-ij-arc-underline bg-ij-warn-bg px-1 text-ij-warn">destructive</span> | ||
| ) : null} | ||
| </span> | ||
| </CommandItem> | ||
| ); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Queuing an affordance is DnD-only and may not be keyboard-operable. dnd-kit's default Keyboard sensor requires the draggable activator to be natively focusable, but the draggable capability item is a cmdk CommandItem with no explicit tabIndex, and no part of this flow offers a non-drag alternative to queue an affordance.
apps/console/src/views/goal-stack/ToolPalette.tsx#L32-L66: giveDraggableCapability'sCommandIteman explicittabIndex={0}(or otherwise verify cmdk grants native focus per item) so dnd-kit's Keyboard sensor can activate it, and addrole/aria-roledescription/aria-labelconsistent with dnd-kit's accessibility guide.apps/console/src/views/goal-stack/PlanTaskNode.tsx#L9-L35: no change needed here once the draggable side is fixed, but confirm the droppable is reachable via the Keyboard sensor's coordinate-based collision detection once picked up.apps/console/src/views/goal-stack/NodeInspector.tsx#L30-L48: add a keyboard-operable "Add affordance" control (e.g. a button opening a capability picker) as a non-DnD fallback for queuing.apps/console/src/views/goal-stack/GoalStackView.tsx#L113-L123: wire the new non-DnD "Add affordance" action to the samemutate('queue_affordance', ...)call used byonDragEnd.
📍 Affects 4 files
apps/console/src/views/goal-stack/ToolPalette.tsx#L32-L66(this comment)apps/console/src/views/goal-stack/PlanTaskNode.tsx#L9-L35apps/console/src/views/goal-stack/NodeInspector.tsx#L30-L48apps/console/src/views/goal-stack/GoalStackView.tsx#L113-L123
🤖 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 `@apps/console/src/views/goal-stack/ToolPalette.tsx` around lines 32 - 66, Make
capability queuing keyboard-operable: in
apps/console/src/views/goal-stack/ToolPalette.tsx:32-66, update
DraggableCapability’s CommandItem with explicit focusability and
dnd-kit-compatible role, aria-roledescription, and aria-label; in
apps/console/src/views/goal-stack/PlanTaskNode.tsx:9-35, make no direct change
but confirm the droppable works with keyboard coordinate collision detection; in
apps/console/src/views/goal-stack/NodeInspector.tsx:30-48, add a
keyboard-operable “Add affordance” control that opens a capability picker; and
in apps/console/src/views/goal-stack/GoalStackView.tsx:113-123, connect that
action to the existing mutate('queue_affordance', ...) flow used by onDragEnd.
| useEffect(() => { | ||
| const controller = new AbortController(); | ||
| let stopped = false; | ||
| let timer: ReturnType<typeof setTimeout> | undefined; | ||
| const poll = async () => { | ||
| try { | ||
| const next = projectId | ||
| ? await fetchWorkspaceSurface(projectId, { signal: controller.signal }) | ||
| : null; | ||
| const nextReadiness = next?.readiness | ||
| ?? await fetchWorkspaceReadiness({ signal: controller.signal }); | ||
| if (stopped) return; | ||
| setSurface(next); | ||
| setReadiness(nextReadiness); | ||
| if (next && expanded.size === 0) { | ||
| setExpanded(new Set(next.tree.roots.map((root) => root.id))); | ||
| } | ||
| setStatus('live'); | ||
| setError(null); | ||
| } catch (pollError) { | ||
| if (stopped || controller.signal.aborted) return; | ||
| setStatus('reconnecting'); | ||
| setError(pollError instanceof Error ? pollError.message : String(pollError)); | ||
| } finally { | ||
| if (!stopped) timer = setTimeout(() => void poll(), POLL_MS); | ||
| } | ||
| }; | ||
| void poll(); | ||
| return () => { | ||
| stopped = true; | ||
| controller.abort(); | ||
| if (timer) clearTimeout(timer); | ||
| }; | ||
| }, [expanded.size, projectId]); |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
Every tree expand/collapse restarts the entire polling loop.
expanded.size is a dependency of the polling useEffect. Since toggleRow changes the set on every click, each expand/collapse aborts the in-flight poll request and immediately issues a new one, resetting the POLL_MS cadence. This causes excess network calls that scale with how much a user interacts with the tree.
⚡ Proposed fix: track initial auto-expand with a ref instead of a dependency
+ const autoExpandedRef = useRef(false);
useEffect(() => {
const controller = new AbortController();
let stopped = false;
let timer: ReturnType<typeof setTimeout> | undefined;
const poll = async () => {
try {
const next = projectId
? await fetchWorkspaceSurface(projectId, { signal: controller.signal })
: null;
const nextReadiness = next?.readiness
?? await fetchWorkspaceReadiness({ signal: controller.signal });
if (stopped) return;
setSurface(next);
setReadiness(nextReadiness);
- if (next && expanded.size === 0) {
+ if (next && !autoExpandedRef.current) {
+ autoExpandedRef.current = true;
setExpanded(new Set(next.tree.roots.map((root) => root.id)));
}
setStatus('live');
setError(null);
} catch (pollError) {
if (stopped || controller.signal.aborted) return;
setStatus('reconnecting');
setError(pollError instanceof Error ? pollError.message : String(pollError));
} finally {
if (!stopped) timer = setTimeout(() => void poll(), POLL_MS);
}
};
void poll();
return () => {
stopped = true;
controller.abort();
if (timer) clearTimeout(timer);
};
- }, [expanded.size, projectId]);
+ }, [projectId]);Reset autoExpandedRef.current = false alongside setExpanded(new Set()) in the "Change project" handler below (see companion comment).
🤖 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 `@apps/console/src/views/workspace/WorkspaceSubstrateView.tsx` around lines 66
- 99, Remove expanded.size from the polling useEffect dependency list so tree
expand/collapse actions do not restart or abort polling. Use a ref to guard the
initial root auto-expansion instead of reading expanded.size, and preserve the
existing projectId-driven polling lifecycle and cadence.
| <button | ||
| type="button" | ||
| onClick={() => { | ||
| window.localStorage.removeItem(PROJECT_STORAGE_KEY); | ||
| setProjectId(''); | ||
| setSurface(null); | ||
| setHistory(null); | ||
| setFindHits([]); | ||
| lastFindRef.current = null; | ||
| }} | ||
| className="h-ij-control rounded-ij-arc border border-ij-control-border px-2 hover:bg-ij-hover-surface" | ||
| > | ||
| Change project | ||
| </button> | ||
| ) : null} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
"Change project" doesn't reset expanded, so the new project's tree may stay collapsed.
If expanded still holds stale ids from the previous project, the polling effect's expanded.size === 0 auto-expand check never fires for the new project (size is non-zero from stale entries), leaving the new tree fully collapsed with no roots shown expanded.
🔧 Proposed fix
onClick={() => {
window.localStorage.removeItem(PROJECT_STORAGE_KEY);
setProjectId('');
setSurface(null);
setHistory(null);
setFindHits([]);
lastFindRef.current = null;
+ setExpanded(new Set());
}}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button | |
| type="button" | |
| onClick={() => { | |
| window.localStorage.removeItem(PROJECT_STORAGE_KEY); | |
| setProjectId(''); | |
| setSurface(null); | |
| setHistory(null); | |
| setFindHits([]); | |
| lastFindRef.current = null; | |
| }} | |
| className="h-ij-control rounded-ij-arc border border-ij-control-border px-2 hover:bg-ij-hover-surface" | |
| > | |
| Change project | |
| </button> | |
| ) : null} | |
| <button | |
| type="button" | |
| onClick={() => { | |
| window.localStorage.removeItem(PROJECT_STORAGE_KEY); | |
| setProjectId(''); | |
| setSurface(null); | |
| setHistory(null); | |
| setFindHits([]); | |
| lastFindRef.current = null; | |
| setExpanded(new Set()); | |
| }} | |
| className="h-ij-control rounded-ij-arc border border-ij-control-border px-2 hover:bg-ij-hover-surface" | |
| > | |
| Change project | |
| </button> |
🤖 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 `@apps/console/src/views/workspace/WorkspaceSubstrateView.tsx` around lines 226
- 240, Update the “Change project” reset handler to clear the expanded tree
state by resetting the expanded collection, alongside setProjectId, setSurface,
setHistory, and setFindHits. Ensure stale expansion IDs cannot affect the new
project’s auto-expand behavior.
| <Popover.Portal> | ||
| <Popover.Content | ||
| sideOffset={6} | ||
| align="end" | ||
| aria-label="Readiness by capability" | ||
| className="z-50 w-80 rounded-ij-arc border border-ij-seam-raised bg-ij-raised p-3 text-ij-ink shadow-xl" | ||
| > |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Raw Tailwind shadow-xl bypasses the Int UI elevation register.
Every other visual property on this popover (bg-ij-raised, border-ij-seam-raised, text-ij-ink) routes through the pinned Int UI register, but elevation here uses a generic Tailwind shadow utility instead of an approved register/theme-engine elevation token. Per coding guidelines, elevation must go through the pinned Int UI registers and gated theme engine, and this surface must pass the gate:register/gate:contrast merge gates.
Based on coding guidelines: "Use the pinned Int UI registers and gated theme engine for color, seams, elevation, focus, and states" and "Do not use... arbitrary-value Tailwind classes, raw palette utilities."
🤖 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 `@apps/console/src/views/workspace/WorkspaceSubstrateView.tsx` around lines 253
- 259, Replace the raw Tailwind shadow-xl class on Popover.Content with the
approved Int UI register/theme-engine elevation token, preserving the existing
layout and semantic styling classes. Ensure the resulting popover uses a gated
elevation value compatible with the register and contrast merge gates.
Source: Coding guidelines
| const NODE_FIELDS = ` | ||
| id kind name path excluded | ||
| children { | ||
| id kind name path excluded | ||
| children { | ||
| id kind name path excluded | ||
| children { | ||
| id kind name path excluded | ||
| children { id kind name path excluded } | ||
| } | ||
| } | ||
| } | ||
| `; | ||
|
|
||
| export async function fetchWorkspaceSurface( | ||
| projectId: string, | ||
| options: WorkspaceGraphqlClientOptions = {}, | ||
| ): Promise<WorkspaceSurfaceSnapshot> { | ||
| const data = await graphql<{ projectTree: ProjectTree; readiness: WorkspaceReadiness }>( | ||
| `query WorkspaceSurface($projectId: String!) { | ||
| projectTree(projectId: $projectId) { projectId generation roots { ${NODE_FIELDS} } } | ||
| readiness { generation capabilities { capability state missing } } | ||
| }`, | ||
| { projectId }, | ||
| options, | ||
| ); | ||
| return { schema: WORKSPACE_SUBSTRATE_SCHEMA, tree: data.projectTree, readiness: data.readiness }; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## workspace-state.ts\n'
wc -l packages/theorem-acp/src/workspace-state.ts
sed -n '1,260p' packages/theorem-acp/src/workspace-state.ts
printf '\n## search for workspace tree / children APIs\n'
rg -n "children\\(|hasChildren|WorkspaceTreeNode|WorkspaceSurface|projectTree|WorkspaceGraphqlClientOptions|workspaceTreeRows" packages/theorem-acp packages -g '!**/dist/**' -g '!**/build/**'Repository: Travis-Gilbert/CommonPlace
Length of output: 12102
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## search repo for GraphQL tree schema and lazy-loading fields\n'
rg -n "projectTree|children\\(|hasChildren|WorkspaceTreeNode|WorkspaceSurface|WorkspaceSubstrate|rootPath|addContentRoot|fetchWorkspaceChildren|fileHistory" . \
-g '!**/dist/**' -g '!**/build/**' -g '!**/node_modules/**'
printf '\n## inspect likely schema/docs files if present\n'
fd -a 'schema.graphql' . || true
fd -a '*.graphql' . || true
fd -a '*workspace*' . || trueRepository: Travis-Gilbert/CommonPlace
Length of output: 12266
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,180p' apps/console/src/views/workspace/WorkspaceSubstrateView.tsxRepository: Travis-Gilbert/CommonPlace
Length of output: 7019
Fixed-depth workspace tree query silently truncates deeper nodes.
NODE_FIELDS only descends 4 levels, so deeper descendants never come back in fetchWorkspaceSurface(). workspaceTreeRows() then treats those truncated nodes as leaves (children.length === 0), so the UI can’t expand into the missing subtree. Fetch children on demand instead of materializing the entire tree with a hard-coded depth cap.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/theorem-acp/src/workspace-state.ts` around lines 89 - 116, The
fixed-depth NODE_FIELDS selection truncates descendants and causes
workspaceTreeRows() to misidentify deeper nodes as leaves. Replace the
hard-coded nested selection used by fetchWorkspaceSurface() with the workspace
tree’s supported on-demand child-fetch mechanism, preserving the existing root
snapshot and readiness data while allowing expansion to retrieve deeper
children.
Summary
apps/webuntouchedValidation
Linked substrate
Summary by CodeRabbit