Skip to content

refactor(cue): full SRP decomposition — engine modules, renderer components, shared types#599

Merged
reachraza merged 3 commits intorcfrom
cue-polish
Mar 20, 2026
Merged

refactor(cue): full SRP decomposition — engine modules, renderer components, shared types#599
reachraza merged 3 commits intorcfrom
cue-polish

Conversation

@reachraza
Copy link
Contributor

@reachraza reachraza commented Mar 20, 2026

Summary

  • Decompose the Cue codebase from 3 monolithic files into single-responsibility modules with clear boundaries for future feature expansion
  • Main process: cue-engine.ts 1,583→719 lines (−55%) via 5 extracted modules (activity log, heartbeat, subscription setup, fan-in tracker, run manager)
  • Renderer: 3 large components decomposed into directory-based modules:
    • NodeConfigPanel 850→249 lines (−71%) — TriggerConfig, AgentConfigPanel, EdgePromptRow
    • CueModal 1,029→490 lines (−52%) — SessionsTable, ActiveRunsList, ActivityLog, ActivityLogDetail, StatusDot, cueModalUtils
    • CueYamlEditor 691→284 lines (−59%) — PatternPicker, PatternPreviewModal, CueAiChat, YamlTextEditor, useCueAiChat hook
  • Pipeline editor: CuePipelineEditor 1,608→607 lines (−62%) — usePipelineState, usePipelineSelection, usePipelineLayout hooks + PipelineToolbar, PipelineCanvas, PipelineContextMenu components
  • 9 cross-cutting fixes: shared CUE_COLOR constant, createCueEvent factory (12 call sites), useCue error state, engine start hardening, IncomingTriggerEdgeInfo/CuePipelineSessionInfo type consolidation, cueEventConstants.ts single source, CUE_YAML_TEMPLATE extraction
  • Bug fixes: chain depth propagation, fan-in routing, scheduled fire key scoping, heartbeat initial fire, concurrency slot freeing, layout restore race conditions
  • All barrel exports preserved — zero breaking import changes

Test plan

  • npm run lint - clean
  • npm run lint:eslint - clean
  • npm run test - 23,649 tests passed
  • Manual: Open Cue Modal → Dashboard tab loads with sessions, active runs, activity log
  • Manual: Toggle master Enable/Disable switch
  • Manual: "Run Now" triggers subscription, appears in active runs; "Stop" removes it
  • Manual: "Edit YAML" opens YAML editor; "View in Pipeline" switches tab
  • Manual: Activity log entries expand to show execution detail
  • Manual: Select trigger node → config fields match event type (heartbeat/scheduled/file/github/task)
  • Manual: Select agent node → input/output prompt textareas; multi-trigger shows per-edge prompts
  • Manual: Right-click node → context menu (Configure, Duplicate, Delete)
  • Manual: YAML editor: line numbers, Tab indentation, validation, pattern presets, AI chat
  • Manual: Save pipeline → writes YAML + prompt files, refreshes sessions
  • Manual: Escape closes modals with unsaved-changes confirmation when dirty
  • Manual: Pipeline legend dots and minimap colors are consistent

Summary by CodeRabbit

  • New Features

    • AI-assisted YAML editor with chat, pattern picker, and preview for Cue configuration.
    • Redesigned Cue modal: dashboard, pipeline editor, active runs list, sessions table, and detailed activity log.
    • Pipeline editor improvements: agent/trigger config panels, per-edge prompt editor, consistent event icons/colors.
  • Bug Fixes

    • Engine startup now aborts and logs errors on database initialization failures.
    • Hook error state handling improved with clearer, recoverable error messages.
  • Tests

    • Expanded test coverage for Cue engine lifecycle, event creation, and hook error scenarios.

… factory, useCue error state, engine start hardening

- Consolidate CUE_TEAL/#06b6d4 to shared CUE_COLOR constant in cue-pipeline-types.ts;
  remove 3 independent declarations, replace inline hex in NodeConfigPanel
- Extract createCueEvent(type, triggerName, payload) factory in cue-types.ts;
  convert 12 of 14 event creation sites across 8 files, remove redundant crypto imports
- Add error state to useCue hook with proper catch block; surface errors in
  CueModal dashboard via dismissible banner with Retry button
- Harden cue-engine.start(): move enabled=true after successful DB init,
  return early on failure instead of continuing in broken state
- 18 new tests: 8 createCueEvent factory, 5 useCue error state, 5 engine start hardening
- Fix cue-concurrency and cue-sleep-wake test isolation (add cue-db mocks, mockReset)
*** NodeConfigPanel (850→249 lines, −71%)
- Extract TriggerConfig switch to panels/triggers/TriggerConfig.tsx (247 lines)
- Extract AgentConfigPanel.tsx (291 lines) — prompt editing, multi-trigger modes
- Extract EdgePromptRow.tsx (78 lines) — per-edge prompt with debounce
- Shared triggerConfigStyles.ts for input/label/select styles
- Shell retains: header chrome, type routing, expand/collapse, delete

*** CueModal (1,029→490 lines, −52%)
- Create CueModal/ directory with barrel index.ts
- Extract SessionsTable.tsx (167 lines) — sessions table with pipeline dots
- Extract ActiveRunsList.tsx (81 lines) — running tasks with stop controls
- Extract ActivityLog.tsx (128 lines) — expandable history with load-more
- Extract ActivityLogDetail.tsx (137 lines) — execution detail view
- Extract StatusDot.tsx (17 lines) — StatusDot + PipelineDot micro-components
- Extract cueModalUtils.ts (70 lines) — formatting + pipeline mapping helpers

*** CueYamlEditor (691→284 lines, −59%)
- Create CueYamlEditor/ directory with barrel index.ts
- Extract PatternPicker.tsx (42 lines) — pattern preset grid
- Extract PatternPreviewModal.tsx (81 lines) — pattern detail with copy
- Extract CueAiChat.tsx (114 lines) — chat history + input + streaming
- Extract YamlTextEditor.tsx (107 lines) — textarea with line numbers + validation
- Extract useCueAiChat hook (197 lines) — agent spawning, streaming, cleanup
- Extract CUE_YAML_TEMPLATE to constants/cueYamlDefaults.ts

Cross-cutting consolidation (CC-5 through CC-9):
- CC-5: Consolidate IncomingTriggerEdgeInfo to shared/cue-pipeline-types.ts
- CC-6: Consolidate SessionInfo → CuePipelineSessionInfo in shared types
- CC-7: Extract cueEventConstants.ts — EVENT_ICONS/LABELS/COLORS single source
- CC-8: Replace 5 hardcoded #06b6d4 with CUE_COLOR in Cue-specific contexts
- CC-9: Extract CUE_YAML_TEMPLATE to constants/cueYamlDefaults.ts

All barrel exports preserved for backwards compatibility.
Type check: 0 errors. Tests: 593 files, 23,649 passed, 0 failures.
@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

Centralizes Cue event creation via a new createCueEvent factory, replaces inline event construction across Cue modules, refactors large Cue UI components into focused submodules, introduces shared cue-pipeline types/constants, improves DB initialization error handling in the engine, and adds error state handling and AI-chat support in renderer hooks/components.

Changes

Cohort / File(s) Summary
Event Factory & Producers
src/main/cue/cue-types.ts, src/main/cue/cue-engine.ts, src/main/cue/cue-fan-in-tracker.ts, src/main/cue/cue-file-watcher.ts, src/main/cue/cue-github-poller.ts, src/main/cue/cue-reconciler.ts, src/main/cue/cue-subscription-setup.ts, src/main/cue/cue-task-scanner.ts
Added createCueEvent(...) in cue-types.ts; replaced inline id/timestamp/event literal construction with createCueEvent calls across Cue producers, removing local crypto usages.
Engine DB Init & Tests
src/main/cue/cue-engine.ts, src/__tests__/main/cue/cue-engine.test.ts, src/__tests__/main/cue/cue-concurrency.test.ts, src/__tests__/main/cue/cue-sleep-wake.test.ts
CueEngine.start() now aborts on DB init/prune failures, logs at 'error' and calls captureException; tests updated/added to mock DB init failures and retry behavior; concurrency test DB mock added.
Cue UI: Modal Split
src/renderer/components/CueModal.tsx (removed), src/renderer/components/CueModal/*
Removed monolithic CueModal file; added modular components and utils (CueModal.tsx, SessionsTable.tsx, ActiveRunsList.tsx, ActivityLog*.tsx, StatusDot.tsx, cueModalUtils.ts, index.ts).
Cue UI: YAML Editor Split & AI Chat
src/renderer/components/CueYamlEditor.tsx (removed), src/renderer/components/CueYamlEditor/*, src/renderer/hooks/cue/useCueAiChat.ts
Removed monolithic YAML editor file; added CueYamlEditor and subcomponents (CueAiChat, YamlTextEditor, PatternPicker, PatternPreviewModal) plus useCueAiChat hook for AI-assisted editing and streaming agent integration.
Pipeline Editor Componentization
src/renderer/components/CuePipelineEditor/panels/NodeConfigPanel.tsx, .../AgentConfigPanel.tsx, .../EdgePromptRow.tsx, .../triggers/TriggerConfig.tsx, .../triggers/*
Extracted trigger/agent/edge prompt logic into TriggerConfig, AgentConfigPanel, EdgePromptRow and styling/constants; NodeConfigPanel simplified to dispatch to these components.
Event Presentation Constants
src/renderer/components/CuePipelineEditor/cueEventConstants.ts, src/renderer/components/CuePipelineEditor/..., src/renderer/components/CuePipelineEditor/PipelineCanvas.tsx
Added centralized EVENT_ICONS, EVENT_LABELS, EVENT_COLORS and updated components to import them; removed duplicated in-file maps.
Shared Types & Color
src/shared/cue-pipeline-types.ts, src/renderer/components/History/historyConstants.tsx
Added CUE_COLOR, CuePipelineSessionInfo, and IncomingTriggerEdgeInfo to shared types; updated consumers to import these shared definitions.
Hook & Renderer Changes
src/renderer/hooks/useCue.ts, src/__tests__/renderer/hooks/useCue.test.ts, src/renderer/hooks/cue/*
Added `error: string
Cue Event Factory Tests
src/__tests__/main/cue/cue-event-factory.test.ts
Added tests validating createCueEvent shape, timestamp format, UUID format, payload defaults, and uniqueness.
Misc UI & Small Fixes
src/renderer/components/CuePipelineEditor/.../TriggerDrawer.tsx, TriggerNode.tsx, PipelineEdge.tsx, and related files
Switched hardcoded color/icon fallbacks to shared constants (EVENT_*, CUE_COLOR), minor prop/type adjustments and import reorders.

Sequence Diagram(s)

sequenceDiagram
    participant Module as Cue Module
    participant Factory as createCueEvent
    participant Crypto as crypto
    participant Time as Date
    participant Consumer as Event Consumer / Dispatcher

    Module->>Factory: request createCueEvent(type, triggerName, payload)
    activate Factory
    Factory->>Crypto: crypto.randomUUID()
    Crypto-->>Factory: uuid
    Factory->>Time: new Date().toISOString()
    Time-->>Factory: isoTimestamp
    Factory-->>Module: CueEvent { id, timestamp, type, triggerName, payload }
    deactivate Factory
    Module->>Consumer: onEvent(event) / dispatch(event)
    Consumer-->>Consumer: handle/store/process event
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 A tiny hop, a little tweak,
Events now born from one small peak.
Modals trimmed into neat small stacks,
Types shared wide along the tracks.
The cue sings clearer — neat and sweet. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: a major refactor decomposing the Cue codebase following the Single Responsibility Principle across engine, renderer, and shared modules.

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

✨ 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 cue-polish
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link

greptile-apps bot commented Mar 20, 2026

Greptile Summary

This PR decomposes three large monolithic Cue files (cue-engine.ts, CueModal.tsx, CueYamlEditor.tsx, NodeConfigPanel.tsx, and CuePipelineEditor) into smaller single-responsibility modules while applying nine cross-cutting fixes including engine start hardening, a createCueEvent factory, useCue error state, shared CUE_COLOR constant, and consolidated types in the shared package.

Key changes:

  • createCueEvent factory in cue-types.ts replaces 12 inline CueEvent object literals across all watchers/pollers — reduces duplication and centralises UUID/timestamp generation
  • Engine start() now aborts with return and logs at error level when DB initialisation throws, preventing the engine from running without a database; previously it continued with a warn and no guard
  • useCue hook now surfaces error: string | null so the dashboard can show a retry banner instead of silently swallowing IPC failures
  • IncomingTriggerEdgeInfo and CuePipelineSessionInfo moved to shared/cue-pipeline-types.ts, eliminating duplicate local interface definitions across four files
  • CUE_COLOR constant unified in shared/cue-pipeline-types.ts and re-exported from historyConstants.tsx for backward compatibility
  • Bug found: ActivityLog.tsx checks entry.event.payload?.file for file.changed events but the payload field is named filename (set by cue-file-watcher.ts), so the filename annotation in the activity log is always suppressed
  • Potential issue: useCueAiChat.ts tells the AI agent the YAML config lives at ${projectRoot}/.maestro/cue.yaml, but the main-process constant CUE_YAML_FILENAME is 'maestro-cue.yaml' (project root, no subdirectory); if these resolve to different paths the AI will edit the wrong file
  • All barrel exports preserved, so no breaking import changes for consumers

Confidence Score: 3/5

  • Safe to merge after fixing the payload field name bug in ActivityLog and confirming the AI YAML path matches the IPC resolver.
  • The decomposition itself is faithful — logic is moved rather than changed, barrel exports are preserved, and test coverage is added for the new factory and engine hardening. Two issues prevent a higher score: (1) a confirmed display bug where file.changed activity log entries never show the filename because the wrong payload key is checked, and (2) a likely path mismatch between what useCueAiChat tells the AI agent (${projectRoot}/.maestro/cue.yaml) and the actual file path used by the IPC read/write handlers (CUE_YAML_FILENAME = 'maestro-cue.yaml'), which would cause the AI assistant to silently fail to edit the config file.
  • src/renderer/components/CueModal/ActivityLog.tsx (payload field name bug) and src/renderer/hooks/cue/useCueAiChat.ts (YAML path consistency with IPC handlers).

Important Files Changed

Filename Overview
src/renderer/components/CueModal/ActivityLog.tsx New component extracted from the monolithic CueModal; contains a payload field name bug (payload?.file instead of payload?.filename) that silently suppresses the filename annotation in all file.changed log entries.
src/main/cue/cue-engine.ts Engine start hardening: DB init now returns early on failure (engine stays disabled) rather than continuing to run without a DB. Start-failure tests added. Clean refactoring with createCueEvent factory.
src/main/cue/cue-types.ts Adds createCueEvent factory function, consolidating UUID generation and timestamping in one place. Minor style note: import * as crypto appears before the file-level JSDoc comment.
src/renderer/hooks/cue/useCueAiChat.ts New hook extracting AI-chat logic from CueYamlEditor. Listener lifecycle and cleanup look correct, but the hardcoded YAML path (.maestro/cue.yaml) may not match the actual file location used by the IPC layer.
src/renderer/hooks/cue/usePipelineState.ts Type consolidation: local SessionInfo interface replaced with CuePipelineSessionInfo from shared package. Has an unusual export-before-import ordering for the re-exported type alias.
src/renderer/components/CueModal/CueModal.tsx Well-decomposed modal shell; correctly consumes the new error state from useCue and surfaces a retry banner. Unsaved-changes guard and layer-stack registration look correct.
src/shared/cue-pipeline-types.ts Good consolidation: adds CUE_COLOR constant, CuePipelineSessionInfo, and IncomingTriggerEdgeInfo to the shared package, eliminating duplicate local definitions across hooks and panels.
src/renderer/hooks/useCue.ts Adds `error: string
src/renderer/components/CuePipelineEditor/panels/AgentConfigPanel.tsx Clean extraction of AgentConfig into a named exportable component. Imports IncomingTriggerEdgeInfo indirectly via NodeConfigPanel re-export rather than directly from the shared package.
src/tests/main/cue/cue-event-factory.test.ts New unit tests for createCueEvent factory; covers UUID uniqueness, timestamp validity, payload defaults, and field assignments. Comprehensive coverage.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph MainProcess["Main Process"]
        CT["cue-types.ts\n(createCueEvent factory)"]
        CE["cue-engine.ts\n(start hardening)"]
        CF["cue-file-watcher.ts"]
        CG["cue-github-poller.ts"]
        CS["cue-subscription-setup.ts"]
        CR["cue-reconciler.ts"]
        CTS["cue-task-scanner.ts"]
        CFI["cue-fan-in-tracker.ts"]
        CT --> CF
        CT --> CG
        CT --> CS
        CT --> CR
        CT --> CTS
        CT --> CFI
        CT --> CE
    end

    subgraph Shared["shared/cue-pipeline-types.ts"]
        CUE_COLOR["CUE_COLOR constant"]
        CPSI["CuePipelineSessionInfo"]
        ITEI["IncomingTriggerEdgeInfo"]
    end

    subgraph RendererHooks["Renderer Hooks"]
        UC["useCue.ts\n(+ error state)"]
        UPST["usePipelineState.ts"]
        UPSL["usePipelineSelection.ts"]
        UPLA["usePipelineLayout.ts"]
        UCAI["useCueAiChat.ts"]
        CPSI --> UPST
        CPSI --> UPLA
        CUE_COLOR --> UPSL
        ITEI --> UPSL
    end

    subgraph CueModalDir["CueModal/"]
        CM["CueModal.tsx"]
        AT["ActivityLog.tsx"]
        AD["ActivityLogDetail.tsx"]
        ARL["ActiveRunsList.tsx"]
        ST["SessionsTable.tsx"]
        CMU["cueModalUtils.ts"]
        UC --> CM
        CM --> AT
        CM --> ARL
        CM --> ST
        AT --> AD
    end

    subgraph CueYamlDir["CueYamlEditor/"]
        CYE["CueYamlEditor.tsx"]
        CAC["CueAiChat.tsx"]
        YTE["YamlTextEditor.tsx"]
        PP["PatternPicker.tsx"]
        PPM["PatternPreviewModal.tsx"]
        UCAI --> CYE
        CYE --> CAC
        CYE --> YTE
        CYE --> PP
        CYE --> PPM
    end

    Shared --> CueModalDir
    Shared --> CueYamlDir
Loading

Comments Outside Diff (1)

  1. src/renderer/components/CueModal/ActivityLog.tsx, line 132-135 (link)

    P1 Wrong payload field name for file.changed events

    entry.event.payload?.file does not exist on file.changed events. The createCueEvent call in cue-file-watcher.ts populates the payload with filename (just the basename) and path (the full absolute path) — there is no file key. As a result, the filePayload annotation is always an empty string for every file.changed activity log entry, silently dropping the filename hint the UI is supposed to show.

Last reviewed commit: "refactor(cue): decom..."

Comment on lines +30 to +31
export type { CuePipelineSessionInfo as SessionInfo } from '../../../shared/cue-pipeline-types';
import type { CuePipelineSessionInfo as SessionInfo } from '../../../shared/cue-pipeline-types';
Copy link

Choose a reason for hiding this comment

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

P2 Duplicate export-then-import of the same identifier

Both lines reference the same alias SessionInfo from the same source module. The export type re-exports the type for downstream consumers, but the immediately following import type is required for internal use within this file. Having export type appear before its import type is non-standard and confusing — TypeScript resolves both, but some bundlers and tooling can misread the order. The conventional pattern is to import first, then re-export:

Suggested change
export type { CuePipelineSessionInfo as SessionInfo } from '../../../shared/cue-pipeline-types';
import type { CuePipelineSessionInfo as SessionInfo } from '../../../shared/cue-pipeline-types';
import type { CuePipelineSessionInfo as SessionInfo } from '../../../shared/cue-pipeline-types';
export type { CuePipelineSessionInfo as SessionInfo } from '../../../shared/cue-pipeline-types';

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


interface AgentConfigPanelProps {
node: PipelineNode;
pipelines: CuePipeline[];
Copy link

Choose a reason for hiding this comment

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

P2 Indirect import of IncomingTriggerEdgeInfo through a re-exporting sibling

AgentConfigPanel imports IncomingTriggerEdgeInfo from ./NodeConfigPanel, which in turn merely re-exports it from ../../../../shared/cue-pipeline-types. This creates an unnecessary indirection: AgentConfigPanel → NodeConfigPanel → shared/cue-pipeline-types. The same pattern exists in EdgePromptRow.tsx (line 12). Both files should import directly from the shared package:

Suggested change
pipelines: CuePipeline[];
import type { IncomingTriggerEdgeInfo } from '../../../../shared/cue-pipeline-types';

EdgePromptRow.tsx (line 12) has the same issue and should apply the same fix.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

: text;

try {
const spawnConfig = await buildSpawnConfigForAgent({
Copy link

Choose a reason for hiding this comment

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

P1 AI prompt hardcodes .maestro/cue.yaml — may not match the actual file path

The path given to the AI agent is assembled as ${projectRoot}/.maestro/cue.yaml, while the main-process constant is CUE_YAML_FILENAME = 'maestro-cue.yaml' (a file placed directly in the project root, not in a .maestro/ subdirectory). If these are the same config file, the AI is being told the wrong path and will fail to read or edit the YAML it is supposed to modify. If they are different files, this is still a hardcoded string that could drift. It should reference the same path helper that readYaml/writeYaml IPC calls resolve to, or at minimum be derived from a shared constant.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (11)
src/renderer/components/CueYamlEditor/PatternPreviewModal.tsx (1)

22-26: Potential state update after unmount.

The setTimeout in handleCopy could trigger a state update after the component unmounts if the user closes the modal within 2 seconds of copying. Consider cleaning up the timeout.

🔧 Proposed fix using useRef for cleanup
-import { useState, useCallback } from 'react';
+import { useState, useCallback, useRef, useEffect } from 'react';
 import { Copy, Check } from 'lucide-react';
 // ... other imports ...

 export function PatternPreviewModal({ pattern, theme, onClose }: PatternPreviewModalProps) {
 	const [copied, setCopied] = useState(false);
+	const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
+
+	useEffect(() => {
+		return () => {
+			if (timeoutRef.current) clearTimeout(timeoutRef.current);
+		};
+	}, []);

 	const handleCopy = useCallback(async () => {
 		await navigator.clipboard.writeText(pattern.yaml);
 		setCopied(true);
-		setTimeout(() => setCopied(false), 2000);
+		timeoutRef.current = setTimeout(() => setCopied(false), 2000);
 	}, [pattern.yaml]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/CueYamlEditor/PatternPreviewModal.tsx` around lines
22 - 26, The setTimeout in handleCopy (which writes pattern.yaml to clipboard
and calls setCopied) can call setCopied after the modal unmounts; store the
timer id in a ref (e.g., copyTimeoutRef), assign the timeout return value to it
inside handleCopy, and add a useEffect cleanup that clears the timeout
(clearTimeout(copyTimeoutRef.current)) on unmount to prevent state updates after
unmount; ensure handleCopy and the cleanup reference the same ref and that the
ref is cleared/reset after the timeout completes.
src/renderer/components/CuePipelineEditor/panels/triggers/TriggerConfig.tsx (1)

31-39: Type-unsafe callback pattern with unknown[] args.

The useDebouncedCallback usage casts args[0] which bypasses type safety. If the hook's API allows typed parameters, consider using them. However, this pattern is functional and isolated to this component.

💡 Alternative: Inline debounced functions

If useDebouncedCallback supports typed callbacks, consider:

-	const { debouncedCallback: debouncedUpdate } = useDebouncedCallback((...args: unknown[]) => {
-		const config = args[0] as TriggerNodeData['config'];
-		onUpdateNode(node.id, { config } as Partial<TriggerNodeData>);
-	}, 300);
+	const { debouncedCallback: debouncedUpdate } = useDebouncedCallback(
+		(config: TriggerNodeData['config']) => {
+			onUpdateNode(node.id, { config });
+		},
+		300
+	);

This would require verifying useDebouncedCallback's type signature supports this pattern.

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

In `@src/renderer/components/CuePipelineEditor/panels/triggers/TriggerConfig.tsx`
around lines 31 - 39, The callbacks passed to useDebouncedCallback are using an
unsafe unknown[] signature and then casting args[0]; update debouncedUpdate and
debouncedUpdateLabel to use a typed callback parameter (e.g., provide a typed
function signature or generic to useDebouncedCallback) so you directly accept
(config: TriggerNodeData['config']) and (customLabel?: string) respectively,
then call onUpdateNode(node.id, { config }) and onUpdateNode(node.id, {
customLabel }) without casts; reference the debouncedUpdate,
debouncedUpdateLabel, useDebouncedCallback, onUpdateNode and TriggerNodeData
symbols when making the change.
src/renderer/components/CueYamlEditor/YamlTextEditor.tsx (1)

25-25: Unused ref declaration.

yamlTextareaRef is declared but never used. The Tab key handler accesses the textarea via e.currentTarget instead. Consider removing this unused ref.

🔧 Proposed fix
-import { useCallback, useRef } from 'react';
+import { useCallback } from 'react';
 import type { Theme } from '../../types';
 
 // ... props interface ...
 
 export function YamlTextEditor({
 	// ... destructured props ...
 }: YamlTextEditorProps) {
-	const yamlTextareaRef = useRef<HTMLTextAreaElement>(null);
 
 	// Handle Tab key in textarea for indentation

And remove the ref attribute from the textarea at line 79:

 				<textarea
-					ref={yamlTextareaRef}
 					value={yamlContent}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/CueYamlEditor/YamlTextEditor.tsx` at line 25, Remove
the unused ref declaration yamlTextareaRef from YamlTextEditor and delete the
ref attribute on the textarea element; the Tab key handler already uses
e.currentTarget to access the textarea, so delete the const yamlTextareaRef =
useRef<HTMLTextAreaElement>(null) and the corresponding ref usage to avoid dead
code and unused imports.
src/renderer/components/CuePipelineEditor/panels/EdgePromptRow.tsx (1)

26-28: Consider stronger typing for the debounced callback.

The ...args: unknown[] pattern works but loses type safety. If useDebouncedCallback supports generics, a typed version would be cleaner.

💡 Potential typed callback alternative
-	const { debouncedCallback: debouncedUpdate } = useDebouncedCallback((...args: unknown[]) => {
-		onUpdateEdgePrompt(edgeInfo.edgeId, args[0] as string);
-	}, 300);
+	const { debouncedCallback: debouncedUpdate } = useDebouncedCallback((prompt: string) => {
+		onUpdateEdgePrompt(edgeInfo.edgeId, prompt);
+	}, 300);

This assumes useDebouncedCallback accepts typed callbacks. If not, the current approach is acceptable.

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

In `@src/renderer/components/CuePipelineEditor/panels/EdgePromptRow.tsx` around
lines 26 - 28, The debounced callback currently uses an untyped rest param;
change it to a strongly typed callback so TypeScript knows the argument is a
string. Update the useDebouncedCallback invocation to supply an explicit generic
or typed function signature (e.g. a callback taking a single string) instead of
(...args: unknown[]), and call onUpdateEdgePrompt(edgeInfo.edgeId, value) inside
that typed callback; target the useDebouncedCallback and debouncedUpdate symbols
and keep onUpdateEdgePrompt/edgeInfo.edgeId usage the same.
src/renderer/components/CuePipelineEditor/cueEventConstants.ts (1)

18-18: Consider a more semantically appropriate icon for github.issue.

GitBranch is typically associated with Git branches rather than issues. Consider using a more issue-specific icon like CircleDot or AlertCircle from lucide-react to better match user expectations.

💡 Suggested icon alternatives
-import { Clock, FileText, Zap, GitPullRequest, GitBranch, CheckSquare } from 'lucide-react';
+import { Clock, FileText, Zap, GitPullRequest, CircleDot, CheckSquare } from 'lucide-react';
...
-	'github.issue': GitBranch,
+	'github.issue': CircleDot,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/CuePipelineEditor/cueEventConstants.ts` at line 18,
The mapping for 'github.issue' currently uses the GitBranch icon which is
semantically inaccurate; update the mapping in cueEventConstants.ts to use a
more issue-specific icon such as CircleDot or AlertCircle from lucide-react
(replace GitBranch with the chosen symbol in the mapping for 'github.issue'),
and update the import list to include the new icon symbol (remove GitBranch if
no longer used). Ensure the changed symbol name (CircleDot or AlertCircle) is
referenced in the same object where 'github.issue' is defined so the renderer
picks up the correct icon.
src/renderer/hooks/cue/useCueAiChat.ts (1)

146-159: Listener cleanup could race with error handler.

Both onExit and onAgentError handlers clean up all listeners. If both events fire (e.g., agent errors then exits), the cleanup runs twice. While calling cleanup functions multiple times is likely safe, consider guarding against this.

Optional: Guard against double cleanup
 const cleanupExit = window.maestro.process.onExit((sid: string) => {
 	if (sid === spawnSessionIdRef.current) {
+		if (aiCleanupRef.current.length === 0) return; // Already cleaned up
 		aiCleanupRef.current.forEach((fn) => fn());
 		aiCleanupRef.current = [];
 		// ... rest of handler
 	}
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/cue/useCueAiChat.ts` around lines 146 - 159, The onExit
handler (registered via window.maestro.process.onExit) and the onAgentError
handler both iterate aiCleanupRef.current and may run the same cleanup functions
twice; add a guard to ensure cleanup runs only once per session by using a
per-session flag or by clearing aiCleanupRef.current before invoking cleanup
functions (e.g., check spawnSessionIdRef.current or a local cleaned boolean,
remove/replace aiCleanupRef.current with [] prior to calling functions). Update
the cleanupExit registration and the onAgentError path to respect this
single-run guard so cleanup functions (cleanupExit) are not executed multiple
times.
src/renderer/components/CueYamlEditor/CueAiChat.tsx (1)

52-64: Consider adding stable keys for chat messages.

Using array index as key (key={i}) works here since messages are append-only, but if message deletion or editing is added later, this could cause rendering issues. A stable ID per message would be more robust.

Optional: Add message IDs in the hook

In useCueAiChat.ts, add an ID when creating messages:

{ id: crypto.randomUUID(), role: 'user', text }

Then use key={msg.id} here.

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

In `@src/renderer/components/CueYamlEditor/CueAiChat.tsx` around lines 52 - 64,
The chat message list uses array index as the React key in chatMessages.map
(key={i}), which is fragile; update the message model to include a stable id and
use it as the key. Add an id property when creating messages in useCueAiChat
(e.g., generate with crypto.randomUUID() or another stable ID) so each message
object has msg.id, then change the mapping to use key={msg.id} in CueAiChat.tsx
(and update any types/interfaces for the message shape).
src/renderer/components/CuePipelineEditor/panels/AgentConfigPanel.tsx (1)

59-70: Debounced callback typing could be improved.

The ...args: unknown[] pattern with type assertions works but loses type safety. Consider using a typed callback signature.

Optional: Type-safe debounced callback
-const { debouncedCallback: debouncedUpdateInput } = useDebouncedCallback((...args: unknown[]) => {
-	const inputPrompt = args[0] as string;
-	onUpdateNode(node.id, { inputPrompt } as Partial<AgentNodeData>);
-}, 300);
+const { debouncedCallback: debouncedUpdateInput } = useDebouncedCallback((inputPrompt: string) => {
+	onUpdateNode(node.id, { inputPrompt });
+}, 300);

This requires useDebouncedCallback to support typed signatures.

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

In `@src/renderer/components/CuePipelineEditor/panels/AgentConfigPanel.tsx` around
lines 59 - 70, Replace the untyped "...args: unknown[]" pattern in the two
debounced callbacks with an explicitly typed callback signature so you don't
need type assertions: update the calls to useDebouncedCallback to accept a
(inputPrompt: string) => void / (outputPrompt: string) => void handler (or use
the hook's generic type if it supports one) and call onUpdateNode(node.id, {
inputPrompt } as Partial<AgentNodeData>) / onUpdateNode(node.id, { outputPrompt
} as Partial<AgentNodeData>) directly; target the debouncedUpdateInput and
debouncedUpdateOutput definitions and the useDebouncedCallback symbol to ensure
full type safety for the string parameter rather than using unknown[].
src/renderer/components/CueModal/CueModal.tsx (1)

92-98: Consider awaiting enable/disable to prevent rapid toggle race conditions.

enable() and disable() return Promise<void> but are called fire-and-forget. Rapid toggling could cause race conditions with the underlying state refresh.

Proposed fix: Disable button during toggle
+const [toggling, setToggling] = useState(false);
+
-const handleToggle = useCallback(() => {
+const handleToggle = useCallback(async () => {
+	if (toggling) return;
+	setToggling(true);
+	try {
 		if (isEnabled) {
-			disable();
+			await disable();
 		} else {
-			enable();
+			await enable();
 		}
-}, [isEnabled, enable, disable]);
+	} finally {
+		setToggling(false);
+	}
+}, [isEnabled, enable, disable, toggling]);

Then disable the toggle button while toggling is true.

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

In `@src/renderer/components/CueModal/CueModal.tsx` around lines 92 - 98, The
handleToggle currently calls enable()/disable() fire-and-forget which can cause
race conditions; update handleToggle to await the Promise returned by enable or
disable and guard re-entrancy by introducing a local toggling state (e.g., const
[toggling, setToggling] = useState(false)), set toggling=true before awaiting
and set it back to false in finally, and pass toggling to the toggle button's
disabled prop so the UI is disabled while the async toggle is in progress;
reference handleToggle, enable, disable, and the toggle button within
CueModal.tsx when making these changes.
src/renderer/components/CueModal/SessionsTable.tsx (2)

81-96: Consider extracting the IIFE to a local variable for readability.

The inline IIFE works but adds visual complexity. A minor readability improvement would be to compute colors and pipelineNames before the return statement.

♻️ Optional refactor
 			<tbody>
 				{sessions.map((s) => {
 					const status = !s.enabled ? 'paused' : s.subscriptionCount > 0 ? 'active' : 'none';
+					const colors = getPipelineColorForAgent(s.sessionId, pipelines);
+					const pipelineNames = pipelines
+						.filter((p) => colors.includes(p.color))
+						.map((p) => p.name);
 					return (
 						<tr
 							key={s.sessionId}
 							...
 							<td className="py-2">
-								{(() => {
-									const colors = getPipelineColorForAgent(s.sessionId, pipelines);
-									if (colors.length === 0) {
-										return <span style={{ color: theme.colors.textDim }}>—</span>;
-									}
-									const pipelineNames = pipelines
-										.filter((p) => colors.includes(p.color))
-										.map((p) => p.name);
-									return (
-										<span className="flex items-center gap-1">
-											{colors.map((color, i) => (
-												<PipelineDot key={color} color={color} name={pipelineNames[i] ?? ''} />
-											))}
-										</span>
-									);
-								})()}
+								{colors.length === 0 ? (
+									<span style={{ color: theme.colors.textDim }}>—</span>
+								) : (
+									<span className="flex items-center gap-1">
+										{colors.map((color, i) => (
+											<PipelineDot key={color} color={color} name={pipelineNames[i] ?? ''} />
+										))}
+									</span>
+								)}
 							</td>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/CueModal/SessionsTable.tsx` around lines 81 - 96,
Extract the inline IIFE in SessionsTable to local variables for clarity: call
getPipelineColorForAgent(s.sessionId, pipelines) into a const colors, compute
pipelineNames by filtering pipelines for colors and mapping to p.name, then
inside the JSX return use a conditional that renders a dimmed dash when
colors.length === 0 or a <span className="flex items-center gap-1"> mapping
colors to <PipelineDot key={color} color={color} name={pipelineNames[i] ?? ''}
/>; keep the existing theme.colors.textDim usage for the empty state and
preserve the same props passed to PipelineDot.

122-127: Consider coordinating rapid-fire subscription triggers to avoid overlapping API calls and refresh requests.

The loop fires onTriggerSubscription (an async function) without awaiting, so multiple subscriptions trigger simultaneously. With N subscriptions, this fires N concurrent IPC calls plus N concurrent refresh() calls, causing:

  • Overlapping state refreshes (potential race conditions)
  • Burst of simultaneous API requests (rate-limiting risk)
  • No user feedback on progress or failures

Suggested fix: Await subscriptions sequentially, or batch them with Promise.all() + single final refresh. Also consider showing a loading state during the operation. If triggering many subscriptions becomes frequent, propose a batch trigger API to the backend.

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

In `@src/renderer/components/CueModal/SessionsTable.tsx` around lines 122 - 127,
The button handler currently loops over subs and calls the async
onTriggerSubscription(sub.name) without awaiting, causing many concurrent IPC
calls and refresh() races; change the handler to coordinate these calls — either
await each onTriggerSubscription sequentially (for (const sub of subs) await
onTriggerSubscription(sub.name)) or run them in parallel with
Promise.all(subs.map(s => onTriggerSubscription(s.name))) and then call
refresh() once after all complete; also set a local loading state during the
operation and handle/report failures from onTriggerSubscription before calling
refresh().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/cue/cue-engine.ts`:
- Around line 145-154: The catch block after initCueDb/pruneCueEvents currently
logs the error and returns, which swallows exceptions; update this path to
explicitly report the exception to Sentry before returning by invoking your
Sentry reporting utility (e.g., Sentry.captureException(error) or the project's
reportError/reportException helper) and pass the caught error (the `error`
variable) along with any contextual info (e.g., a message/tag indicating "[CUE]
DB init failure"); place the call immediately after this.deps.onLog(...) and
before the return so the failure is sent to Sentry while keeping the existing
log and early exit behavior.

In `@src/renderer/components/CueModal/CueModal.tsx`:
- Around line 135-146: The effect that calls window.maestro.cue.getGraphData
silently swallows errors in its .catch(() => {}) block; update the CueModal
useEffect to handle failures by either logging the error (e.g., console.error or
a logger) and/or setting a component error state (e.g., introduce [graphError,
setGraphError]) so the UI can show a failure message, and ensure you still
respect the cancelled flag before calling setGraphSessions or setGraphError;
replace the empty .catch with logic that logs the error and sets the error
state.

In `@src/renderer/components/CueModal/cueModalUtils.ts`:
- Around line 9-21: The formatRelativeTime function currently computes diff
using new Date(dateStr).getTime() which yields NaN for invalid date strings and
produces "NaNd ago"; fix this by parsing the date into a Date object first
(const d = new Date(dateStr)), check isNaN(d.getTime()) and if invalid return
the placeholder '—' (same as the empty input branch), otherwise proceed to
compute diff and format seconds/minutes/hours/days; update references in the
function name formatRelativeTime and any local variables you add to ensure the
invalid-date check short-circuits before doing arithmetic.
- Around line 31-34: formatElapsed can pass NaN to formatDuration because new
Date(startedAt).getTime() may be NaN; update formatElapsed to validate the
parsed time (use Date.parse or new Date(startedAt).getTime()), check
isNaN(parsed) and if invalid use 0 (or an explicit fallback) before calling
formatDuration, so call formatDuration(Math.max(0, parsedDifference)) where
parsedDifference is only computed when startedAt is a valid date; reference
function formatElapsed and formatDuration to locate the change.

In `@src/renderer/components/CueYamlEditor/CueYamlEditor.tsx`:
- Around line 119-128: The catch block inside the handleSave async callback
(used to write YAML and refresh session) is empty and swallows errors instead of
letting them bubble to Sentry; update the catch to accept the error (e.g., catch
(err)) and rethrow it after any optional user-facing handling so exceptions
propagate (or remove the try/catch entirely) — target the handleSave function
where window.maestro.cue.writeYaml and refreshSession are called and ensure
errors are rethrown rather than silently ignored.
- Around line 130-137: handleClose currently only checks for unsaved changes
(yamlContent vs originalContent) but doesn't prevent closing when an async AI
chat operation is in progress; update handleClose to also check the chatBusy
boolean (or prop/state) before proceeding, and if chatBusy is true either block
the close and show an informative alert/confirmation or include chatBusy in the
confirmation flow so the user cannot discard in-progress AI work; specifically
modify the useCallback for handleClose to reference chatBusy alongside
yamlContent, originalContent, and onClose and conditionally return early or
prompt when chatBusy is true.

In `@src/renderer/components/CueYamlEditor/PatternPreviewModal.tsx`:
- Around line 22-26: The handleCopy callback lacks error handling around
navigator.clipboard.writeText which can reject; wrap the await call in a
try/catch inside handleCopy, call setCopied(true) only on success, and
setCopied(false) in a finally or via the existing timeout; on error log or
surface the error (e.g., process via a toast or console.error) so the UI can
fail gracefully; reference the handleCopy function,
navigator.clipboard.writeText, and setCopied when making the changes.

In `@src/renderer/hooks/cue/useCueAiChat.ts`:
- Around line 64-81: When isOpen becomes true you need to cleanup any stale AI
listeners before resetting state: inside the useEffect that handles "if
(isOpen)" call and clear aiCleanupRef.current (e.g.,
aiCleanupRef.current.forEach(fn => fn()); aiCleanupRef.current = []) before
calling setChatMessages, setChatInput, setChatBusy and resetting
agentSessionIdRef/spawnSessionIdRef; this ensures existing listeners from
previous sessions are removed prior to creating a new spawnSessionIdRef and
starting a new agent session.

In `@src/renderer/hooks/useCue.ts`:
- Around line 99-101: The catch block in the refresh flow (around the code that
updates Cue status in useCue.ts) currently converts any thrown value into UI
error state via setError and hides unexpected defects; change it to treat
expected/recoverable errors (e.g., known network or API error types returned by
your fetch function) by setting setError when mountedRef.current is true, but
for unexpected exceptions call Sentry.captureException(err) (or the project’s
reporting utility) and rethrow the error so it surfaces to Sentry instead of
being swallowed; update the catch to inspect the thrown value ( instanceof Error
or custom error classes / known error markers from your fetchCueStatus or
refreshCue function) and only setError for the expected cases, otherwise
report+rethrow.

---

Nitpick comments:
In `@src/renderer/components/CueModal/CueModal.tsx`:
- Around line 92-98: The handleToggle currently calls enable()/disable()
fire-and-forget which can cause race conditions; update handleToggle to await
the Promise returned by enable or disable and guard re-entrancy by introducing a
local toggling state (e.g., const [toggling, setToggling] = useState(false)),
set toggling=true before awaiting and set it back to false in finally, and pass
toggling to the toggle button's disabled prop so the UI is disabled while the
async toggle is in progress; reference handleToggle, enable, disable, and the
toggle button within CueModal.tsx when making these changes.

In `@src/renderer/components/CueModal/SessionsTable.tsx`:
- Around line 81-96: Extract the inline IIFE in SessionsTable to local variables
for clarity: call getPipelineColorForAgent(s.sessionId, pipelines) into a const
colors, compute pipelineNames by filtering pipelines for colors and mapping to
p.name, then inside the JSX return use a conditional that renders a dimmed dash
when colors.length === 0 or a <span className="flex items-center gap-1"> mapping
colors to <PipelineDot key={color} color={color} name={pipelineNames[i] ?? ''}
/>; keep the existing theme.colors.textDim usage for the empty state and
preserve the same props passed to PipelineDot.
- Around line 122-127: The button handler currently loops over subs and calls
the async onTriggerSubscription(sub.name) without awaiting, causing many
concurrent IPC calls and refresh() races; change the handler to coordinate these
calls — either await each onTriggerSubscription sequentially (for (const sub of
subs) await onTriggerSubscription(sub.name)) or run them in parallel with
Promise.all(subs.map(s => onTriggerSubscription(s.name))) and then call
refresh() once after all complete; also set a local loading state during the
operation and handle/report failures from onTriggerSubscription before calling
refresh().

In `@src/renderer/components/CuePipelineEditor/cueEventConstants.ts`:
- Line 18: The mapping for 'github.issue' currently uses the GitBranch icon
which is semantically inaccurate; update the mapping in cueEventConstants.ts to
use a more issue-specific icon such as CircleDot or AlertCircle from
lucide-react (replace GitBranch with the chosen symbol in the mapping for
'github.issue'), and update the import list to include the new icon symbol
(remove GitBranch if no longer used). Ensure the changed symbol name (CircleDot
or AlertCircle) is referenced in the same object where 'github.issue' is defined
so the renderer picks up the correct icon.

In `@src/renderer/components/CuePipelineEditor/panels/AgentConfigPanel.tsx`:
- Around line 59-70: Replace the untyped "...args: unknown[]" pattern in the two
debounced callbacks with an explicitly typed callback signature so you don't
need type assertions: update the calls to useDebouncedCallback to accept a
(inputPrompt: string) => void / (outputPrompt: string) => void handler (or use
the hook's generic type if it supports one) and call onUpdateNode(node.id, {
inputPrompt } as Partial<AgentNodeData>) / onUpdateNode(node.id, { outputPrompt
} as Partial<AgentNodeData>) directly; target the debouncedUpdateInput and
debouncedUpdateOutput definitions and the useDebouncedCallback symbol to ensure
full type safety for the string parameter rather than using unknown[].

In `@src/renderer/components/CuePipelineEditor/panels/EdgePromptRow.tsx`:
- Around line 26-28: The debounced callback currently uses an untyped rest
param; change it to a strongly typed callback so TypeScript knows the argument
is a string. Update the useDebouncedCallback invocation to supply an explicit
generic or typed function signature (e.g. a callback taking a single string)
instead of (...args: unknown[]), and call onUpdateEdgePrompt(edgeInfo.edgeId,
value) inside that typed callback; target the useDebouncedCallback and
debouncedUpdate symbols and keep onUpdateEdgePrompt/edgeInfo.edgeId usage the
same.

In `@src/renderer/components/CuePipelineEditor/panels/triggers/TriggerConfig.tsx`:
- Around line 31-39: The callbacks passed to useDebouncedCallback are using an
unsafe unknown[] signature and then casting args[0]; update debouncedUpdate and
debouncedUpdateLabel to use a typed callback parameter (e.g., provide a typed
function signature or generic to useDebouncedCallback) so you directly accept
(config: TriggerNodeData['config']) and (customLabel?: string) respectively,
then call onUpdateNode(node.id, { config }) and onUpdateNode(node.id, {
customLabel }) without casts; reference the debouncedUpdate,
debouncedUpdateLabel, useDebouncedCallback, onUpdateNode and TriggerNodeData
symbols when making the change.

In `@src/renderer/components/CueYamlEditor/CueAiChat.tsx`:
- Around line 52-64: The chat message list uses array index as the React key in
chatMessages.map (key={i}), which is fragile; update the message model to
include a stable id and use it as the key. Add an id property when creating
messages in useCueAiChat (e.g., generate with crypto.randomUUID() or another
stable ID) so each message object has msg.id, then change the mapping to use
key={msg.id} in CueAiChat.tsx (and update any types/interfaces for the message
shape).

In `@src/renderer/components/CueYamlEditor/PatternPreviewModal.tsx`:
- Around line 22-26: The setTimeout in handleCopy (which writes pattern.yaml to
clipboard and calls setCopied) can call setCopied after the modal unmounts;
store the timer id in a ref (e.g., copyTimeoutRef), assign the timeout return
value to it inside handleCopy, and add a useEffect cleanup that clears the
timeout (clearTimeout(copyTimeoutRef.current)) on unmount to prevent state
updates after unmount; ensure handleCopy and the cleanup reference the same ref
and that the ref is cleared/reset after the timeout completes.

In `@src/renderer/components/CueYamlEditor/YamlTextEditor.tsx`:
- Line 25: Remove the unused ref declaration yamlTextareaRef from YamlTextEditor
and delete the ref attribute on the textarea element; the Tab key handler
already uses e.currentTarget to access the textarea, so delete the const
yamlTextareaRef = useRef<HTMLTextAreaElement>(null) and the corresponding ref
usage to avoid dead code and unused imports.

In `@src/renderer/hooks/cue/useCueAiChat.ts`:
- Around line 146-159: The onExit handler (registered via
window.maestro.process.onExit) and the onAgentError handler both iterate
aiCleanupRef.current and may run the same cleanup functions twice; add a guard
to ensure cleanup runs only once per session by using a per-session flag or by
clearing aiCleanupRef.current before invoking cleanup functions (e.g., check
spawnSessionIdRef.current or a local cleaned boolean, remove/replace
aiCleanupRef.current with [] prior to calling functions). Update the cleanupExit
registration and the onAgentError path to respect this single-run guard so
cleanup functions (cleanupExit) are not executed multiple times.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 702dde9e-6d71-4999-8dba-402c24bd7160

📥 Commits

Reviewing files that changed from the base of the PR and between 87b4421 and 695f2ce.

📒 Files selected for processing (48)
  • src/__tests__/main/cue/cue-concurrency.test.ts
  • src/__tests__/main/cue/cue-engine.test.ts
  • src/__tests__/main/cue/cue-event-factory.test.ts
  • src/__tests__/main/cue/cue-sleep-wake.test.ts
  • src/__tests__/renderer/hooks/useCue.test.ts
  • src/main/cue/cue-engine.ts
  • src/main/cue/cue-fan-in-tracker.ts
  • src/main/cue/cue-file-watcher.ts
  • src/main/cue/cue-github-poller.ts
  • src/main/cue/cue-reconciler.ts
  • src/main/cue/cue-subscription-setup.ts
  • src/main/cue/cue-task-scanner.ts
  • src/main/cue/cue-types.ts
  • src/renderer/components/CueModal.tsx
  • src/renderer/components/CueModal/ActiveRunsList.tsx
  • src/renderer/components/CueModal/ActivityLog.tsx
  • src/renderer/components/CueModal/ActivityLogDetail.tsx
  • src/renderer/components/CueModal/CueModal.tsx
  • src/renderer/components/CueModal/SessionsTable.tsx
  • src/renderer/components/CueModal/StatusDot.tsx
  • src/renderer/components/CueModal/cueModalUtils.ts
  • src/renderer/components/CueModal/index.ts
  • src/renderer/components/CuePipelineEditor/PipelineCanvas.tsx
  • src/renderer/components/CuePipelineEditor/cueEventConstants.ts
  • src/renderer/components/CuePipelineEditor/drawers/TriggerDrawer.tsx
  • src/renderer/components/CuePipelineEditor/edges/PipelineEdge.tsx
  • src/renderer/components/CuePipelineEditor/nodes/TriggerNode.tsx
  • src/renderer/components/CuePipelineEditor/panels/AgentConfigPanel.tsx
  • src/renderer/components/CuePipelineEditor/panels/EdgePromptRow.tsx
  • src/renderer/components/CuePipelineEditor/panels/NodeConfigPanel.tsx
  • src/renderer/components/CuePipelineEditor/panels/triggers/TriggerConfig.tsx
  • src/renderer/components/CuePipelineEditor/panels/triggers/index.ts
  • src/renderer/components/CuePipelineEditor/panels/triggers/triggerConfigStyles.ts
  • src/renderer/components/CueYamlEditor.tsx
  • src/renderer/components/CueYamlEditor/CueAiChat.tsx
  • src/renderer/components/CueYamlEditor/CueYamlEditor.tsx
  • src/renderer/components/CueYamlEditor/PatternPicker.tsx
  • src/renderer/components/CueYamlEditor/PatternPreviewModal.tsx
  • src/renderer/components/CueYamlEditor/YamlTextEditor.tsx
  • src/renderer/components/CueYamlEditor/index.ts
  • src/renderer/components/History/historyConstants.tsx
  • src/renderer/constants/cueYamlDefaults.ts
  • src/renderer/hooks/cue/useCueAiChat.ts
  • src/renderer/hooks/cue/usePipelineLayout.ts
  • src/renderer/hooks/cue/usePipelineSelection.ts
  • src/renderer/hooks/cue/usePipelineState.ts
  • src/renderer/hooks/useCue.ts
  • src/shared/cue-pipeline-types.ts
💤 Files with no reviewable changes (2)
  • src/renderer/components/CueModal.tsx
  • src/renderer/components/CueYamlEditor.tsx

Comment on lines +99 to +101
} catch (err) {
if (!mountedRef.current) return;
setError(err instanceof Error ? err.message : 'Failed to fetch Cue status');
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t treat all refresh failures as recoverable UI errors.

This catch path currently swallows unexpected exceptions into error state, which can hide real defects from Sentry. Split expected/recoverable failures from unexpected ones, and report unexpected exceptions explicitly.

Proposed direction
 import { useState, useEffect, useCallback, useRef } from 'react';
+import { captureException } from '../utils/sentry';
@@
 		} catch (err) {
 			if (!mountedRef.current) return;
-			setError(err instanceof Error ? err.message : 'Failed to fetch Cue status');
+			if (err instanceof Error) {
+				// Expected/recoverable IPC/network failures can be shown to users
+				setError(err.message);
+				return;
+			}
+			// Unexpected failure: report explicitly
+			captureException(err, { extra: { operation: 'useCue.refresh' } });
+			setError('Failed to fetch Cue status');
 		} finally {

As per coding guidelines: “For expected/recoverable errors, catch them explicitly…” and “For unexpected errors, rethrow to let Sentry capture them,” plus “Use Sentry utilities for explicit reporting.”

📝 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
} catch (err) {
if (!mountedRef.current) return;
setError(err instanceof Error ? err.message : 'Failed to fetch Cue status');
import { useState, useEffect, useCallback, useRef } from 'react';
import { captureException } from '../utils/sentry';
} catch (err) {
if (!mountedRef.current) return;
if (err instanceof Error) {
// Expected/recoverable IPC/network failures can be shown to users
setError(err.message);
return;
}
// Unexpected failure: report explicitly
captureException(err, { extra: { operation: 'useCue.refresh' } });
setError('Failed to fetch Cue status');
} finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/useCue.ts` around lines 99 - 101, The catch block in the
refresh flow (around the code that updates Cue status in useCue.ts) currently
converts any thrown value into UI error state via setError and hides unexpected
defects; change it to treat expected/recoverable errors (e.g., known network or
API error types returned by your fetch function) by setting setError when
mountedRef.current is true, but for unexpected exceptions call
Sentry.captureException(err) (or the project’s reporting utility) and rethrow
the error so it surfaces to Sentry instead of being swallowed; update the catch
to inspect the thrown value ( instanceof Error or custom error classes / known
error markers from your fetchCueStatus or refreshCue function) and only setError
for the expected cases, otherwise report+rethrow.

- cue-engine.ts: report DB init failure to Sentry before early return
- CueModal: surface getGraphData errors in dashboard error banner,
  add toggling guard to prevent enable/disable race conditions
- cueModalUtils: guard formatRelativeTime/formatElapsed against NaN dates
- CueYamlEditor: remove swallowed catch in handleSave (let errors bubble
  to Sentry), block close while AI chat is in progress
- PatternPreviewModal: wrap clipboard.writeText in try/catch, clear
  setTimeout on unmount via ref
- useCueAiChat: clean up stale listeners on modal reopen, snapshot-and-
  clear pattern prevents double cleanup when both onExit and onAgentError
  fire for the same session
- cueEventConstants: use CircleDot instead of GitBranch for github.issue
- YamlTextEditor: remove unused yamlTextareaRef

Skipped (verified, no fix needed):
- useCue.ts catch: IPC failures are expected/recoverable, main process
  reports unexpected errors to Sentry — double-reporting not desirable
- useDebouncedCallback typing: hook generic constraint requires unknown[],
  changing requires modifying the shared utility (out of scope)
- Chat message key={i}: append-only list, never reorders — index key is safe
- SessionsTable IIFE: works correctly, pure style preference
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/renderer/components/CueModal/CueModal.tsx (1)

206-214: Minor duplication with onEscape handler.

The confirmation logic here duplicates lines 120-126 in the onEscape callback. Consider extracting a shared helper, though this is optional given the small scope.

💡 Possible extraction
+const confirmCloseIfDirty = useCallback(() => {
+	if (pipelineDirtyRef.current) {
+		const confirmed = window.confirm(
+			'You have unsaved changes in the pipeline editor. Discard and close?'
+		);
+		if (!confirmed) return false;
+	}
+	return true;
+}, []);

 // In registerLayer onEscape:
 onEscape: () => {
 	if (showHelpRef.current) {
 		setShowHelp(false);
 		return;
 	}
-	if (pipelineDirtyRef.current) {
-		const confirmed = window.confirm(
-			'You have unsaved changes in the pipeline editor. Discard and close?'
-		);
-		if (!confirmed) return;
-	}
-	onCloseRef.current();
+	if (confirmCloseIfDirty()) onCloseRef.current();
 },

 // handleCloseWithConfirm:
 const handleCloseWithConfirm = useCallback(() => {
-	if (pipelineDirtyRef.current) {
-		const confirmed = window.confirm(
-			'You have unsaved changes in the pipeline editor. Discard and close?'
-		);
-		if (!confirmed) return;
-	}
-	onClose();
+	if (confirmCloseIfDirty()) onClose();
 }, [onClose, confirmCloseIfDirty]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/CueModal/CueModal.tsx` around lines 206 - 214,
Extract the duplicated confirmation logic used in handleCloseWithConfirm and the
onEscape callback into a small shared helper (e.g., confirmDiscardIfDirty) that
accepts pipelineDirtyRef and onClose (or returns a boolean) and performs the
window.confirm and conditional onClose invocation; then replace the bodies of
handleCloseWithConfirm and onEscape to call that helper to avoid duplication
while keeping existing behavior for pipelineDirtyRef and onClose.
src/renderer/components/CueYamlEditor/CueYamlEditor.tsx (1)

129-146: Silent failures in refreshYamlFromDisk may confuse users.

Both catch blocks are marked "non-fatal" but provide no user feedback. If the AI agent modifies the file but the refresh fails, the editor will show stale content without indication. Consider setting a brief error state or toast.

💡 Possible improvement
 const refreshYamlFromDisk = useCallback(async () => {
 	try {
 		const content = await window.maestro.cue.readYaml(projectRoot);
 		if (content) {
 			setYamlContent(content);
 			setOriginalContent(content);
 			try {
 				const result = await window.maestro.cue.validateYaml(content);
 				setIsValid(result.valid);
 				setValidationErrors(result.errors);
 			} catch {
-				// non-fatal
+				// Validation failed but content was read successfully
 			}
 		}
 	} catch {
-		// non-fatal
+		// Could optionally show a brief warning that refresh failed
+		console.warn('Failed to refresh YAML from disk');
 	}
 }, [projectRoot]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/CueYamlEditor/CueYamlEditor.tsx` around lines 129 -
146, refreshYamlFromDisk currently swallows both readYaml and validateYaml
errors leaving the editor showing stale content with no feedback; update
refreshYamlFromDisk to surface errors to the user by setting a short-lived error
state or invoking the app toast/notification helper when either
window.maestro.cue.readYaml(projectRoot) or
window.maestro.cue.validateYaml(content) throws — specifically update the catch
blocks in refreshYamlFromDisk to call a UI notifier or set a new state (e.g.,
setYamlLoadError / showToast) and ensure you still treat validation failures as
non-fatal but communicate the problem (while keeping existing setters:
setYamlContent, setOriginalContent, setIsValid, setValidationErrors).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/renderer/components/CueModal/CueModal.tsx`:
- Around line 206-214: Extract the duplicated confirmation logic used in
handleCloseWithConfirm and the onEscape callback into a small shared helper
(e.g., confirmDiscardIfDirty) that accepts pipelineDirtyRef and onClose (or
returns a boolean) and performs the window.confirm and conditional onClose
invocation; then replace the bodies of handleCloseWithConfirm and onEscape to
call that helper to avoid duplication while keeping existing behavior for
pipelineDirtyRef and onClose.

In `@src/renderer/components/CueYamlEditor/CueYamlEditor.tsx`:
- Around line 129-146: refreshYamlFromDisk currently swallows both readYaml and
validateYaml errors leaving the editor showing stale content with no feedback;
update refreshYamlFromDisk to surface errors to the user by setting a
short-lived error state or invoking the app toast/notification helper when
either window.maestro.cue.readYaml(projectRoot) or
window.maestro.cue.validateYaml(content) throws — specifically update the catch
blocks in refreshYamlFromDisk to call a UI notifier or set a new state (e.g.,
setYamlLoadError / showToast) and ensure you still treat validation failures as
non-fatal but communicate the problem (while keeping existing setters:
setYamlContent, setOriginalContent, setIsValid, setValidationErrors).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 11dc4005-d6f0-4460-8650-f8b9d7cdb49f

📥 Commits

Reviewing files that changed from the base of the PR and between 695f2ce and 3e78fc1.

📒 Files selected for processing (8)
  • src/main/cue/cue-engine.ts
  • src/renderer/components/CueModal/CueModal.tsx
  • src/renderer/components/CueModal/cueModalUtils.ts
  • src/renderer/components/CuePipelineEditor/cueEventConstants.ts
  • src/renderer/components/CueYamlEditor/CueYamlEditor.tsx
  • src/renderer/components/CueYamlEditor/PatternPreviewModal.tsx
  • src/renderer/components/CueYamlEditor/YamlTextEditor.tsx
  • src/renderer/hooks/cue/useCueAiChat.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/renderer/components/CueYamlEditor/PatternPreviewModal.tsx
  • src/renderer/components/CueYamlEditor/YamlTextEditor.tsx
  • src/main/cue/cue-engine.ts
  • src/renderer/components/CueModal/cueModalUtils.ts

@reachraza reachraza changed the title Cue polish refactor(cue): full SRP decomposition — engine modules, renderer components, shared types Mar 20, 2026
@reachraza reachraza merged commit 82dfe26 into rc Mar 20, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant