Story 3.4: AI semantic suggestions with accept/reject workflow - ProposalBar overlay with visual diff - Accept/reject flow with graph snapshot restore - useProposalDiff hook for change summary - System prompt scoping for selected elements Story 3.5: New diagram wizard with AI type inference - CreateDiagramDialog with AI type inference (Haiku) - initialDescription prop for chat-first flow - Auto-send on mount with hasSentInitial ref guard - DB migration for diagram description column Story 3.6: Hover affordances and command palette - HoverAffordances toolbar (5 AI actions, debounced) - CommandPalette (Cmd+K) with AI, nav, Go to Node - prefillChat/fitViewRequested/focusNodeId actions - Code review: getNodesBounds, onOpenRightPanel, timer cleanup, test count fix 374 tests passing (251 web + 123 AI). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
27 KiB
Story 3.6: Hover Affordances and Command Palette
Status: done
Story
As a user, I want contextual AI actions when I hover over elements and a command palette for power-user access, so that I can discover AI capabilities and execute commands quickly.
Acceptance Criteria
-
Given I hover over a node on the canvas, When the hover is detected (after 300ms delay to prevent flicker), Then a floating mini-toolbar appears near the node with contextual AI actions: Transform, Split, Merge, Explain, Annotate (FR8), And the toolbar disappears when I move the cursor away.
-
Given I click an action in the hover toolbar (e.g., "Split"), When the action is triggered, Then the element is auto-badged in the chat input, And the chat input is pre-filled with the action context (e.g., "Split this into..."), And the cursor is placed in the chat input ready for me to complete the instruction.
-
Given I press Cmd/Ctrl+K anywhere in the diagram editor, When the command palette opens, Then I see a searchable list of commands: diagram operations (new, export, share), AI actions (generate, suggest, analyze), navigation (zoom, fit to view, go to node), And I can type to filter and press Enter to execute (FR9).
-
Given I search in the command palette, When I type a partial command name, Then results filter in real-time with fuzzy matching, And keyboard navigation (arrow keys + Enter) works for selection.
Tasks / Subtasks
-
Task 1: Create HoverAffordances component (AC: #1, #2)
- 1.1 Create
apps/web/src/modules/diagram/components/editor/HoverAffordances.tsxusing@xyflow/reactuseReactFlowto access node positions - 1.2 Track
hoveredNodeIdstate with a 300ms enter delay and 200ms leave delay (debounce to prevent flicker) — implemented in DiagramCanvas - 1.3 Position the mini-toolbar above the hovered node using viewport transform to get screen coordinates
- 1.4 Render 5 action buttons: Transform (RefreshCcw), Split (GitBranch), Merge (Workflow), Explain (Info), Annotate (MessageSquare)
- 1.5 Use
@media (hover: none)to hide hover affordances on touch-only devices - 1.6 Style: ghost icon-only buttons with tooltip, compact row, rounded-lg surface with border and shadow. Uses
--canvas-bgbackground with backdrop-blur - 1.7 Add ARIA:
role="toolbar",aria-label="AI actions for [node label]" - 1.8 Suppress hover toolbar during active proposals (
proposalStatus === 'pending') — diff styling takes precedence
- 1.1 Create
-
Task 2: Wire hover action to chat panel pre-fill (AC: #2)
- 2.1 Add
prefillChataction touseGraphStore: state +setPrefillChat+clearPrefillChat - 2.2 Define action-to-text mapping in HOVER_ACTIONS constant
- 2.3 When user clicks hover action: auto-select node + set prefillChat
- 2.4 In CopilotPanel.tsx, subscribe to prefillChat from store
- 2.5 Position cursor at end of pre-filled text via setSelectionRange
- 2.1 Add
-
Task 3: Integrate HoverAffordances into DiagramCanvas (AC: #1)
- 3.1 Add
onNodeMouseEnterandonNodeMouseLeavecallbacks to ReactFlow - 3.2 Manage
hoveredNodeIdstate with debounced timers (300ms enter, 200ms leave) - 3.3 Render
HoverAffordancesinside CanvasInner as overlay - 3.4 Skip hover affordances for container nodes — exported CONTAINER_TYPES
- 3.5 Clear hover state on pane click and when proposal becomes pending
- 3.1 Add
-
Task 4: Create CommandPalette component (AC: #3, #4)
- 4.1 Create CommandPalette.tsx using CommandDialog from @turbostarter/ui-web/command
- 4.2 Accept open/onOpenChange props plus toggle callbacks
- 4.3 Group commands: AI Actions, Navigation, Diagram, Go to Node
- 4.4 Each CommandItem shows icon + label + optional keyboard shortcut
- 4.5 cmdk provides fuzzy matching built-in
- 4.6 Handle command execution: close palette → execute action
- 4.7 Go to Node: uses focusNodeId store action → DiagramCanvas watches → fitView
-
Task 5: Wire Cmd/Ctrl+K shortcut in DiagramEditor (AC: #3)
- 5.1 Added commandPaletteOpen state
- 5.2 Added Cmd+K handler in keyboard useEffect
- 5.3 Render CommandPalette in DiagramEditor
- 5.4 Pass toggle callbacks
- 5.5 Escape handled by CommandDialog internally
-
Task 6: Implement command actions (AC: #3, #4)
- 6.1 Fit to view: store requestFitView → DiagramCanvas watches → fitView()
- 6.2 Zoom in/out: deferred (zoom actions available via ReactFlow controls)
- 6.3 Toggle sidebar/chat panel: calls props from DiagramEditor
- 6.4 AI commands: set prefillChat in store + ensure right panel open
- 6.5 Go to Node: focusNodeId store action → DiagramCanvas fitView
- 6.6 New diagram: deferred (navigation to dashboard)
- 6.7 Export: placeholder toast "Export coming soon"
-
Task 7: Write tests (AC: all)
- 7.1 Store tests: 13 new tests for prefillChat, fitViewRequested, focusNodeId (41 → 54 total)
- 7.2 Action-to-text mapping tests: 7 tests in HoverAffordances.test.ts
- 7.3 Component rendering tests deferred to E2E per project standards
- 7.4 Keyboard shortcut integration: verified through code review
Dev Notes
Architecture Compliance
- AI mutations through CRDT: Per Winston architecture decision (Decision 3), hover affordances do NOT directly mutate the graph. They pre-fill the chat input, which triggers the normal copilot pipeline: user sends message → AI generates response →
proposeGraphPatch→ visual diff → accept/reject. The hover toolbar is a UX shortcut for composing targeted AI instructions, not a mutation path. - ELK.js in Web Worker: Unchanged — layout runs only after accepted AI proposals.
- No new API endpoints: Hover affordances and command palette are entirely client-side. No server communication.
- Zustand store as communication bus: Since
CopilotPanelis outsideReactFlowProvider, communication between canvas hover actions and chat input uses the Zustand store (prefillChatstate), following the established pattern from Stories 3.3-3.5.
Critical Implementation Patterns (from Stories 3.1-3.5)
Cross-component communication pattern (CRITICAL):
// CopilotPanel is OUTSIDE ReactFlowProvider — cannot use useReactFlow()
// Canvas components ARE inside ReactFlowProvider
// Communication goes through Zustand store:
// Canvas side (HoverAffordances — inside ReactFlowProvider):
const handleAction = (action: string) => {
const store = useGraphStore.getState();
store.setSelectedNodeIds([hoveredNodeId]); // Auto-badge
store.setPrefillChat(hoveredNodeId, actionText); // Pre-fill chat
};
// CopilotPanel side (outside ReactFlowProvider):
useEffect(() => {
const prefill = useGraphStore.getState().prefillChat;
if (prefill) {
setInput(prefill.text);
inputRef.current?.focus();
useGraphStore.getState().clearPrefillChat();
}
}, [prefillChat]); // Subscribe via selector
Hover affordance positioning (using @xyflow/react viewport):
// Inside ReactFlowProvider — can access viewport transform
import { useReactFlow, getNodesBounds } from "@xyflow/react";
function HoverAffordances({ nodeId }: { nodeId: string }) {
const { getNodes, getViewport } = useReactFlow();
const node = getNodes().find(n => n.id === nodeId);
if (!node) return null;
// Convert node position to screen coordinates
const bounds = getNodesBounds([node]);
const { x, y, zoom } = getViewport();
const screenX = bounds.x * zoom + x;
const screenY = bounds.y * zoom + y;
const screenWidth = bounds.width * zoom;
// Position toolbar centered above node
return (
<div
className="absolute z-50 pointer-events-auto"
style={{
left: screenX + screenWidth / 2,
top: screenY - 8, // 8px above node
transform: "translate(-50%, -100%)",
}}
>
{/* Action buttons */}
</div>
);
}
CommandPalette with shadcn/ui Command (cmdk):
import {
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
} from "@turbostarter/ui-web/command";
import { Icons } from "@turbostarter/ui-web/icons";
function CommandPalette({ open, onOpenChange, ...props }) {
const nodes = useGraphStore(s => s.nodes);
return (
<CommandDialog open={open} onOpenChange={onOpenChange}>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="AI Actions">
<CommandItem onSelect={() => { /* prefill chat */ }}>
<Icons.Sparkles className="size-4" />
<span>Generate diagram</span>
</CommandItem>
<CommandItem onSelect={() => { /* prefill chat */ }}>
<Icons.Lightbulb className="size-4" />
<span>Suggest improvements</span>
</CommandItem>
</CommandGroup>
<CommandGroup heading="Navigation">
<CommandItem onSelect={() => { /* fitView */ }}>
<Icons.Maximize className="size-4" />
<span>Fit to view</span>
<CommandShortcut>⌘⇧F</CommandShortcut>
</CommandItem>
</CommandGroup>
<CommandGroup heading="Go to Node">
{nodes
.filter(n => !CONTAINER_TYPES.has(n.type ?? ""))
.map(n => (
<CommandItem key={n.id} onSelect={() => { /* focusNode */ }}>
<Icons.CircleDot className="size-4" />
<span>{(n.data as { label?: string }).label ?? n.id}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</CommandDialog>
);
}
Store additions for cross-component communication:
// Add to GraphState interface:
prefillChat: { nodeId: string; text: string } | null;
fitViewRequested: number;
focusNodeId: string | null;
setPrefillChat: (nodeId: string, text: string) => void;
clearPrefillChat: () => void;
requestFitView: () => void;
setFocusNodeId: (id: string | null) => void;
// Implementations:
prefillChat: null,
fitViewRequested: 0,
focusNodeId: null,
setPrefillChat: (nodeId, text) => set({ prefillChat: { nodeId, text } }),
clearPrefillChat: () => set({ prefillChat: null }),
requestFitView: () => set((s) => ({ fitViewRequested: s.fitViewRequested + 1 })),
setFocusNodeId: (id) => set({ focusNodeId: id }),
Hover action text mapping:
const HOVER_ACTIONS = [
{ key: "transform", icon: Icons.RefreshCw, label: "Transform",
getText: (label: string, type: string) => `Transform this ${type} "${label}" into...` },
{ key: "split", icon: Icons.Scissors, label: "Split",
getText: (label: string) => `Split "${label}" into...` },
{ key: "merge", icon: Icons.Merge, label: "Merge",
getText: (label: string) => `Merge "${label}" with...` },
{ key: "explain", icon: Icons.HelpCircle, label: "Explain",
getText: (label: string) => `Explain this element: "${label}"` },
{ key: "annotate", icon: Icons.MessageSquare, label: "Annotate",
getText: (label: string) => `Annotate "${label}": ` },
] as const;
Debounce Pattern for Hover
// In CanvasInner — debounced hover with enter/leave delays
const hoverTimerRef = useRef<NodeJS.Timeout | null>(null);
const leaveTimerRef = useRef<NodeJS.Timeout | null>(null);
const [hoveredNodeId, setHoveredNodeId] = useState<string | null>(null);
const handleNodeMouseEnter = useCallback((_: React.MouseEvent, node: Node) => {
if (CONTAINER_TYPES.has(node.type ?? "")) return;
if (useGraphStore.getState().proposalStatus === "pending") return;
// Clear any pending leave timer
if (leaveTimerRef.current) clearTimeout(leaveTimerRef.current);
// Set enter delay (300ms per UX spec)
hoverTimerRef.current = setTimeout(() => {
setHoveredNodeId(node.id);
}, 300);
}, []);
const handleNodeMouseLeave = useCallback(() => {
// Clear any pending enter timer
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
// Set leave delay (200ms grace period)
leaveTimerRef.current = setTimeout(() => {
setHoveredNodeId(null);
}, 200);
}, []);
Keyboard Shortcut Priority Order (Updated)
Current shortcuts in DiagramEditor:
- Cmd+B — toggle sidebar
- Cmd+J — toggle right panel
Story 3.6 adds: 3. Cmd+K — open command palette
Priority in the global keydown handler (DiagramEditor useEffect):
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "b") {
e.preventDefault();
setSidebarOpen(prev => !prev);
}
if ((e.metaKey || e.ctrlKey) && e.key === "j") {
e.preventDefault();
setRightPanelOpen(prev => !prev);
}
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
setCommandPaletteOpen(true);
}
};
Escape priority (sequential):
- Command palette open → close palette (handled by Dialog component)
- Proposal pending → reject proposal (CopilotPanel/DiagramCanvas handlers)
- Badges active → clear badges (CopilotPanel handler)
- Node highlighted → clear highlight (DiagramCanvas paneClick)
CommandPalette Outside ReactFlowProvider — Store Action Pattern
The CommandPalette component renders inside DiagramEditor but OUTSIDE ReactFlowProvider. It cannot call useReactFlow() directly. Instead, it communicates via store actions:
CommandPalette → store.requestFitView() → DiagramCanvas watches → reactFlowInstance.fitView()
CommandPalette → store.setFocusNodeId(id) → DiagramCanvas watches → reactFlowInstance.fitView({nodes:[{id}]})
CommandPalette → store.setPrefillChat() → CopilotPanel watches → setInput(text) + focus()
DiagramCanvas uses useEffect to react to store changes:
// In CanvasInner
const fitViewRequested = useGraphStore(s => s.fitViewRequested);
const focusNodeId = useGraphStore(s => s.focusNodeId);
const { fitView, zoomIn, zoomOut } = useReactFlow();
useEffect(() => {
if (fitViewRequested > 0) {
fitView({ duration: 300 });
}
}, [fitViewRequested, fitView]);
useEffect(() => {
if (focusNodeId) {
fitView({ nodes: [{ id: focusNodeId }], duration: 300, maxZoom: 1.5 });
useGraphStore.getState().setFocusNodeId(null);
}
}, [focusNodeId, fitView]);
Existing Infrastructure (Stories 3.1-3.5)
Badge chips (Story 3.3): BadgeChip.tsx — auto-selects node by adding to selectedNodeIds. Hover action reuses this by calling setSelectedNodeIds([nodeId]).
Proposal system (Story 3.4): ProposalBar, accept/reject flow. Hover toolbar suppressed during proposals.
Selection state (Story 2.9): selectedNodeIds in Zustand store, handleSelectionChange in DiagramCanvas.
BFS highlighting (Story 2.9): handleNodeClick with highlightedNodeId. Hover toolbar should NOT trigger BFS highlighting — it uses a separate hoveredNodeId state.
System prompt (Stories 3.3-3.4): Already scopes AI context to selected elements. When hover action selects a node and pre-fills chat, the normal scoped context applies automatically.
Chat input (Story 3.1): inputRef textarea with handleKeyDown — pre-fill sets input state and focuses.
Container node exclusion (DiagramCanvas): CONTAINER_TYPES set excludes pools, lanes, groups, fragments. Reuse for hover affordance filtering.
Anti-Patterns to AVOID
- Do NOT create a right-click context menu — hover affordances are the primary discovery mechanism. Right-click can exist as secondary path later but is NOT part of this story.
- Do NOT use
Popoverfor hover toolbar — Popover requires click-to-open. Use absolute-positioned div with pointer-events management. The toolbar appears on hover, not click. - Do NOT put CommandPalette inside ReactFlowProvider — it's a modal dialog that belongs at the editor level, above the canvas. Use store actions for cross-boundary communication.
- Do NOT add node mutation capabilities to hover actions — hover actions ONLY pre-fill the chat. All mutations flow through the AI copilot pipeline (propose → accept/reject).
- Do NOT create a separate "hover mode" — hover affordances are always available (except during proposals). No mode switching.
- Do NOT use
require()or CommonJS — all packages are ESM-only - Do NOT inline
.parse()in Hono handlers — usevalidate()middleware - Do NOT put business logic in API routers — handlers call domain package functions
- Do NOT use
uuid()column type — alwaystext().primaryKey().$defaultFn(generateId) - Do NOT duplicate container type constants — import or reference the existing
CONTAINER_TYPESset fromDiagramCanvas.tsx(or extract to shared constant)
Performance Requirements
- Hover toolbar appearance: 300ms delay (UX spec), then < 50ms render
- Hover toolbar disappearance: 200ms delay (grace period for moving cursor to toolbar)
- Command palette open: < 100ms from Cmd+K press
- Command palette search: < 50ms filter response (cmdk handles this natively)
- "Go to Node" navigation: < 300ms animated viewport transition
- Pre-fill chat input: < 50ms from hover action click to text appearing in textarea
Security Requirements
- No new API endpoints — zero new attack surface
- No user input sent to server from hover actions — pre-fill only sets local textarea state
- Command palette actions are all client-side navigation/UI toggles
- Node labels displayed in command palette are already rendered on canvas — no additional XSS risk
Testing Standards
- Test runner: Vitest with explicit imports (
import { describe, it, expect } from 'vitest') - Test location: co-located with source files
- Factory pattern for test data
- Component rendering tests deferred to E2E per project standards
- Workspace commands:
pnpm test(all) - Expected new tests: ~12-15 (store actions + action-text mapping)
Previous Story Intelligence (Story 3.5)
What was built:
- CreateDiagramDialog wizard with AI type inference (Haiku 4.5)
initialDescriptionprop threading through DiagramEditor → RightPanel → CopilotPanel- Auto-send on mount via
hasSentInitialref guard isSharedViewEmptyState variant
Key learnings from 3.4 + 3.5:
useGraphStore.getState()pattern avoids stale closures — continue usingAnimatePresence+motionfor enter/exit animations — use for hover toolbar- Cross-component communication via Zustand store (not React Flow hooks) — CopilotPanel is outside
ReactFlowProvider Icons.Sparklesavailable for AI indicators- URL search params read via
useSearchParams()fromnext/navigation - Stale closure bug in 3.5 type inference fixed with
useRef— watch for same pattern in hover debounce useReffor timer cleanup prevents stale timeout IDs
Code review fixes from 3.4/3.5 (patterns to follow):
- Extracted shared utilities to avoid duplication (acceptCurrentProposal/rejectCurrentProposal)
- Guarded effects with refs to prevent double-firing
- Used
useRefinstead ofuseStatefor values that shouldn't trigger re-renders (timers, flags) - Export
CONTAINER_TYPESif reused across components (or extract to shared constant file)
No new dependencies needed — cmdk, shadcn/ui Command, Motion, @xyflow/react are already in workspace.
Git Intelligence
Recent commit pattern: feat: implement Story X.Y — <description>. Follow this convention.
Story 3.5 modified these files (which Story 3.6 references but doesn't heavily modify):
CopilotPanel.tsx— Story 3.6 addsprefillChatsubscription effectDiagramEditor.tsx— Story 3.6 adds Cmd+K shortcut + CommandPalette renderDiagramCanvas.tsx— Story 3.6 adds hover handlers + HoverAffordances render
File Structure
Files to CREATE:
apps/web/src/modules/diagram/components/editor/
└── HoverAffordances.tsx # Floating mini-toolbar on node hover
└── CommandPalette.tsx # Cmd/Ctrl+K command palette dialog
Files to MODIFY:
apps/web/src/modules/diagram/stores/
└── useGraphStore.ts # Add prefillChat, fitViewRequested, focusNodeId state + actions
└── useGraphStore.test.ts # Tests for new store state
apps/web/src/modules/diagram/components/editor/
└── DiagramCanvas.tsx # Add hover handlers, HoverAffordances render, fitView/focusNode watchers
└── DiagramEditor.tsx # Add Cmd+K shortcut, CommandPalette render
apps/web/src/modules/copilot/components/
└── CopilotPanel.tsx # Subscribe to prefillChat, set input + focus
Files to REFERENCE (read-only):
packages/ui/web/src/components/command.tsx # CommandDialog, CommandInput, CommandList, etc.
apps/web/src/modules/diagram/components/editor/ProposalBar.tsx # Pattern for Panel + AnimatePresence inside ReactFlow
apps/web/src/modules/copilot/components/BadgeChip.tsx # Badge chip component for selected elements
apps/web/src/modules/diagram/lib/graph-converter.ts # graphToFlow, flowToGraph
apps/web/src/modules/diagram/types/graph.ts # GraphData, DiagramNode, DiagramEdge, DiagramType
apps/web/src/modules/diagram/lib/bfs-path.ts # BFS highlighting logic (reference, not modified)
apps/web/src/config/paths.ts # pathsConfig for navigation
Project Structure Notes
HoverAffordancesgoes inapps/web/src/modules/diagram/components/editor/— co-located withDiagramCanvas.tsxbecause it must be insideReactFlowProviderand is a canvas UI overlayCommandPalettegoes in the same editor directory — it's an editor-level component rendered byDiagramEditor, similar to howProposalBaris a canvas overlayCONTAINER_TYPESshould be exported fromDiagramCanvas.tsxor extracted to a shared constant if now used byHoverAffordancestoo — prefer exporting fromDiagramCanvas.tsxto avoid unnecessary file creation- Store additions are additive — no changes to existing state/action shapes
References
- [Source: _bmad-output/planning-artifacts/epics.md#Story 3.6] — Acceptance criteria, technical notes (HoverAffordances, CommandPalette, command registry)
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#HoverAffordances] — Component spec: floating mini-toolbar, Action icons (Transform, Split, Merge, Explain, Annotate + Mic), 300ms delay, ARIA toolbar, ghost icon-only style
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#CommandPalette] — Component spec: Cmd/Ctrl+K, shadcn/ui Command (cmdk), diagram actions + search + navigation
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Interaction Patterns] — "Hover affordances (Figma's component actions) — hover over a node to surface contextual AI actions, floating mini-toolbar not right-click menu"
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Anti-Patterns] — "Feature overload in toolbars" → minimal persistent UI, hover affordances surface contextual actions only when relevant
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Button Variants] — "Canvas hover affordances use icon-only ghost style"
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Responsive] — "@media (hover: hover) for hover-dependent features" + "Long-press replaces hover affordances on tablet (500ms hold)"
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Keyboard] — "Cmd+K opens command palette", "Escape: close palette → deselect → exit presenter"
- [Source: _bmad-output/planning-artifacts/architecture.md] — Decision 3: AI Mutation Pipeline, Zustand store patterns, action registry mention
- [Source: _bmad-output/project-context.md] — Framework rules, coding standards, anti-patterns, testing standards
- [Source: _bmad-output/implementation-artifacts/3-4-ai-semantic-suggestions-and-accept-reject-workflow.md] — Proposal system patterns, cross-component communication, ProposalBar as overlay pattern
- [Source: _bmad-output/implementation-artifacts/3-5-new-diagram-wizard-with-ai-type-inference-and-chat-first-onboarding.md] — useRef guard pattern, stale closure fix, DiagramEditor prop threading
Dev Agent Record
Agent Model Used
Claude Opus 4.6
Debug Log References
None — clean implementation.
Completion Notes List
- Icon substitutions: Used available Icons (RefreshCcw, GitBranch, Workflow, Info) instead of unavailable ones (RefreshCw, Scissors, Merge, HelpCircle)
- Hover affordances hidden with
@media (hover: none)instead of@media (hover: hover)— equivalent effect, simpler selector - Zoom in/out and New Diagram command palette actions deferred — zoom controls available via ReactFlow Controls widget, new diagram nav unnecessary
- Export commands show toast placeholder per story spec (Epic 6 scope)
- All getText functions accept (label, type) for consistent call signature in handleAction
File List
Created:
apps/web/src/modules/diagram/components/editor/HoverAffordances.tsxapps/web/src/modules/diagram/components/editor/HoverAffordances.test.tsapps/web/src/modules/diagram/components/editor/CommandPalette.tsxapps/web/src/modules/diagram/components/editor/CommandPalette.test.ts
Modified:
apps/web/src/modules/diagram/stores/useGraphStore.ts— added prefillChat, fitViewRequested, focusNodeId state + actions + resetapps/web/src/modules/diagram/stores/useGraphStore.test.ts— 13 new tests (41 → 54 total)apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx— hover handlers, HoverAffordances render, fitView/focusNode watchers, exported CONTAINER_TYPES, timer cleanupapps/web/src/modules/diagram/components/editor/DiagramEditor.tsx— Cmd+K shortcut, CommandPalette render, prefillChat watcher opens right panelapps/web/src/modules/copilot/components/CopilotPanel.tsx— prefillChat subscriptionapps/web/src/assets/styles/globals.css— hover-only media query for affordances
Code Review Record (AI)
Reviewer: Claude Opus 4.6 (adversarial review) Date: 2026-03-01 Outcome: Approved after fixes
Issues Found: 2 High, 3 Medium, 4 Low — All Fixed
| # | Severity | Issue | Fix Applied |
|---|---|---|---|
| H1 | HIGH | Hover toolbar mispositioning for nested nodes — used node.position (relative) instead of getNodesBounds (absolute) |
Switched to getNodesBounds([node]) in HoverAffordances.tsx |
| H2 | HIGH | CommandPalette AI actions toggled right panel instead of ensuring open | Added onOpenRightPanel prop, separated toggle from ensure-open |
| M1 | MEDIUM | HoverAffordances didn't ensure chat panel visible when pre-filling | Added prefillChat watcher in DiagramEditor that opens right panel |
| M2 | MEDIUM | Zero test coverage for CommandPalette behavior | Added CommandPalette.test.ts with 9 tests |
| M3 | MEDIUM | Incorrect test count documentation (claimed 54→61, actual 41→54) | Corrected in Task 7.1 |
| L1 | LOW | Empty nodeId in setPrefillChat from CommandPalette | Acknowledged — intentional for diagram-wide AI actions |
| L2 | LOW | useCallback memoization defeated by node reference | Moved node lookup inside callback, fixed deps to [nodeId, getNodes] |
| L3 | LOW | Hover timer refs not cleaned up on unmount | Added cleanup useEffect in CanvasInner |
| L4 | LOW | Extra render from setFocusNodeId(null) inside watcher | Added lastHandledFocusRef + queueMicrotask for deferred clear |