Skip to content

feat(console): add V2 workspace and goal stack surfaces - #78

Merged
cursor[bot] merged 2 commits into
mainfrom
Travis-Gilbert/workspace-goal-stack-ui
Jul 19, 2026
Merged

feat(console): add V2 workspace and goal stack surfaces#78
cursor[bot] merged 2 commits into
mainfrom
Travis-Gilbert/workspace-goal-stack-ui

Conversation

@Travis-Gilbert

@Travis-Gilbert Travis-Gilbert commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • adds the Workspace substrate surface to the canonical V2 console
  • adds the Goal Stack Plan canvas with React Flow, Dagre, dnd-kit, cmdk, CodeMirror Merge, TanStack Virtual, and Radix Popover
  • wires identity-bound workspace GraphQL and Harness Plan routes
  • leaves legacy apps/web untouched

Validation

  • theorem-acp and Console TypeScript checks
  • 117 Console unit tests
  • Console lint and five UI gates
  • production webpack build
  • 36 focused Playwright behavior and visual tests; the live route smoke remains available behind environment IDs

Linked substrate

  • Theorem #273 provides the durable VFS, workspace/readiness model, Goal and Plan runtime, Code Mode, GraphQL, and MCP contracts consumed by this V2 surface.

Summary by CodeRabbit

  • New Features
    • Added a Goal Stack surface with interactive plan graphs, task progress, capability palette, approvals, replanning, and restore actions.
    • Added a Workspace surface with virtualized project navigation, project search, file history, revision comparisons, imports, and content-root management.
    • Added support for saving completed plans as reusable programs.
    • Added keyboard navigation for six primary surfaces, including Goals.
  • Bug Fixes
    • Improved workspace import validation and protection against unauthorized or symlink-escaped paths.
    • Assistant messages now indicate when responses are degraded due to unavailable capabilities.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

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

Changes

Shared projection contracts

Layer / File(s) Summary
Workspace, plan, and programmable graph contracts
packages/theorem-acp/src/*, packages/theorem-acp/package.json
Adds normalized workspace and plan projections, polling/subscription utilities, GraphQL operations, programmable graph conversion, and public package exports.

Server integration and validation

Layer / File(s) Summary
Workspace and harness API routes
apps/console/src/app/api/workspace/route.ts, apps/console/src/app/api/harness/plan/route.ts, apps/console/src/lib/server/harness-mcp.ts
Adds canonical GraphQL forwarding, workspace path validation, identity-bound MCP requests, plan polling, mutations, refusal responses, and program materialization.

Workspace Substrate

Layer / File(s) Summary
Workspace surface and persistence migration
apps/console/src/views/workspace/*, apps/console/src/lib/workspace-seed.ts, apps/console/src/lib/console-host.ts, apps/console/src/views/registry.tsx
Adds virtualized workspace trees, readiness/search, project import, file history, restore, CodeMirror diffs, seeded surface wiring, and persisted-layout migration.

Goal Stack

Layer / File(s) Summary
Plan graph and interaction surface
apps/console/src/views/goal-stack/*, apps/console/src/app/api/harness/plan/route.ts, apps/console/src/motion/*
Adds DAG layout, task nodes, progress edges, capability drag-and-drop, approval prompts, task inspection actions, and plan mutations with animated edge progress.

Readiness degradation

Layer / File(s) Summary
Assistant degradation metadata
apps/console/src/lib/thread-store.ts, apps/console/src/components/ConsoleApp.tsx, apps/console/src/views/ThreadView.tsx
Derives missing readiness indexes, attaches degradation metadata to assistant messages, and renders the degradation badge.

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding V2 Workspace and Goal Stack surfaces to the Console.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch Travis-Gilbert/workspace-goal-stack-ui

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.

@ecc-tools

ecc-tools Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR.

@Travis-Gilbert
Travis-Gilbert force-pushed the Travis-Gilbert/workspace-goal-stack-ui branch from 7cde5b5 to e2e8a48 Compare July 19, 2026 05:23
@ecc-tools

ecc-tools Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR.

@ecc-tools

ecc-tools Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR.

@Travis-Gilbert
Travis-Gilbert marked this pull request as ready for review July 19, 2026 09:37
Copilot AI review requested due to automatic review settings July 19, 2026 09:37

Copilot AI 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.

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.

Comment on lines +228 to +235
onClick={() => {
window.localStorage.removeItem(PROJECT_STORAGE_KEY);
setProjectId('');
setSurface(null);
setHistory(null);
setFindHits([]);
lastFindRef.current = null;
}}
Comment on lines +97 to +107
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;
Comment on lines +38 to +45
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 })),
]);
Comment on lines +72 to +88
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,
},
};
}),

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

Comment on lines +12 to +14
children {
id kind name path excluded
children { id kind name path excluded }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +42 to +44
...task.supersedes
.filter((source) => visible.has(source))
.map((source) => ({ source, target: task.id, relation: 'supersedes' as const })),

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 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 👍 / 👎.

Comment on lines +83 to +88
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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +98 to +101
return {
...base,
kind: 'stochastic',
affordance_id: task.queuedAffordances[0]?.ref ?? `plan-task:${task.alias}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +229 to +234
window.localStorage.removeItem(PROJECT_STORAGE_KEY);
setProjectId('');
setSurface(null);
setHistory(null);
setFindHits([]);
lastFindRef.current = 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 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 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

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 win

Fix keyboard shortcut detection for Alt and Shift modifiers.

Checking event.key against a numeric string (e.g., '1') fails when modifier keys alter the character output. For example, pressing Shift + 1 sets event.key to !, and pressing Alt + 1 on macOS sets event.key to ¡. This breaks navigation for macOS users and prevents all users from toggling companions with Alt+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 win

Memoize 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 neither nodes nor selectedTaskId changed meaningfully for most nodes. Since custom node types are matched by reference/shallow-prop diffing in @xyflow/react, this can force every PlanTaskNode to 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 win

O(n) tasks.find per 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 a Map<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 win

Duplicate "Readiness by capability" list markup.

The same capability list JSX is repeated verbatim in the popover and the "Entity contract" panel. Extracting a small ReadinessCapabilityList component 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 win

Parallelize the three independent Harness MCP calls in the polling path.

snapshot, events, and manifest are all independent (events only needs cursor/planId; manifest only needs planId), yet they're awaited sequentially. Since this is a polling endpoint, three sequential round trips per poll tick adds unnecessary latency that Promise.all would 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

📥 Commits

Reviewing files that changed from the base of the PR and between 98ab888 and 98bc32c.

⛔ Files ignored due to path filters (16)
  • apps/console/e2e/appearance.spec.ts-snapshots/workspace-1280-light-darwin.png is excluded by !**/*.png
  • apps/console/e2e/appearance.spec.ts-snapshots/workspace-1280-light-linux.png is excluded by !**/*.png
  • apps/console/e2e/appearance.spec.ts-snapshots/workspace-1440-light-darwin.png is excluded by !**/*.png
  • apps/console/e2e/appearance.spec.ts-snapshots/workspace-1440-light-linux.png is excluded by !**/*.png
  • apps/console/e2e/console-ia.spec.ts-snapshots/chat-empty-darwin.png is excluded by !**/*.png
  • apps/console/e2e/console-ia.spec.ts-snapshots/chat-plan-darwin.png is excluded by !**/*.png
  • apps/console/e2e/console-ia.spec.ts-snapshots/chat-streaming-darwin.png is excluded by !**/*.png
  • apps/console/e2e/console-ia.spec.ts-snapshots/files-projection-darwin.png is excluded by !**/*.png
  • apps/console/e2e/console-ia.spec.ts-snapshots/files-projection-linux.png is excluded by !**/*.png
  • apps/console/e2e/console-ia.spec.ts-snapshots/stripe-groups-darwin.png is excluded by !**/*.png
  • apps/console/e2e/console-ia.spec.ts-snapshots/stripe-groups-linux.png is excluded by !**/*.png
  • apps/console/e2e/workspace-goal-stack.spec.ts-snapshots/goal-stack-v2-1440-dark-darwin.png is excluded by !**/*.png
  • apps/console/e2e/workspace-goal-stack.spec.ts-snapshots/goal-stack-v2-1440-dark-linux.png is excluded by !**/*.png
  • apps/console/e2e/workspace-goal-stack.spec.ts-snapshots/workspace-substrate-v2-1440-dark-darwin.png is excluded by !**/*.png
  • apps/console/e2e/workspace-goal-stack.spec.ts-snapshots/workspace-substrate-v2-1440-dark-linux.png is excluded by !**/*.png
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (41)
  • apps/console/AGENTS.md
  • apps/console/CLAUDE.md
  • apps/console/e2e/cards.spec.ts
  • apps/console/e2e/console-ia.spec.ts
  • apps/console/e2e/workspace-goal-stack.spec.ts
  • apps/console/package.json
  • apps/console/src/app/api/harness/plan/route.ts
  • apps/console/src/app/api/workspace/route.ts
  • apps/console/src/components/ConsoleApp.tsx
  • apps/console/src/components/shell/IntuiShell.tsx
  • apps/console/src/components/shell/SearchField.tsx
  • apps/console/src/lib/console-host.test.ts
  • apps/console/src/lib/console-host.ts
  • apps/console/src/lib/goal-stack-contract.test.ts
  • apps/console/src/lib/harness-mcp-core.ts
  • apps/console/src/lib/harness-mcp.test.ts
  • apps/console/src/lib/plan-route.test.ts
  • apps/console/src/lib/server/harness-mcp.ts
  • apps/console/src/lib/thread-readiness.test.ts
  • apps/console/src/lib/thread-store.test.ts
  • apps/console/src/lib/thread-store.ts
  • apps/console/src/lib/workspace-route.test.ts
  • apps/console/src/lib/workspace-seed.ts
  • apps/console/src/lib/workspace-state.test.ts
  • apps/console/src/motion/motion-tokens.ts
  • apps/console/src/motion/motion.css
  • apps/console/src/views/ThreadView.tsx
  • apps/console/src/views/goal-stack/GoalStackView.tsx
  • apps/console/src/views/goal-stack/NodeInspector.tsx
  • apps/console/src/views/goal-stack/PlanPermissionPrompt.tsx
  • apps/console/src/views/goal-stack/PlanTaskNode.tsx
  • apps/console/src/views/goal-stack/ProgressEdge.tsx
  • apps/console/src/views/goal-stack/ToolPalette.tsx
  • apps/console/src/views/goal-stack/plan-layout.ts
  • apps/console/src/views/registry.tsx
  • apps/console/src/views/workspace/WorkspaceHistoryDiff.tsx
  • apps/console/src/views/workspace/WorkspaceSubstrateView.tsx
  • packages/theorem-acp/package.json
  • packages/theorem-acp/src/plan-program.ts
  • packages/theorem-acp/src/plan-state.ts
  • packages/theorem-acp/src/workspace-state.ts

Comment on lines +15 to +27
@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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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:


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.

Comment on lines +32 to +66
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>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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: give DraggableCapability's CommandItem an explicit tabIndex={0} (or otherwise verify cmdk grants native focus per item) so dnd-kit's Keyboard sensor can activate it, and add role/aria-roledescription/aria-label consistent 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 same mutate('queue_affordance', ...) call used by onDragEnd.
📍 Affects 4 files
  • apps/console/src/views/goal-stack/ToolPalette.tsx#L32-L66 (this comment)
  • apps/console/src/views/goal-stack/PlanTaskNode.tsx#L9-L35
  • apps/console/src/views/goal-stack/NodeInspector.tsx#L30-L48
  • apps/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.

Comment on lines +66 to +99
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]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment on lines +226 to +240
<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}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

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

Suggested change
<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.

Comment on lines +253 to +259
<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"
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 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

Comment on lines +89 to +116
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 };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 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*' . || true

Repository: Travis-Gilbert/CommonPlace

Length of output: 12266


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,180p' apps/console/src/views/workspace/WorkspaceSubstrateView.tsx

Repository: 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.

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.

2 participants