Skip to content

New cli update#107

Merged
AviPeltz merged 13 commits intomainfrom
new-cli-update
Nov 20, 2025
Merged

New cli update#107
AviPeltz merged 13 commits intomainfrom
new-cli-update

Conversation

@AviPeltz
Copy link
Copy Markdown
Collaborator

@AviPeltz AviPeltz commented Nov 20, 2025

Description

Related Issues

Type of Change

  • Bug fix
  • [ x] New feature
  • Documentation
  • Refactor
  • Other (please describe):

Testing

Screenshots (if applicable)

Additional Notes

Summary by CodeRabbit

  • New Features

    • Interactive workspace init (local & cloud), real-time dashboard, three-panel manager, new CLI commands (init, dashboard, panels, workspace use, agent start/attach) and a default welcome view
    • Agent session management with tmux-based launch/attach and improved lifecycle handling
    • CLI executable renamed to "superset" and persistent current-workspace tracking
  • Bug Fixes / Behavior

    • stop-all now stops agents only (terminals preserved)
    • Processes now expose explicit statuses (RUNNING / IDLE / STOPPED / ERROR)
  • Documentation

    • Expanded CLI docs: quick start, core concepts, commands, storage, tmux tips and troubleshooting

✏️ Tip: You can customize this high-level summary in your review settings.

- Added LaunchOverlay component to display loading state while attaching to agents in the dashboard.
- Removed unnecessary launching state from panels, simplifying the agent attachment logic.
- Updated launchAgent and attachToAgent functions to handle synchronous checks for tmux sessions, improving performance and reliability.
- Adjusted database initialization to use a default path from configuration instead of hardcoded values.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 20, 2025

Walkthrough

Adds new interactive CLI UI components (Init, Dashboard, Panels), workspace-scoped agent management (start, attach, stop-all), tmux-based launch/attach tooling, orchestrator/storage/type refinements (backfills, current workspace, options-based create, ProcessStatus), tests, docs (including new CLAUDE.md), and packaging/build adjustments for the CLI.

Changes

Cohort / File(s) Summary
Documentation
WARP.md, apps/cli/readme.md, apps/cli/src/lib/persistence_docs/EXAMPLE.md, apps/cli/src/lib/persistence_docs/SUMMARY.md, CLAUDE.md
Added/updated CLI docs and examples; clarified that stopAll stops agents only; added new CLAUDE.md.
Packaging & Entry
apps/cli/package.json, apps/cli/readme.md
Changed bin to object mapping ("superset": "dist/cli.js"), removed --packages=bundle from build script, added ink-text-input dependency, and updated README/quick-start and binary naming.
CLI Top-level / Exports
apps/cli/src/cli.tsx, apps/cli/src/commands/index.ts
Expanded CLI surface and default action; exported new commands: Init, Dashboard, Panels, WorkspaceUse, AgentStart, AgentAttach.
Commands — Agent & Workspace
apps/cli/src/commands/agent.tsx, apps/cli/src/commands/workspace.tsx
Added AgentStart and AgentAttach; AgentStopAll now accepts optional workspaceId; added WorkspaceUse; Workspace.create now accepts an options object payload.
Commands — UI Components
apps/cli/src/commands/dashboard.tsx, apps/cli/src/commands/panels.tsx, apps/cli/src/commands/init.tsx
New Ink-based Dashboard (real-time view, keyboard navigation, launch), Panels (three-panel management UI), and multi-step Init wizard (local/cloud flows, validation, create).
Launcher / Tmux
apps/cli/src/lib/launch/config.ts, apps/cli/src/lib/launch/run.ts
Added default per-agent launch commands and env-var overrides; implemented launchAgent and attachToAgent with tmux checks, session create/attach, retries, and lifecycle handling.
Database Access & Storage
apps/cli/src/lib/db.ts, apps/cli/src/lib/storage/adapter.ts, apps/cli/src/lib/storage/lowdb-adapter.ts, apps/cli/src/lib/storage/types.ts
Removed hard-coded DB path creation (adapter handles path); introduced CollectionValue typing for adapter methods; adapter/lowdb get/set signatures updated; initial DB now includes state.currentWorkspaceId and a default environment.
Orchestrators — Workspace & Process
apps/cli/src/lib/orchestrators/workspace-orchestrator.ts, apps/cli/src/lib/orchestrators/process-orchestrator.ts
Workspace.create now accepts an options object; added use and getCurrent; backfill defaults on get/list; ProcessOrchestrator added tmux reconciliation, deterministic IDs/session names, launchCommand defaulting, and stopAll now stops agents only and returns count.
Types
apps/cli/src/types/process.ts, apps/cli/src/types/workspace.ts
Added ProcessStatus enum and status/metadata fields on Process; discriminated Agent/Terminal types; added CloudWorkspace and workspace metadata fields; updated orchestrator signatures.
Tests
apps/cli/src/lib/orchestrators/__tests__/*, apps/cli/src/lib/storage/__tests__/lowdb-adapter.test.ts
Updated tests to use new workspace.create options-object, use ProcessStatus enum values, reflect agent-only stopAll behavior, and align storage/test data with new shapes.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as superset CLI
    participant Init as Init Wizard
    participant WSOrch as WorkspaceOrchestrator
    participant EnvOrch as EnvironmentOrchestrator
    participant DB as DB

    User->>CLI: run "superset init"
    CLI->>Init: launch wizard
    Init->>Init: collect type/path/branch/name/agents
    Init->>WSOrch: validate/create (envId, type, options)
    WSOrch->>DB: read/write workspaces & state
    EnvOrch->>DB: ensure/select environment
    WSOrch-->>Init: created workspace id
    Init->>User: show summary and exit
Loading
sequenceDiagram
    participant User
    participant Dashboard
    participant Orchs as Orchestrators
    participant DB as DB
    participant Launcher
    participant Tmux

    User->>Dashboard: open
    Dashboard->>Orchs: parallel load (workspaces, processes, current)
    Orchs->>DB: fetch & backfill defaults
    DB-->>Orchs: data
    Orchs-->>Dashboard: DashboardData
    Dashboard->>Dashboard: render + poll
    User->>Dashboard: select agent + Enter
    Dashboard->>Launcher: launchAgent(agent,{attach:true})
    Launcher->>Tmux: check/create/attach session
    alt session reachable
        Tmux-->>Launcher: attach success
        Launcher-->>Dashboard: attach result
    else fail
        Tmux-->>Launcher: error
        Launcher-->>Orchs: mark agent ERROR
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Areas to focus on:
    • apps/cli/src/commands/init.tsx — multi-step flow, validation, environment selection, persistence calls.
    • apps/cli/src/commands/dashboard.tsx & panels.tsx — polling, input handling, launch/attach and clean Ink exits.
    • apps/cli/src/lib/launch/run.ts — tmux/session lifecycle, command discovery, retries, and cleanup.
    • apps/cli/src/lib/orchestrators/process-orchestrator.ts & workspace-orchestrator.ts — backfill/sync logic, API signature changes, and stopAll behavior.

Possibly related PRs

Poem

🐰 I hopped into code with a curious twitch,

Dashboards glow bright and an Init that clicks,
Tmux sessions hum, agents spring to play,
Workspaces chosen, defaults saved away,
A rabbit cheers — CLI delights today!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is largely incomplete, containing only template placeholders with no actual content, implementation details, testing information, or context about the changes. Fill in all required sections: provide a clear description of the new features, link any related issues, describe the testing performed, and add any relevant implementation notes.
Docstring Coverage ⚠️ Warning Docstring coverage is 58.82% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'New cli update' is vague and generic, using non-descriptive phrasing that does not clearly convey the specific changes made in the changeset. Replace with a more specific title that describes the main changes, such as 'Add dashboard, init, and panels commands to CLI' or 'Enhance CLI with workspace and agent management features'.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch new-cli-update

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 16cdcbf and 0b79f09.

📒 Files selected for processing (1)
  • apps/cli/readme.md (1 hunks)
🧰 Additional context used
🪛 LanguageTool
apps/cli/readme.md

[style] ~43-~43: To form a complete sentence, be sure to include a subject.
Context: ...nment and state.currentWorkspaceId. - Can be overridden with `SUPERSET_CLI_DATA_D...

(MISSING_IT_THERE)


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

Copy link
Copy Markdown
Contributor

@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: 6

Caution

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

⚠️ Outside diff range comments (3)
apps/old-desktop/src/renderer/screens/main/components/PlanView/PlanView.tsx (1)

178-179: Pre-existing bug: Undefined variable references.

These lines reference onTabSelect and onTabCreated, but those variables are not defined. Based on lines 18-19, the correct variable names are handleTabSelect and handleTabCreated. This would cause runtime errors when rendering the Kanban columns.

Note: This bug pre-dates this PR (these lines were not changed), but should be addressed.

Apply this diff to fix the undefined references:

 					onTabSelect={handleTabSelect}
-					onTabCreated={onTabCreated}
+					onTabCreated={handleTabCreated}

Apply this same pattern to all five KanbanColumn instances (lines 178-179, 189-190, 200-201, 211-212, 223-224).

Also applies to: 189-190, 200-201, 211-212, 223-224

apps/old-desktop/src/main/lib/desktop-stores.ts (1)

52-90: Don’t swallow initialization/migration failures; gate version writes on success

initializeAsync currently catches all errors, logs, and then resolves, so DesktopStores.initialize() appears successful even if storage dir setup or legacy migration fail. Additionally, DomainVersion/ui version writes run regardless of whether migration succeeded, which can incorrectly mark versions as initialized after a failed migration.

-	private async initializeAsync(): Promise<void> {
-		try {
-			// Ensure storage directories exist first
-			await ensureDesktopStorageDirs();
-
-			// Then run migration if needed
-			await this.runMigrationIfNeeded();
-		} catch (error) {
-			console.error("Failed to initialize Desktop stores:", error);
-		}
-	}
+	private async initializeAsync(): Promise<void> {
+		try {
+			// Ensure storage directories exist first
+			await ensureDesktopStorageDirs();
+
+			// Then run migration if needed
+			await this.runMigrationIfNeeded();
+		} catch (error) {
+			console.error("Failed to initialize Desktop stores:", error);
+			throw error; // surface failures to callers of DesktopStores.initialize()
+		}
+	}
@@
-	private async runMigrationIfNeeded(): Promise<void> {
-		const migrator = new LegacyMigrator();
-		if (migrator.shouldMigrate()) {
-			console.log("[DesktopStores] Running migration from legacy config...");
-			const result = await migrator.migrate(
-				this.environmentOrchestrator,
-				this.workspaceOrchestrator,
-				this.uiStore,
-				false,
-			);
-			if (result.success) {
-				console.log(
-					`[DesktopStores] Migration completed: ${result.migrated.workspaces} workspaces migrated`,
-				);
-			} else {
-				console.error(`[DesktopStores] Migration failed: ${result.error}`);
-			}
-		}
-
-		// Initialize versions if not set
-		if (DomainVersion.read() === 0) {
-			DomainVersion.write();
-		}
-		if (this.uiStore.readUiVersion() === 0) {
-			this.uiStore.writeUiVersion();
-		}
-	}
+	private async runMigrationIfNeeded(): Promise<void> {
+		const migrator = new LegacyMigrator();
+		let migrationOk = true;
+
+		if (migrator.shouldMigrate()) {
+			console.log("[DesktopStores] Running migration from legacy config...");
+			const result = await migrator.migrate(
+				this.environmentOrchestrator,
+				this.workspaceOrchestrator,
+				this.uiStore,
+				false,
+			);
+			migrationOk = result.success;
+			if (result.success) {
+				console.log(
+					`[DesktopStores] Migration completed: ${result.migrated.workspaces} workspaces migrated`,
+				);
+			} else {
+				console.error(`[DesktopStores] Migration failed: ${result.error}`);
+				throw new Error(`Legacy migration failed: ${result.error}`);
+			}
+		}
+
+		// Initialize versions if not set, but only after successful migration / no migration
+		if (migrationOk && DomainVersion.read() === 0) {
+			DomainVersion.write();
+		}
+		if (migrationOk && this.uiStore.readUiVersion() === 0) {
+			this.uiStore.writeUiVersion();
+		}
+	}
apps/cli/src/types/workspace.ts (1)

28-50: WorkspaceOrchestrator interface return types should be async to match implementation

WorkspaceOrchestrator.update and WorkspaceOrchestrator.delete are typed as synchronous (=> void), but the implementation in apps/cli/src/lib/orchestrators/workspace-orchestrator.ts is async and returns Promise<void>. That mismatch will cause implements IWorkspaceOrchestrator type errors and/or misleading call sites.

Consider updating the interface to reflect the async contract:

 export interface WorkspaceOrchestrator {
@@
-	update: (id: string, workspace: Partial<Workspace>) => void;
+	update: (id: string, workspace: Partial<Workspace>) => Promise<void>;
@@
-	delete: (id: string) => void;
+	delete: (id: string) => Promise<void>;
🧹 Nitpick comments (22)
apps/old-desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/WorktreeItemArborist.tsx (1)

88-92: Consider aligning expandedGroupTabs with Tree’s actual open state

Right now expandedGroupTabs is only used to compute initialOpenState, while onToggle and the selectedTabId effect update the set but don’t directly control the arborist tree’s open state. If the intent is to auto‑expand the group that contains the selected tab, you may want to confirm how initialOpenState is treated by react-arborist and, if needed, switch to a controlled open‑state pattern (or call the tree API) so visual expansion always matches expandedGroupTabs.

Also applies to: 108-125, 441-477

apps/old-desktop/src/renderer/screens/main/components/MainContent/TabGroup.tsx (1)

9-12: Centralizing the drag-and-drop manager looks good

Using the shared dragDropManager from renderer/lib/dnd for the Mosaic dragAndDropManager prop keeps DnD behavior consistent across the app and avoids per-component manager instances. Import consolidation for the tab/workspace contexts is also clean and idiomatic.

If you touch this file again, consider trimming unused values from renderTile’s dependency array (workingDirectory, workspaceId, worktreeId, handleTabFocus) to reduce unnecessary re-creations of the callback.

Also applies to: 241-249

apps/old-desktop/src/renderer/screens/main/utils.ts (1)

2-2: Good reuse of formatRelativeTime; confirm timestamp choice

Importing formatRelativeTime from the AddTaskModal utils keeps relative-time formatting consistent across the UI, and using it for task.lastUpdated is reasonable.

One thing to double‑check: lastUpdated is derived from worktree.createdAt. If the Worktree type ever gains an updatedAt/lastActivity field, that would likely be a better source for this value.

Also applies to: 98-99

apps/old-desktop/src/main/lib/workspace-ipcs.ts (1)

778-807: Consider explicit “workspace not found” handling in new-architecture IPCs

In workspace-activate and workspace-rescan, the code assumes workspaceOrch.get(input.workspaceId) returns a workspace and immediately reads workspace.type. If get can return null/undefined, this will throw and be caught by the generic catch, producing a less clear error.

You might prefer a direct guard like you use in earlier handlers:

- const workspace = await workspaceOrch.get(input.workspaceId);
- if (workspace.type !== "local") {
+ const workspace = await workspaceOrch.get(input.workspaceId);
+ if (!workspace) {
+   return {
+     success: false,
+     error: "Workspace not found",
+   };
+ }
+ if (workspace.type !== "local") {
    return {
      success: false,
      error: "Only local workspaces are supported",
    };
  }

Applying the same pattern to both IPCs would make error responses more predictable.

Also applies to: 851-881

apps/cli/src/components/LaunchOverlay.tsx (1)

13-41: Hoist animation frame arrays outside the component for stability/perf

The spinnerFrames and breathingFrames arrays are recreated on every render, and the effect depends on spinnerFrames.length. This works, but you can simplify and avoid per-render allocations by moving both arrays to module-level constants and changing the effect dependency to an empty array. That also makes the intent (“run once”) clearer.

-export function LaunchOverlay({ agentType, sessionName }: LaunchOverlayProps) {
-	const [frame, setFrame] = React.useState(0);
-
-	// Braille spinner frames for smooth animation
-	const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
-
-	// Breathing dots animation
-	const breathingFrames = [
-		"●",
-		"●○",
-		"●○○",
-		"○●○",
-		"○○●",
-		"○●",
-		"●",
-		"",
-	];
+const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
+const BREATHING_FRAMES = ["●", "●○", "●○○", "○●○", "○○●", "○●", "●", ""] as const;
+
+export function LaunchOverlay({ agentType, sessionName }: LaunchOverlayProps) {
+	const [frame, setFrame] = React.useState(0);
@@
-	React.useEffect(() => {
-		const interval = setInterval(() => {
-			setFrame((prev) => (prev + 1) % spinnerFrames.length);
-		}, 80);
-
-		return () => clearInterval(interval);
-	}, [spinnerFrames.length]);
-
-	const spinner = spinnerFrames[frame];
-	const breathing = breathingFrames[frame % breathingFrames.length];
+	React.useEffect(() => {
+		const interval = setInterval(() => {
+			setFrame((prev) => (prev + 1) % SPINNER_FRAMES.length);
+		}, 80);
+		return () => clearInterval(interval);
+	}, []);
+
+	const spinner = SPINNER_FRAMES[frame];
+	const breathing = BREATHING_FRAMES[frame % BREATHING_FRAMES.length];
apps/cli/src/lib/orchestrators/__tests__/environment-orchestrator.test.ts (1)

107-158: Prefer using the per-test tempDir for workspace paths

The updated create calls correctly use the new { path } options shape. To keep tests more isolated and avoid relying on global /tmp/* paths (and potential collisions across parallel runs), consider deriving these paths from the per-test tempDir you already create.

-			const workspace = await workspaceOrchestrator.create(
-				env.id,
-				WorkspaceType.LOCAL,
-				{ path: "/tmp/test" },
-			);
+			const workspace = await workspaceOrchestrator.create(
+				env.id,
+				WorkspaceType.LOCAL,
+				{ path: join(tempDir, "workspace") },
+			);
@@
-			const workspace = await workspaceOrchestrator.create(
-				env.id,
-				WorkspaceType.LOCAL,
-				{ path: "/tmp/test" },
-			);
+			const workspace = await workspaceOrchestrator.create(
+				env.id,
+				WorkspaceType.LOCAL,
+				{ path: join(tempDir, "workspace") },
+			);
@@
-			const ws1 = await workspaceOrchestrator.create(
-				env.id,
-				WorkspaceType.LOCAL,
-				{ path: "/tmp/test1" },
-			);
-			const ws2 = await workspaceOrchestrator.create(
-				env.id,
-				WorkspaceType.LOCAL,
-				{ path: "/tmp/test2" },
-			);
+			const ws1 = await workspaceOrchestrator.create(
+				env.id,
+				WorkspaceType.LOCAL,
+				{ path: join(tempDir, "ws1") },
+			);
+			const ws2 = await workspaceOrchestrator.create(
+				env.id,
+				WorkspaceType.LOCAL,
+				{ path: join(tempDir, "ws2") },
+			);
apps/cli/src/lib/storage/adapter.ts (1)

3-9: Narrow get/set to collection-like keys to avoid misuse on non-Record fields

CollectionValue is a nice improvement. Right now, though, get/set accept any keyof Database, which means you can (typesafely) call get("state", "id") even though state isn’t really an id-keyed collection.

You can tighten this by introducing a CollectionKey type that only includes Record-like collections and using that in get/set (and optionally getCollection/updateCollection).

-// Helper type to extract value type from Record collections
-type CollectionValue<K extends keyof Database> = Database[K] extends Record<
-	string,
-	infer V
->
-	? V
-	: never;
+// Helper types to work only with Record<string, V> collections
+type CollectionKey = {
+	[K in keyof Database]: Database[K] extends Record<string, any> ? K : never;
+}[keyof Database];
+
+type CollectionValue<K extends CollectionKey> = Database[K] extends Record<
+	string,
+	infer V
+>
+	? V
+	: never;
@@
-	get<K extends keyof Database>(
+	get<K extends CollectionKey>(
 		collection: K,
 		id: string,
 	): Promise<CollectionValue<K> | undefined>;
@@
-	set<K extends keyof Database>(
+	set<K extends CollectionKey>(
 		collection: K,
 		id: string,
 		value: CollectionValue<K>,
 	): Promise<void>;

Also applies to: 42-54

apps/cli/src/lib/storage/lowdb-adapter.ts (1)

155-156: Consider extracting repeated casting pattern.

The pattern of casting this.db!.data[collection] as Record<string, unknown> is repeated in get, set, delete, and has methods. Consider extracting this into a private helper method to reduce duplication.

Example refactor:

private getCollectionAsRecord<K extends keyof Database>(
  collection: K
): Record<string, unknown> {
  return this.db!.data[collection] as Record<string, unknown>;
}

Then use it as:

const coll = this.getCollectionAsRecord(collection);

Also applies to: 167-168, 178-179, 189-190

apps/cli/src/lib/launch/config.ts (2)

6-10: Document that default commands assume PATH availability.

The default launch commands ("claude", "codex", "cursor") assume these executables are in the user's PATH. Consider documenting this requirement in the docstring or README, and potentially validating command availability before attempting to launch.


28-28: Document the environment variable override mechanism.

The SUPERSET_AGENT_LAUNCH_<TYPE> environment variable pattern (e.g., SUPERSET_AGENT_LAUNCH_CLAUDE) should be documented in user-facing documentation so users know they can override default launch commands.

Also applies to: 50-50

apps/cli/src/lib/orchestrators/process-orchestrator.ts (1)

103-193: Consider centralizing “outdated launchCommand/sessionName” checks and caching tmux availability

needsPersist and backfillDefaults duplicate the heuristics for when an agent’s launchCommand/sessionName are missing or outdated. Similarly, isTmuxInstalled() shells out on every tmuxSessionExists call.

You could:

  • Extract a small helper like isLaunchConfigOutdated(agent: Agent): boolean to share between needsPersist and backfillDefaults.
  • Cache the result of isTmuxInstalled() in a module-level variable to avoid repeatedly running which tmux inside list() loops, while still keeping tmuxSessionExists synchronous.
apps/cli/src/lib/orchestrators/workspace-orchestrator.ts (1)

94-147: create options and auto-select behavior are coherent; consider using CloudWorkspace type

The new create(environmentId, type, options) signature and per-type validation (path for LOCAL, branch for CLOUD) are clear, and auto-selecting the created workspace via use() is a good default.

Since CloudWorkspace is now defined in types/workspace.ts, you could import it here and replace the as any cast for the CLOUD branch with a proper CloudWorkspace cast for stronger typing.

apps/cli/src/commands/dashboard.tsx (2)

57-82: Parallel data load is good; consider resetting error state on retry

loadDashboard correctly fetches workspaces, processes, and current workspace in parallel and updates state. However, once error is set in the catch block, it’s never cleared on subsequent successful refreshes, so the UI will stay in the error branch even if later calls succeed.
[signal_strength]

You can reset error at the start of loadDashboard:

	const loadDashboard = React.useCallback(async () => {
-		try {
+		try {
+			setError(null);
 			const db = getDb();

149-193: LaunchOverlay/launching state is currently unreachable

The launching and launchingAgent state, plus the LaunchOverlay branch, are never activated; the Enter handler exits the Ink app immediately and runs launchAgent via setTimeout, so the overlay path can’t be reached.

Either:

  • Remove launching/launchingAgent state and the LaunchOverlay branch to simplify the component, or
  • If you want the overlay, set launching/launchingAgent before calling exit() (or avoid exit() until after the overlay is shown) so the branch can actually render.

Also applies to: 226-236

apps/cli/src/lib/launch/run.ts (1)

17-38: Centralize tmux/session handling and consider portability

isTmuxInstalled, tmuxSessionExists, and the repeated sessionName derivation appear in multiple places. This is fine functionally, but you could:

  • Extract a small helper (e.g., getSessionName(agent)) to avoid any future drift between launchAgent and attachToAgent.
  • Optionally replace which tmux with a direct spawnSync("tmux", ["-V"])-style check if you want better portability to environments without which.

These are non-blocking but would improve maintainability and resilience.

Also applies to: 69-72, 140-144

apps/cli/src/types/process.ts (1)

47-62: ProcessOrchestrator interface still models async methods as synchronous

update, stop, and delete are typed as returning void, while the concrete ProcessOrchestrator implementation is async and returns Promise<void>. stopAll was updated to Promise<number>, but the others remain out of sync.

For callers typed against this interface, this can be misleading (e.g., no await implied, no way to know when mutations persist). Consider adjusting to:

update: (id: string, process: Partial<Process>) => Promise<void>;
stop: (id: string) => Promise<void>;
delete: (id: string) => Promise<void>;

to reflect actual behavior and avoid subtle type mismatches.

apps/cli/src/commands/init.tsx (1)

58-88: Duplicate path/branch checks ignore environment and silently swallow DB errors

The duplicate detection for paths and branches is reasonable, but two behaviors are worth verifying:

  • You treat duplicates globally across all environments (orchestrator.list() with no environment filter). If workspaces are meant to be unique per environment rather than globally, this will reject some valid setups.
  • The catch (err) { /* Ignore database errors during validation */ } blocks mean DB issues will be silently ignored and the flow will proceed as if there were no duplicates, only to fail later when creating the workspace.

If you care about these cases, consider:

  • Filtering by environmentId when checking duplicates, or explicitly documenting that path/branch must be globally unique.
  • At least logging DB validation errors to stderr so they're not completely invisible.

Also applies to: 100-135

apps/cli/src/commands/panels.tsx (1)

79-128: Index wrap-around logic assumes non-empty arrays but degrades gracefully

The navigation logic wraps indices with filteredAgents.length - 1 and data.workspaces.length - 1. When the corresponding arrays are empty, the index can become -1, which then yields undefined for selectedWorkspace/selectedAgent.

Because you guard usages (selectedAgent && ...) and treat undefined as “no selection,” this is not a functional bug, but it is slightly awkward state. If you want to tighten it up, you could early-return in the navigation branches when the relevant list is empty, or clamp indices to 0 when length === 0.

Also applies to: 186-194

apps/cli/src/commands/agent.tsx (4)

276-341: Workspace-scoped AgentStopAll behavior looks correct, but consider exposing the resolved workspace id

AgentStopAll correctly:

  • Resolves a target workspace (explicit workspaceId or current workspace via WorkspaceOrchestrator.getCurrent()),
  • Filters to non-terminated ProcessType.AGENT processes for that workspace, and
  • Stops them sequentially via processOrchestrator.stop.

On success you show the count, and when called with workspaceId you display a truncated id. For consistency, you might also want to show the resolved workspace when using the current workspace path (e.g., render the id/name from targetWorkspaceId) so users know which context was affected.

Also applies to: 351-361


482-503: Avoid re-importing launchAgent inside the per-agent loop

Inside startAgents you perform a dynamic import of "../lib/launch/run" on every iteration:

const agent = process as import("../types/process").Agent;
const { launchAgent } = await import("../lib/launch/run");
const result = await launchAgent(agent, { attach: false });

Because Node caches modules this won’t be disastrous, but it’s unnecessary work. You can:

  • Hoist the dynamic import out of the loop (once per startAgents call), or
  • Prefer a static top-level import if circular dependencies aren’t a concern.

That keeps the loop focused on launching agents and simplifies reasoning about side effects.


430-474: AgentStart ignores the onComplete callback and relies solely on process exit

AgentStart accepts onComplete but never calls it; instead it uses exit() from Ink and a setTimeout to exit the app after showing success. This is fine for direct CLI usage, but if you ever embed AgentStart within a higher-level Ink tree or tests that rely on onComplete, they’ll never be notified.

If composability is desired, consider invoking onComplete when:

  • You transition to StartStep.COMPLETE, or
  • Right before calling exit().

You can still keep the auto-exit behavior while giving callers a clean completion signal.

Also applies to: 533-551, 596-621


631-699: AgentAttach correctly resolves by id or sessionName, but also drops onComplete

The attach flow is solid:

  • Tries ProcessOrchestrator.get(id), then falls back to searching agents by sessionName.
  • Verifies the process is an agent before launching.
  • Uses launchAgent(agent, { attach: true }) with a small delay after exiting Ink, then exits the Node process based on the result.

Similar to AgentStart, the onComplete prop is accepted but not used. If you plan to embed this component, consider invoking onComplete before calling globalThis.process.exit(...) or after a successful attach/return so callers can hook into completion.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b036cd3 and fd0fbf2.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (75)
  • WARP.md (1 hunks)
  • apps/cli/package.json (2 hunks)
  • apps/cli/readme.md (1 hunks)
  • apps/cli/src/cli.tsx (8 hunks)
  • apps/cli/src/commands/agent.tsx (5 hunks)
  • apps/cli/src/commands/dashboard.tsx (1 hunks)
  • apps/cli/src/commands/index.ts (1 hunks)
  • apps/cli/src/commands/init.tsx (1 hunks)
  • apps/cli/src/commands/panels.tsx (1 hunks)
  • apps/cli/src/commands/workspace.tsx (3 hunks)
  • apps/cli/src/components/LaunchOverlay.tsx (1 hunks)
  • apps/cli/src/lib/db.ts (1 hunks)
  • apps/cli/src/lib/launch/config.ts (1 hunks)
  • apps/cli/src/lib/launch/run.ts (1 hunks)
  • apps/cli/src/lib/orchestrators/__tests__/change-orchestrator.test.ts (8 hunks)
  • apps/cli/src/lib/orchestrators/__tests__/environment-orchestrator.test.ts (3 hunks)
  • apps/cli/src/lib/orchestrators/__tests__/process-orchestrator.test.ts (16 hunks)
  • apps/cli/src/lib/orchestrators/__tests__/workspace-orchestrator.test.ts (7 hunks)
  • apps/cli/src/lib/orchestrators/process-orchestrator.ts (7 hunks)
  • apps/cli/src/lib/orchestrators/workspace-orchestrator.ts (3 hunks)
  • apps/cli/src/lib/storage/__tests__/lowdb-adapter.test.ts (3 hunks)
  • apps/cli/src/lib/storage/adapter.ts (2 hunks)
  • apps/cli/src/lib/storage/lowdb-adapter.ts (5 hunks)
  • apps/cli/src/lib/storage/types.ts (3 hunks)
  • apps/cli/src/types/process.ts (2 hunks)
  • apps/cli/src/types/workspace.ts (1 hunks)
  • apps/desktop/src/renderer/screens/main/components/TabView/Sidebar/ModeCarousel/ModeNavigation.tsx (1 hunks)
  • apps/desktop/src/renderer/screens/main/components/TabView/Sidebar/ModeCarousel/index.tsx (1 hunks)
  • apps/old-desktop/src/main/index.ts (1 hunks)
  • apps/old-desktop/src/main/lib/desktop-stores.ts (1 hunks)
  • apps/old-desktop/src/main/lib/migration/migrator.ts (1 hunks)
  • apps/old-desktop/src/main/lib/storage/index.ts (1 hunks)
  • apps/old-desktop/src/main/lib/storage/lowdb-adapter.ts (1 hunks)
  • apps/old-desktop/src/main/lib/storage/orchestrators/index.ts (1 hunks)
  • apps/old-desktop/src/main/lib/types/cli-types.ts (1 hunks)
  • apps/old-desktop/src/main/lib/ui-store/index.ts (1 hunks)
  • apps/old-desktop/src/main/lib/ui-store/store.ts (1 hunks)
  • apps/old-desktop/src/main/lib/ui-store/types.ts (1 hunks)
  • apps/old-desktop/src/main/lib/workspace-composition/composer.ts (1 hunks)
  • apps/old-desktop/src/main/lib/workspace-composition/index.ts (1 hunks)
  • apps/old-desktop/src/main/lib/workspace-composition/types.ts (1 hunks)
  • apps/old-desktop/src/main/lib/workspace-ipcs.ts (1 hunks)
  • apps/old-desktop/src/renderer/contexts/AppProviders.tsx (1 hunks)
  • apps/old-desktop/src/renderer/contexts/TabContext.tsx (1 hunks)
  • apps/old-desktop/src/renderer/contexts/TaskContext.tsx (1 hunks)
  • apps/old-desktop/src/renderer/contexts/WorktreeOperationsContext.tsx (1 hunks)
  • apps/old-desktop/src/renderer/contexts/index.ts (1 hunks)
  • apps/old-desktop/src/renderer/screens/main/MainScreen.tsx (1 hunks)
  • apps/old-desktop/src/renderer/screens/main/components/Layout/AddTaskModal/CreatingView.tsx (1 hunks)
  • apps/old-desktop/src/renderer/screens/main/components/Layout/AddTaskModal/TaskList.tsx (1 hunks)
  • apps/old-desktop/src/renderer/screens/main/components/Layout/AddTaskModal/index.ts (1 hunks)
  • apps/old-desktop/src/renderer/screens/main/components/Layout/AddTaskModal/types.ts (1 hunks)
  • apps/old-desktop/src/renderer/screens/main/components/Layout/AddTaskModal/useTaskForm.ts (1 hunks)
  • apps/old-desktop/src/renderer/screens/main/components/Layout/AddTaskModal/utils.ts (1 hunks)
  • apps/old-desktop/src/renderer/screens/main/components/MainContent/TabContent.tsx (1 hunks)
  • apps/old-desktop/src/renderer/screens/main/components/MainContent/TabGroup.tsx (1 hunks)
  • apps/old-desktop/src/renderer/screens/main/components/MainContentArea/MainContentArea.tsx (1 hunks)
  • apps/old-desktop/src/renderer/screens/main/components/PlanView/PlanView.tsx (1 hunks)
  • apps/old-desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx (1 hunks)
  • apps/old-desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/index.ts (1 hunks)
  • apps/old-desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/WorktreeItem.tsx (1 hunks)
  • apps/old-desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/WorktreeItemArborist.tsx (2 hunks)
  • apps/old-desktop/src/renderer/screens/main/hooks/useTasks.ts (1 hunks)
  • apps/old-desktop/src/renderer/screens/main/utils.ts (1 hunks)
  • apps/old-desktop/src/shared/ipc-channels/index.ts (1 hunks)
  • apps/old-desktop/src/shared/ipc-channels/ui.ts (1 hunks)
  • apps/website/src/app/components/HeroSection/HeroSection.tsx (1 hunks)
  • apps/website/src/app/components/HeroSection/components/HeroCanvas/components/LitBackground/LitBackground.tsx (1 hunks)
  • apps/website/src/app/page.tsx (1 hunks)
  • packages/ui/src/components/context-menu.tsx (1 hunks)
  • packages/ui/src/components/dialog.tsx (1 hunks)
  • packages/ui/src/components/dropdown-menu.tsx (1 hunks)
  • packages/ui/src/components/resizable.tsx (1 hunks)
  • packages/ui/src/components/select.tsx (1 hunks)
  • packages/ui/src/components/separator.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (16)
apps/cli/src/lib/db.ts (1)
apps/cli/src/lib/storage/lowdb-adapter.ts (1)
  • LowdbAdapter (25-198)
apps/cli/src/commands/workspace.tsx (3)
apps/cli/src/lib/db.ts (1)
  • getDb (9-18)
apps/cli/src/lib/orchestrators/workspace-orchestrator.ts (1)
  • WorkspaceOrchestrator (14-224)
apps/cli/src/types/workspace.ts (1)
  • WorkspaceOrchestrator (28-50)
apps/cli/src/lib/launch/config.ts (1)
apps/cli/src/types/process.ts (1)
  • Agent (41-45)
apps/cli/src/lib/storage/adapter.ts (1)
apps/cli/src/lib/storage/types.ts (1)
  • Database (15-25)
apps/cli/src/lib/launch/run.ts (2)
apps/cli/src/types/process.ts (1)
  • Agent (41-45)
apps/cli/src/lib/launch/config.ts (1)
  • getLaunchCommand (19-42)
apps/cli/src/commands/init.tsx (3)
apps/cli/src/lib/db.ts (1)
  • getDb (9-18)
apps/cli/src/lib/orchestrators/workspace-orchestrator.ts (1)
  • WorkspaceOrchestrator (14-224)
apps/cli/src/types/workspace.ts (1)
  • WorkspaceOrchestrator (28-50)
apps/cli/src/lib/orchestrators/process-orchestrator.ts (3)
apps/cli/src/types/process.ts (2)
  • Agent (41-45)
  • Process (15-29)
apps/cli/src/lib/launch/config.ts (1)
  • getDefaultLaunchCommand (48-58)
apps/cli/src/types/workspace.ts (1)
  • Workspace (6-16)
apps/cli/src/cli.tsx (7)
apps/cli/src/commands/init.tsx (1)
  • Init (37-397)
apps/cli/src/commands/dashboard.tsx (1)
  • Dashboard (29-555)
apps/cli/src/commands/panels.tsx (1)
  • Panels (28-486)
apps/cli/src/commands/workspace.tsx (1)
  • WorkspaceUse (232-280)
apps/cli/src/commands/agent.tsx (3)
  • AgentStart (430-624)
  • AgentAttach (631-710)
  • AgentStopAll (281-365)
apps/cli/src/lib/db.ts (1)
  • getDb (9-18)
apps/cli/src/lib/orchestrators/workspace-orchestrator.ts (1)
  • WorkspaceOrchestrator (14-224)
apps/cli/src/commands/panels.tsx (4)
apps/cli/src/types/process.ts (3)
  • Process (15-29)
  • ProcessOrchestrator (47-62)
  • Agent (41-45)
apps/cli/src/lib/db.ts (1)
  • getDb (9-18)
apps/cli/src/lib/orchestrators/process-orchestrator.ts (1)
  • ProcessOrchestrator (50-328)
apps/cli/src/lib/launch/run.ts (1)
  • launchAgent (47-129)
apps/cli/src/lib/storage/lowdb-adapter.ts (1)
apps/cli/src/lib/storage/types.ts (3)
  • Database (15-25)
  • SerializedDatabase (31-41)
  • createEmptyDatabase (64-76)
apps/cli/src/lib/storage/types.ts (1)
apps/cli/src/lib/storage/index.ts (2)
  • createEmptyDatabase (5-5)
  • SerializedDatabase (4-4)
apps/cli/src/types/workspace.ts (1)
apps/cli/src/lib/orchestrators/workspace-orchestrator.ts (1)
  • WorkspaceOrchestrator (14-224)
apps/cli/src/lib/orchestrators/workspace-orchestrator.ts (2)
apps/cli/src/lib/orchestrators/process-orchestrator.ts (1)
  • needsPersist (103-118)
apps/cli/src/types/workspace.ts (2)
  • Workspace (6-16)
  • LocalWorkspace (18-21)
apps/cli/src/commands/dashboard.tsx (7)
apps/cli/src/types/workspace.ts (2)
  • Workspace (6-16)
  • WorkspaceOrchestrator (28-50)
apps/cli/src/types/process.ts (3)
  • Process (15-29)
  • Agent (41-45)
  • ProcessOrchestrator (47-62)
apps/cli/src/lib/db.ts (1)
  • getDb (9-18)
apps/cli/src/lib/orchestrators/workspace-orchestrator.ts (1)
  • WorkspaceOrchestrator (14-224)
apps/cli/src/lib/orchestrators/process-orchestrator.ts (1)
  • ProcessOrchestrator (50-328)
apps/cli/src/lib/launch/run.ts (1)
  • launchAgent (47-129)
apps/cli/src/components/LaunchOverlay.tsx (1)
  • LaunchOverlay (13-70)
apps/old-desktop/src/renderer/screens/main/components/PlanView/PlanView.tsx (1)
packages/api/src/index.ts (1)
  • RouterOutputs (9-9)
apps/cli/src/commands/agent.tsx (6)
apps/cli/src/lib/orchestrators/process-orchestrator.ts (2)
  • stopAll (275-312)
  • ProcessOrchestrator (50-328)
apps/cli/src/lib/db.ts (1)
  • getDb (9-18)
apps/cli/src/types/process.ts (2)
  • ProcessOrchestrator (47-62)
  • Process (15-29)
apps/cli/src/lib/orchestrators/workspace-orchestrator.ts (1)
  • WorkspaceOrchestrator (14-224)
apps/cli/src/lib/launch/config.ts (1)
  • getDefaultLaunchCommand (48-58)
apps/cli/src/lib/launch/run.ts (1)
  • launchAgent (47-129)
🔇 Additional comments (83)
apps/old-desktop/src/renderer/screens/main/components/MainContent/TabContent.tsx (1)

3-3: Import reordering aligns with project conventions.

The hook imports have been reordered to group tab-related hooks before workspace-related hooks, consistent with the broader refactoring pattern across the codebase. No functional changes.

apps/old-desktop/src/renderer/screens/main/components/MainContentArea/MainContentArea.tsx (1)

9-10: LGTM! Non-functional import reordering.

The import statements have been reorganized without any functional impact on the component.

Also applies to: 13-14

apps/website/src/app/components/HeroSection/components/HeroCanvas/components/LitBackground/LitBackground.tsx (1)

9-17: LGTM! Clean import organization.

The import reordering logically groups related imports together (config imports followed by shader imports) without any functional changes.

packages/ui/src/components/dropdown-menu.tsx (1)

3-3: LGTM - Import ordering improvement.

The icon imports have been moved to follow the type import, improving consistency with TypeScript conventions. This change has no functional impact.

packages/ui/src/components/dialog.tsx (1)

3-3: LGTM - Consistent import ordering.

The icon import now follows the type import, aligning with the standardized import sequence across the UI package.

packages/ui/src/components/select.tsx (1)

4-4: LGTM - Type-only import addition.

Adding the explicit type-only import for React is a good practice that can improve tree-shaking and makes the type-only usage explicit.

packages/ui/src/components/resizable.tsx (1)

2-2: LGTM - Import ordering refinement.

Moving the icon import after the type import maintains consistency with the standardized import sequence.

packages/ui/src/components/separator.tsx (1)

2-2: LGTM - Correct type-only import.

Converting to a type-only import is appropriate since React is only used for type annotations in this component, which can improve bundle size through better tree-shaking.

packages/ui/src/components/context-menu.tsx (1)

3-3: LGTM - Completes import standardization.

This import reordering completes the consistent import sequencing pattern across all reviewed UI components. The changes improve code organization without affecting functionality.

apps/old-desktop/src/renderer/screens/main/components/PlanView/PlanView.tsx (1)

6-6: LGTM – Minor stylistic changes with no functional impact.

The import reordering and the conversion from interface to type for the empty props object are both cosmetic changes that don't affect functionality.

Also applies to: 14-14

apps/old-desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/WorktreeItem.tsx (1)

21-21: dragDropManager wiring into <Tree> looks correct

Using the shared dragDropManager instance via dndManager={dragDropManager} is the right pattern for react‑arborist DnD; the added import cleanly resolves that dependency with no other behavioral changes.

Also applies to: 1067-1072

apps/old-desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/WorktreeItemArborist.tsx (1)

10-11: Imports and DnD wiring are consistent with the main WorktreeItem

Type‑only NodeApi/TreeApi imports plus a value import for Tree, and using the shared dragDropManager instance, match the pattern in the primary WorktreeItem implementation and should integrate cleanly with react‑arborist and your shared DnD setup.

Also applies to: 20-24

apps/website/src/app/components/HeroSection/HeroSection.tsx (1)

1-3: LGTM!

Import reordering is a safe, non-functional change that improves code organization.

apps/desktop/src/renderer/screens/main/components/TabView/Sidebar/ModeCarousel/index.tsx (1)

1-8: LGTM!

Import reordering maintains consistency without affecting functionality.

apps/old-desktop/src/renderer/contexts/WorktreeOperationsContext.tsx (1)

1-6: LGTM!

Import reordering is consistent with the broader refactoring across context files.

apps/desktop/src/renderer/screens/main/components/TabView/Sidebar/ModeCarousel/ModeNavigation.tsx (1)

1-4: LGTM!

Consolidating the AnimatedBackground import to a single location at the top eliminates duplication and improves maintainability.

apps/old-desktop/src/renderer/screens/main/components/Layout/AddTaskModal/CreatingView.tsx (1)

1-5: LGTM!

Import reordering aligns with the PR's broader organizational improvements.

apps/old-desktop/src/renderer/screens/main/hooks/useTasks.ts (1)

1-5: LGTM!

Type import reordering maintains consistency without affecting behavior.

apps/old-desktop/src/renderer/contexts/TabContext.tsx (1)

1-5: LGTM!

Placing React imports after type imports follows a common convention and improves consistency.

WARP.md (1)

1-1: Review comment is based on incorrect file content observation.

WARP.md actually contains @AGENTS.md, not CLAUDE.md as stated in the review. Both WARP.md and CLAUDE.md use the @ prefix syntax to reference AGENTS.md (a guide document for the Superset monorepo). This appears to be intentional reference syntax rather than an error, placeholder, or misnamed content. Please re-examine the actual file content and revise your review if necessary.

Likely an incorrect or invalid review comment.

apps/old-desktop/src/main/lib/storage/lowdb-adapter.ts (1)

6-6: Config-based collection path wiring is correct

Importing and using getDomainCollectionPath for initCollection keeps collection file locations centralized in config, which is safer than hardcoding paths in the adapter and aligns with the rest of the storage setup.

Also applies to: 27-33

apps/old-desktop/src/main/lib/workspace-ipcs.ts (1)

12-15: Window manager import placement is fine

Moving windowManager into the main import block and using it for the window‑scoped workspace IPCs keeps the module layout consistent and avoids duplicate imports without changing behavior.

Also applies to: 207-231

apps/old-desktop/src/main/lib/ui-store/store.ts (1)

1-10: Atomic write helpers and fs imports are aligned

Bringing closeSync into the node:fs named imports matches its use in atomicWrite and ensures the temp file descriptor is always closed in the finally block before the atomic rename. The rest of the import reordering is purely cosmetic.

Also applies to: 52-68

apps/old-desktop/src/main/lib/ui-store/types.ts (1)

1-1: Type import reorder is harmless

Reordering the MosaicNode/Tab type import from shared/types doesn’t alter any of the exported UI store types and keeps the dependencies explicit.

apps/old-desktop/src/main/lib/ui-store/index.ts (1)

1-2: Re-export ordering is fine

Exporting ./store before ./types is purely organizational and keeps the public surface unchanged for consumers importing from this module.

apps/old-desktop/src/renderer/contexts/TaskContext.tsx (1)

5-8: Task context wiring remains correct after import reordering

Reordering the type and context imports doesn’t change behavior, and TaskProvider still correctly passes currentWorkspace, setSelectedWorktreeId, handleTabSelect, and handleWorktreeCreatedWithResult into useTasks, then exposes that via the context.

Also applies to: 40-50

apps/cli/readme.md (1)

1-45: Excellent documentation expansion!

The new documentation is comprehensive, well-organized, and provides clear guidance on:

  • Quick start and development workflow
  • Core concepts (Environment, Workspace, Agent, Change)
  • Common command patterns with examples
  • tmux integration details
  • Storage schema and tips

This significantly improves the developer experience for the CLI.

apps/cli/package.json (3)

5-7: LGTM: Correct binary naming.

The change from a string to an object with a named binary is the correct approach for npm package binaries.


36-36: LGTM: Dependency addition aligns with interactive features.

The ink-text-input dependency supports the new interactive CLI components mentioned in the PR summary.


13-13: I need to gather more information to verify this change. Let me run additional verification scripts and search for documentation on the bun flag that was removed.

Removal of --packages=bundle flag is safe—it specifies default behavior.

The --packages parameter controls whether package dependencies are included in the bundle, with "bundle" being the default value. Removing the redundant flag has no impact on dependency bundling or CLI portability; the CLI will continue bundling dependencies as expected. No other references to this flag were found in the codebase, and the build output behavior remains unchanged.

apps/old-desktop/src/main/lib/storage/orchestrators/index.ts (1)

1-4: LGTM: Export reordering.

This is a non-functional reordering of exports with no runtime impact.

apps/old-desktop/src/main/lib/storage/index.ts (1)

1-5: LGTM: Export reordering.

This is a non-functional reordering of exports with no runtime impact.

apps/old-desktop/src/main/index.ts (1)

3-3: LGTM: Import reordering.

This is a non-functional reordering of imports with no runtime impact.

apps/old-desktop/src/renderer/screens/main/components/Layout/AddTaskModal/useTaskForm.ts (1)

3-3: LGTM: Import reordering.

This is a non-functional reordering of imports with no runtime impact.

apps/cli/src/lib/orchestrators/__tests__/change-orchestrator.test.ts (1)

37-37: LGTM: Test updates align with new API.

The tests have been correctly updated to pass workspace creation options as an object { path: string } instead of a primitive string, aligning with the WorkspaceOrchestrator.create API evolution that now accepts an options object.

Also applies to: 59-59, 71-71, 76-76, 100-100, 127-127, 154-154, 173-173, 212-212

apps/old-desktop/src/renderer/screens/main/components/Layout/AddTaskModal/types.ts (1)

2-2: LGTM: Import reordering.

This is a non-functional reordering of imports with no runtime impact.

apps/old-desktop/src/main/lib/workspace-composition/index.ts (1)

1-2: LGTM!

The export reordering has no functional impact and the changes are safe.

apps/old-desktop/src/shared/ipc-channels/index.ts (1)

16-16: LGTM!

The import reordering has no functional impact.

apps/old-desktop/src/main/lib/workspace-composition/types.ts (1)

3-3: LGTM!

The import reordering has no functional impact.

apps/old-desktop/src/renderer/screens/main/components/Layout/AddTaskModal/utils.ts (1)

3-3: LGTM!

The import reordering has no functional impact.

apps/old-desktop/src/renderer/screens/main/components/Layout/AddTaskModal/TaskList.tsx (1)

13-13: LGTM!

The import reordering has no functional impact.

apps/old-desktop/src/renderer/contexts/AppProviders.tsx (1)

5-8: LGTM!

The import reordering has no functional impact.

apps/old-desktop/src/shared/ipc-channels/ui.ts (1)

5-5: LGTM!

Using explicit type-only imports is good practice and has no functional impact.

apps/website/src/app/page.tsx (3)

4-5: LGTM!

The new component imports are properly added.

Also applies to: 10-10


13-13: LGTM!

The state management for the waitlist modal is correctly initialized.


20-28: LGTM!

The component integration and modal state management are correctly implemented. The handlers properly control the modal visibility through state updates.

apps/old-desktop/src/main/lib/workspace-composition/composer.ts (1)

4-5: LGTM! Import organization improved.

The addition of the WorktreeUiMetadata type import and reordering of imports improves code organization without affecting runtime behavior.

apps/cli/src/lib/orchestrators/__tests__/workspace-orchestrator.test.ts (1)

35-37: LGTM! Tests properly updated for new API signature.

All test calls to WorkspaceOrchestrator.create have been consistently updated to pass an options object { path: "..." } instead of a direct path string, correctly reflecting the API change mentioned in the PR summary.

Also applies to: 58-60, 81-86, 98-103, 114-116, 128-130, 138-140, 155-157

apps/cli/src/lib/db.ts (1)

7-7: LGTM! Cleaner separation of concerns.

The refactoring properly delegates database path resolution to the LowdbAdapter and storage config layer. The updated comment accurately reflects that the path defaults to ~/.superset/cli/db.json via the config system.

Also applies to: 14-15

apps/old-desktop/src/renderer/screens/main/components/Layout/AddTaskModal/index.ts (1)

5-5: LGTM! Export reordering with no functional impact.

The change only reorders the exported types and does not affect functionality or the public API surface.

apps/old-desktop/src/main/lib/migration/migrator.ts (1)

2-4: LGTM! Import organization improved.

The import reordering improves code organization without any runtime impact. Grouping related imports together follows best practices.

Also applies to: 10-11

apps/old-desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx (1)

6-11: LGTM! Import organization improved.

The reordering of context imports aligns with the centralized context module structure without affecting component behavior.

apps/cli/src/lib/storage/__tests__/lowdb-adapter.test.ts (1)

7-7: LGTM! Tests properly updated for new Process interface.

The addition of status: ProcessStatus.RUNNING to test Process objects correctly reflects the new status field in the Process interface mentioned in the PR summary.

Also applies to: 127-127, 147-147

apps/old-desktop/src/renderer/screens/main/MainScreen.tsx (1)

1-1: LGTM! Import additions support new keyboard shortcut functionality.

The expanded imports provide the necessary context hooks and utility functions to enable the keyboard shortcut handling implemented in the useEffect hook (lines 88-134). The changes are additive and properly integrated.

Also applies to: 3-12

apps/cli/src/commands/index.ts (1)

1-22: CLI command barrel updates look consistent

The added exports (AgentAttach/AgentStart, Dashboard, Init, Panels, WorkspaceUse) cleanly expand the public CLI surface without affecting existing commands. As long as the underlying modules define these symbols, this barrel looks good.

apps/old-desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/index.ts (1)

2-2: LGTM: Export reordering only.

This is a non-functional change that only affects the order of type exports.

apps/cli/src/lib/orchestrators/__tests__/process-orchestrator.test.ts (3)

6-6: LGTM: ProcessStatus import added for type safety.

Good addition of the ProcessStatus enum import to replace string literals in test assertions.


39-39: LGTM: Workspace creation updated to use options object.

The updated API signature create(environmentId, type, { path }) is more extensible than passing the path directly and aligns with the broader API changes in the PR.

Also applies to: 60-60, 81-81, 110-110, 131-131, 136-136, 154-154, 175-175, 207-207, 225-225, 247-247, 272-272, 297-297, 313-313


71-71: LGTM: Using ProcessStatus enum instead of string literals.

This improves type safety and makes status checks more maintainable.

Also applies to: 236-236, 264-264

apps/cli/src/lib/storage/types.ts (2)

22-24: LGTM: State field added for current workspace tracking.

The addition of state.currentWorkspaceId enables the app to persist which workspace is currently active, aligning with the workspace selection features introduced in this PR.

Also applies to: 38-40


65-69: Default environment initialization is correct—all required fields are present.

The Environment interface contains only one required field: id: string. The default environment initialization at lines 65-69 provides this field with id: "default". No additional mandatory fields like createdAt or updatedAt are defined in the Environment type, so the concern about missing required fields is not applicable here.

apps/cli/src/commands/workspace.tsx (3)

140-140: LGTM: Updated to use options object.

The change from passing path directly to { path } aligns with the new workspace orchestrator API that accepts an options object for more flexibility.


165-171: Good UX: Clear feedback and next steps.

The added guidance helps users understand that the workspace is now current and what to do next.


227-280: LGTM: Well-structured WorkspaceUse component.

The component properly:

  • Handles loading and error states
  • Verifies workspace exists before setting it as current
  • Updates lastUsedAt timestamp via orchestrator.use()
  • Provides clear user feedback
apps/cli/src/lib/storage/lowdb-adapter.ts (2)

47-58: LGTM: Ensures default database is written on first initialization.

The logic to check if the database file exists and write the default structure if it doesn't is a good addition that ensures the database is properly initialized.


13-19: No changes required—the type duplication is intentional and correct.

The CollectionValue type in adapter.ts is not exported, so lowdb-adapter.ts cannot import it. Each file correctly maintains its own scope-local definition. This is the proper design: adapter.ts uses the type in its interface definition, and lowdb-adapter.ts uses it independently in its class implementation. No consolidation is needed.

apps/cli/src/lib/launch/config.ts (1)

19-42: LGTM: Clear priority hierarchy for launch command resolution.

The function properly implements a clear precedence chain: agent-specific command → environment override → user config (TODO) → default.

apps/old-desktop/src/main/lib/types/cli-types.ts (3)

6-11: LGTM: FileDiff added to change exports.

Good addition of FileDiff to the public exports for change-related types.


20-25: LGTM: Expanded process-related exports.

The addition of Process, ProcessType, and Terminal to the type exports, along with the named export of ProcessType, properly exposes these types for desktop app usage.


27-31: LGTM: Workspace exports consolidated.

Good organization to have all workspace-related exports in a dedicated block.

apps/old-desktop/src/renderer/contexts/index.ts (2)

3-6: LGTM: Additional context providers exported.

Good consolidation of context exports in the barrel file, making these providers and hooks easier to import from a single location.


9-9: LGTM: WorktreeOperationsProvider added to exports.

Good catch adding the provider alongside the hook for consistency with other context exports.

apps/cli/src/cli.tsx (5)

42-65: New init, dashboard, and panels commands are wired cleanly

The new commands correctly render the corresponding Ink components and keep the CLI surface coherent. The split between non-interactive (env/workspace/agent/change) and interactive (init/dashboard/panels) flows is clear and consistent.


103-172: workspace use command integrates well with the orchestrator

The workspace use <id> command cleanly delegates to the WorkspaceUse Ink component and exits the process on completion, aligning with the new “current workspace” concept in the orchestrator.


201-218: Agent start and attach commands line up with the new lifecycle flows

The start command’s optional workspaceId argument and the attach command’s id/session name argument match the behavior described for AgentStart and AgentAttach, and error/exit handling is delegated to those components appropriately.


271-284: stop-all description and option now match AgentStopAll’s semantics

Updating the description and adding the --workspace option to pass workspaceId into AgentStopAll correctly reflects the behavior of stopping agents scoped to a workspace or the current workspace when omitted.


335-381: Default root action provides a helpful, workspace-aware entrypoint

The new root program.action that shows a welcome message, current workspace (including path/branch), and a concise “getting started” command list is a solid UX improvement. Dynamic imports keep startup cheap when subcommands are used.

apps/cli/src/types/workspace.ts (1)

6-16: Workspace metadata and CloudWorkspace typing look consistent

Adding name, description, timestamps, lastUsedAt, and defaultAgents, plus introducing CloudWorkspace with a branch field, lines up with how WorkspaceOrchestrator creates/backfills workspaces. This gives the CLI enough structure to drive the new interactive flows.

Also applies to: 23-26

apps/cli/src/lib/orchestrators/process-orchestrator.ts (3)

19-45: tmux presence/session helpers are reasonable for a CLI context

Using isTmuxInstalled + tmuxSessionExists based on execSync keeps status checks simple and synchronous, which is acceptable for a terminal CLI tool, especially since errors are safely caught and mapped to false.


53-101: Backfill + tmux-based status sync in get/list is well-structured

The flow of backfillDefaultssyncAgentStatus → conditional storage.set in both get and list cleanly keeps persisted processes in sync with reality (status, timestamps, launchCommand, sessionName) without over-writing on every read.


246-312: stop and stopAll handle tmux cleanup and status transitions appropriately

Both methods now kill tmux sessions when present, mark agents as STOPPED with endedAt/updatedAt, and stopAll returns a useful stoppedCount. Errors from tmux kill-session are swallowed safely so stale sessions don’t break the orchestrator.

apps/cli/src/lib/orchestrators/workspace-orchestrator.ts (1)

17-49: Default backfilling and listing behavior matches the extended Workspace shape

Using backfillDefaults in get/list to ensure createdAt, updatedAt, and defaultAgents are always populated (and persisting only from get when needed) nicely smooths migration from older DBs without excessive writes.

apps/cli/src/types/process.ts (1)

8-13: Process/Agent type extensions look consistent and useful

Adding ProcessStatus, pid, lastHeartbeat, launchCommand, and discriminated type plus sessionName on Agent lines up well with the orchestrator logic and new CLI features. The shapes are clear and should make downstream status-driven logic and tmux integration straightforward.

Also applies to: 19-19, 26-29, 31-45

Comment thread apps/cli/src/commands/agent.tsx
Comment on lines +260 to +271
// Categorize agents by status
const runningAgents = filteredAgents.filter(
(a) => a.status === ProcessStatus.RUNNING || !a.endedAt,
);
const idleAgents = filteredAgents.filter(
(a) => a.status === ProcessStatus.IDLE && !a.endedAt,
);
const stoppedAgents = filteredAgents.filter((a) => a.endedAt);
const errorAgents = filteredAgents.filter(
(a) => a.status === ProcessStatus.ERROR,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Running/idle agent counts treat all non-ended agents as “running”

Both the global summary and per-workspace counts use predicates like:

const runningAgents = filteredAgents.filter(
  (a) => a.status === ProcessStatus.RUNNING || !a.endedAt,
);

and

const wsRunning = wsAgents.filter(
  (a) => a.status === ProcessStatus.RUNNING || !a.endedAt,
);

This classifies all agents without endedAt (including IDLE agents) as running, so idle agents are double-counted as both running and idle, and workspace “N running” labels are misleading.

Filter running agents strictly by status (optionally also checking !endedAt) and keep idle separate. For example:

-	const runningAgents = filteredAgents.filter(
-		(a) => a.status === ProcessStatus.RUNNING || !a.endedAt,
-	);
-	const idleAgents = filteredAgents.filter(
-		(a) => a.status === ProcessStatus.IDLE && !a.endedAt,
-	);
+	const runningAgents = filteredAgents.filter(
+		(a) => a.status === ProcessStatus.RUNNING && !a.endedAt,
+	);
+	const idleAgents = filteredAgents.filter(
+		(a) => a.status === ProcessStatus.IDLE && !a.endedAt,
+	);
@@
-							const wsRunning = wsAgents.filter(
-								(a) => a.status === ProcessStatus.RUNNING || !a.endedAt,
-							);
+							const wsRunning = wsAgents.filter(
+								(a) => a.status === ProcessStatus.RUNNING && !a.endedAt,
+							);

This will make the summary and per-workspace badges accurately reflect actual RUNNING vs IDLE states.

Also applies to: 409-445

Comment thread apps/cli/src/commands/init.tsx Outdated
Comment thread apps/cli/src/commands/panels.tsx Outdated
Comment on lines +267 to +289
{workspaces.map((ws, index) => {
const isSelected = index === selectedWorkspaceIndex;
const isCurrent = ws.id === currentWorkspaceId;
const wsAgents = agents.filter((a) => a.workspaceId === ws.id);
const wsRunning = wsAgents.filter(
(a) => a.status === ProcessStatus.RUNNING,
);

return (
<Box key={ws.id} flexDirection="row" gap={1}>
<Text
bold={isSelected}
color={isSelected ? "cyan" : isCurrent ? "white" : "gray"}
>
{isSelected ? "▸" : " "} {wsRunning.length > 0 ? "●" : "○"}{" "}
{ws.name || ws.id.slice(0, 8)}
</Text>
</Box>
);
})}
<Box marginTop={1}>
<Text dimColor>[W] New</Text>
</Box>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

UI hints for [W] New and [N] New Agent don’t map to any key handling

The footer and panel bodies advertise:

  • [W] New in the workspaces panel, and
  • [N] New Agent plus [Enter] Open in the agents panel,

but useInput only handles 1/2/3, arrows/j/k, r, q, and Enter for opening. Pressing w or n currently has no effect.

Either wire up handlers for w/n to trigger the intended actions (e.g., init workspace or start-agent flow), or hide these hints until those actions are implemented to avoid confusing users.

Also applies to: 307-355, 460-482

🤖 Prompt for AI Agents
In apps/cli/src/commands/panels.tsx around lines 267-289 (and also apply same
fix to sections 307-355 and 460-482), the UI shows "[W] New" and "[N] New Agent"
hints but no input handlers for 'w'/'n' exist; update the useInput handler to
listen for both lowercase and uppercase 'w' and 'n' (and their corresponding key
names if using key.sequence/key.name), and call the existing new-workspace and
new-agent kickoff functions (or the functions that open those flows/modals) so
the hints are actionable; if those kickoff functions don’t exist yet, either
implement simple wrappers that set the same state used by the current create
flows or remove the hint text until the handlers are implemented.

Comment thread apps/cli/src/lib/launch/run.ts Outdated
Comment on lines +150 to +166
async use(id: string): Promise<void> {
// Verify workspace exists
await this.get(id);

// Update lastUsedAt
const workspace = await this.storage.get("workspaces", id);
if (workspace) {
workspace.lastUsedAt = new Date();
workspace.updatedAt = new Date();
await this.storage.set("workspaces", id, workspace);
}

// Set as current workspace
const db = await this.storage.read();
db.state.currentWorkspaceId = id;
await this.storage.write(db);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

use should defensively initialize db.state before writing currentWorkspaceId

use() reads const db = await this.storage.read(); and then writes db.state.currentWorkspaceId = id; without ensuring db.state exists. On a fresh or legacy DB with no state object, this will throw at runtime. This can happen on first workspace creation, since create() calls use() immediately.

A minimal fix mirroring getCurrent()’s defensive logic:

 	async use(id: string): Promise<void> {
@@
-		// Set as current workspace
-		const db = await this.storage.read();
-		db.state.currentWorkspaceId = id;
-		await this.storage.write(db);
+		// Set as current workspace
+		const db = await this.storage.read();
+		if (!db.state) {
+			db.state = {};
+		}
+		db.state.currentWorkspaceId = id;
+		await this.storage.write(db);
 	}
📝 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
async use(id: string): Promise<void> {
// Verify workspace exists
await this.get(id);
// Update lastUsedAt
const workspace = await this.storage.get("workspaces", id);
if (workspace) {
workspace.lastUsedAt = new Date();
workspace.updatedAt = new Date();
await this.storage.set("workspaces", id, workspace);
}
// Set as current workspace
const db = await this.storage.read();
db.state.currentWorkspaceId = id;
await this.storage.write(db);
}
async use(id: string): Promise<void> {
// Verify workspace exists
await this.get(id);
// Update lastUsedAt
const workspace = await this.storage.get("workspaces", id);
if (workspace) {
workspace.lastUsedAt = new Date();
workspace.updatedAt = new Date();
await this.storage.set("workspaces", id, workspace);
}
// Set as current workspace
const db = await this.storage.read();
if (!db.state) {
db.state = {};
}
db.state.currentWorkspaceId = id;
await this.storage.write(db);
}
🤖 Prompt for AI Agents
In apps/cli/src/lib/orchestrators/workspace-orchestrator.ts around lines 150 to
166, the method sets db.state.currentWorkspaceId without ensuring db.state
exists; update use() to defensively initialize db.state (e.g., if (!db.state)
db.state = {}) before assigning currentWorkspaceId, then set
db.state.currentWorkspaceId = id and write the db back as before so writes never
throw on fresh/legacy DBs.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
apps/cli/src/commands/panels.tsx (1)

79-177: [W] New / [N] New Agent hints still have no key handlers

The left and middle panels render:

  • [W] New under workspaces, and
  • [N] New Agent plus [Enter] Open under agents,

but useInput only handles 1/2/3, arrows/j/k, r, q/Escape/Ctrl+C, and Enter for opening an agent. Pressing w or n is still a no-op.

Either:

  • Add handlers for input === "w" / "W" and "n" / "N" that kick off the corresponding flows (e.g., workspace init / agent start), or
  • Remove or hide these hints until those flows are implemented, to avoid misleading users.

This will keep the UI hints aligned with actual keybindings.

Also applies to: 263-295, 355-360, 465-487

apps/cli/src/commands/dashboard.tsx (1)

244-255: Running/idle counts still treat all non-ended agents as “running”

runningAgents and wsRunning filter with a.status === ProcessStatus.RUNNING || !a.endedAt, so IDLE agents (and any non-ended non-RUNNING states) are counted as running and can be double-counted in the summary.

You can keep running/idle disjoint and more accurate by filtering strictly on status (optionally still requiring !endedAt):

-	const runningAgents = filteredAgents.filter(
-		(a) => a.status === ProcessStatus.RUNNING || !a.endedAt,
-	);
-	const idleAgents = filteredAgents.filter(
-		(a) => a.status === ProcessStatus.IDLE && !a.endedAt,
-	);
+	const runningAgents = filteredAgents.filter(
+		(a) => a.status === ProcessStatus.RUNNING && !a.endedAt,
+	);
+	const idleAgents = filteredAgents.filter(
+		(a) => a.status === ProcessStatus.IDLE && !a.endedAt,
+	);
@@
-							const wsRunning = wsAgents.filter(
-								(a) => a.status === ProcessStatus.RUNNING || !a.endedAt,
-							);
+							const wsRunning = wsAgents.filter(
+								(a) => a.status === ProcessStatus.RUNNING && !a.endedAt,
+							);

This keeps “running” counts aligned with true RUNNING state and avoids double-counting idle agents.

Also applies to: 392-427

apps/cli/src/commands/agent.tsx (1)

476-523: AgentStart reports success even when no agents actually started

In startAgents, only successful launchAgent calls push into created, but after the loop you always do:

setStartedAgents(created);
setStep(StartStep.COMPLETE);

and the COMPLETE view renders:

<Text color="green">
	✓ Started {startedAgents.length} agent(s) successfully!
</Text>

If all launchAgent calls fail, this shows a green “✓ Started 0 agent(s) successfully!” message, which is misleading.

Consider:

  • If created.length === 0, set an error state (or a dedicated failure step) and avoid the green success UI.
  • If created.length < agents.length, surface partial success, e.g. “Started N agent(s); M failed — see logs above”.
  • Only transition to StartStep.COMPLETE and show the green success message when at least one agent started.

That will give users an accurate signal about whether anything is actually running.

Also applies to: 604-620

🧹 Nitpick comments (1)
apps/cli/src/lib/orchestrators/process-orchestrator.ts (1)

53-101: Synchronous tmux checks on every get/list can be heavy; tighten typing for agent-specific defaults

get/list now call backfillDefaults + syncAgentStatus for every process, and syncAgentStatus invokes tmuxSessionExists, which in turn runs which tmux + tmux has-session synchronously. With the dashboard/panels polling every 3 seconds, a larger number of agents will translate into frequent blocking subprocesses on the main CLI thread.

Also, needsPersist and backfillDefaults access agent-specific fields (agentType, sessionName, launchCommand) on a parameter typed as Process, which doesn’t declare those properties, so this is fragile from a TypeScript perspective.

Consider:

  • Caching isTmuxInstalled() at module level and optionally gating syncAgentStatus behind a feature flag or a “lazy sync” path, so that simple list/get calls don’t always spawn tmux processes.
  • Narrowing process explicitly where you use agent-only fields, e.g.:
if (process.type === ProcessType.AGENT) {
	const agent = process as Agent;
	// use agent.agentType, agent.sessionName, etc
}

or by changing the helper signatures to accept Agent where appropriate, to keep the type surface honest and avoid surprises when evolving these types.

Also applies to: 103-190

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd0fbf2 and 9e20bb6.

📒 Files selected for processing (5)
  • apps/cli/src/commands/agent.tsx (5 hunks)
  • apps/cli/src/commands/dashboard.tsx (1 hunks)
  • apps/cli/src/commands/panels.tsx (1 hunks)
  • apps/cli/src/lib/launch/run.ts (1 hunks)
  • apps/cli/src/lib/orchestrators/process-orchestrator.ts (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
apps/cli/src/lib/launch/run.ts (2)
apps/cli/src/types/process.ts (1)
  • Agent (41-45)
apps/cli/src/lib/launch/config.ts (1)
  • getLaunchCommand (19-42)
apps/cli/src/lib/orchestrators/process-orchestrator.ts (3)
apps/cli/src/types/process.ts (2)
  • Agent (41-45)
  • Process (15-29)
apps/cli/src/lib/launch/config.ts (1)
  • getDefaultLaunchCommand (48-58)
apps/cli/src/types/workspace.ts (1)
  • Workspace (6-16)
apps/cli/src/commands/dashboard.tsx (6)
apps/cli/src/types/workspace.ts (2)
  • Workspace (6-16)
  • WorkspaceOrchestrator (28-50)
apps/cli/src/types/process.ts (3)
  • Process (15-29)
  • ProcessOrchestrator (47-62)
  • Agent (41-45)
apps/cli/src/lib/db.ts (1)
  • getDb (9-18)
apps/cli/src/lib/orchestrators/workspace-orchestrator.ts (1)
  • WorkspaceOrchestrator (14-224)
apps/cli/src/lib/orchestrators/process-orchestrator.ts (1)
  • ProcessOrchestrator (50-325)
apps/cli/src/lib/launch/run.ts (1)
  • launchAgent (47-138)
apps/cli/src/commands/agent.tsx (6)
apps/cli/src/lib/orchestrators/process-orchestrator.ts (2)
  • stopAll (272-309)
  • ProcessOrchestrator (50-325)
apps/cli/src/lib/db.ts (1)
  • getDb (9-18)
apps/cli/src/types/process.ts (2)
  • ProcessOrchestrator (47-62)
  • Process (15-29)
apps/cli/src/lib/orchestrators/workspace-orchestrator.ts (1)
  • WorkspaceOrchestrator (14-224)
apps/cli/src/lib/launch/config.ts (1)
  • getDefaultLaunchCommand (48-58)
apps/cli/src/lib/launch/run.ts (1)
  • launchAgent (47-138)
apps/cli/src/commands/panels.tsx (4)
apps/cli/src/types/process.ts (3)
  • Process (15-29)
  • ProcessOrchestrator (47-62)
  • Agent (41-45)
apps/cli/src/lib/db.ts (1)
  • getDb (9-18)
apps/cli/src/lib/orchestrators/process-orchestrator.ts (1)
  • ProcessOrchestrator (50-325)
apps/cli/src/lib/launch/run.ts (1)
  • launchAgent (47-138)
🪛 GitHub Actions: CI
apps/cli/src/commands/agent.tsx

[error] 434-434: lint/suspicious/noExplicitAny FIXABLE. Unexpected any. Specify a different type.


[error] 476-476: lint/suspicious/noExplicitAny FIXABLE. Unexpected any. Specify a different type.

Comment on lines +430 to +437
export function AgentStart({ workspaceId, onComplete }: AgentStartProps) {
const { exit } = useApp();
const [step, setStep] = React.useState(StartStep.LOADING);
const [error, setError] = React.useState<string | null>(null);
const [workspace, setWorkspace] = React.useState<any>(null);
const [selectedAgents, setSelectedAgents] = React.useState<AgentType[]>([]);
const [startedAgents, setStartedAgents] = React.useState<Process[]>([]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace any with concrete workspace types to satisfy lint and improve safety

The workspace state and startAgents parameter are both typed as any, which is tripping noExplicitAny in CI and weakens type safety around workspace handling:

const [workspace, setWorkspace] = React.useState<any>(null);

const startAgents = async (ws: any, agents: AgentType[]) => {

You can fix the lint errors and get better checking by using the Workspace type:

-import { getDefaultLaunchCommand } from "../lib/launch/config";
+import { getDefaultLaunchCommand } from "../lib/launch/config";
+import type { Workspace } from "../types/workspace";
@@
-	const [workspace, setWorkspace] = React.useState<any>(null);
+	const [workspace, setWorkspace] = React.useState<Workspace | null>(null);
@@
-	const startAgents = async (ws: any, agents: AgentType[]) => {
+	const startAgents = async (ws: Workspace, agents: AgentType[]) => {

This should clear the noExplicitAny failures and make it harder to accidentally call startAgents with an incompatible object.

Also applies to: 476-531

🧰 Tools
🪛 GitHub Actions: CI

[error] 434-434: lint/suspicious/noExplicitAny FIXABLE. Unexpected any. Specify a different type.

🤖 Prompt for AI Agents
In apps/cli/src/commands/agent.tsx around lines 430-437 (and also apply the same
change to lines 476-531), the workspace state and startAgents parameter use the
explicit any type which triggers noExplicitAny and reduces type safety; change
the React state declaration to use the concrete Workspace type (e.g.
React.useState<Workspace | null>(null)) and update any function signatures like
startAgents to accept ws: Workspace (or Workspace | null if appropriate), import
the Workspace type from its module if not already imported, and update any
downstream usages/assignments to satisfy the new type (narrow checks for null
where needed) so lint passes and typing is stricter.

Comment on lines +1 to +7
import { exec, execSync, spawn } from "node:child_process";
import { promisify } from "node:util";
import type { Agent } from "../../types/process";
import { getLaunchCommand } from "./config";

const execAsync = promisify(exec);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Avoid interpolating command into a shell string; use spawn to prevent injection and quoting issues

launchAgent currently creates tmux sessions via:

await execAsync(`tmux new-session -d -s "${sessionName}" "${command}"`);

Since command can come from SUPERSET_AGENT_LAUNCH_*, any embedded quotes or shell metacharacters (;, &&, etc.) will be interpreted by the shell, which is both fragile (easy to break) and a potential injection vector.

A safer approach is to avoid the shell altogether and use spawn with arguments:

-import { exec, execSync, spawn } from "node:child_process";
-import { promisify } from "node:util";
+import { execSync, spawn } from "node:child_process";
@@
-const execAsync = promisify(exec);
@@
-	// Session doesn't exist - create it in detached mode
-	try {
-		await execAsync(`tmux new-session -d -s "${sessionName}" "${command}"`);
+	// Session doesn't exist - create it in detached mode
+	try {
+		await new Promise<void>((resolve, reject) => {
+			const child = spawn("tmux", ["new-session", "-d", "-s", sessionName, command], {
+				stdio: "ignore",
+				detached: false,
+			});
+
+			child.on("error", (error) => reject(error));
+			child.on("exit", (code) => {
+				if (code === 0) {
+					resolve();
+				} else {
+					reject(new Error(`tmux new-session exited with code ${code ?? "null"}`));
+				}
+			});
+		});

This way command is passed as a single argument to tmux (no local shell parsing), making env-configured launch commands much more robust and removing the shell-injection vector.

Also applies to: 89-137


I'll verify the shell injection concern by examining the actual codebase.


🏁 Script executed:

#!/bin/bash
# Find and display the launchAgent function around lines 89-137
cat apps/cli/src/lib/launch/run.ts | head -150 | tail -70

Length of output: 2112


🏁 Script executed:

#!/bin/bash
# Get full content of the run.ts file to understand context
wc -l apps/cli/src/lib/launch/run.ts

Length of output: 34


🏁 Script executed:

#!/bin/bash
# Search for launchAgent function definition
rg -n "launchAgent" apps/cli/src/lib/launch/run.ts

Length of output: 165


🏁 Script executed:

#!/bin/bash
# Search for SUPERSET_AGENT_LAUNCH to understand command source
rg -n "SUPERSET_AGENT_LAUNCH" apps/cli/src/

Length of output: 536


🏁 Script executed:

#!/bin/bash
# Search for execAsync usage in the file
rg -n "execAsync" apps/cli/src/lib/launch/run.ts -A2 -B2

Length of output: 366


🏁 Script executed:

#!/bin/bash
# Get full launchAgent function (lines 47 onwards)
sed -n '47,137p' apps/cli/src/lib/launch/run.ts

Length of output: 2860


🏁 Script executed:

#!/bin/bash
# Get the getLaunchCommand function from config.ts
cat apps/cli/src/lib/launch/config.ts

Length of output: 1613


🏁 Script executed:

#!/bin/bash
# Check if there are any other exec/execSync/spawn calls in run.ts
rg -n "exec|spawn" apps/cli/src/lib/launch/run.ts

Length of output: 416


🏁 Script executed:

#!/bin/bash
# Get attachToAgent function and surrounding context (lines 175-210)
sed -n '140,210p' apps/cli/src/lib/launch/run.ts

Length of output: 2125


🏁 Script executed:

#!/bin/bash
# Get context around lines 181, 184, 187 execSync calls
sed -n '175,200p' apps/cli/src/lib/launch/run.ts

Length of output: 741


Fix shell injection vulnerability at line 91 by using spawn() instead of execAsync()

The vulnerability is confirmed. Line 91 interpolates the command variable (sourced from environment variable SUPERSET_AGENT_LAUNCH_*) directly into a shell string:

await execAsync(`tmux new-session -d -s "${sessionName}" "${command}"`);

This allows shell metacharacters (;, &&, |, etc.) to be interpreted, creating both a fragility and injection vector.

The codebase already uses the safe spawn() pattern elsewhere (line 195 in attachToAgent), so the proposed fix using spawn() with an argument array is the correct approach:

-import { exec, execSync, spawn } from "node:child_process";
-import { promisify } from "node:util";
+import { execSync, spawn } from "node:child_process";
@@
-const execAsync = promisify(exec);
@@
-	// Session doesn't exist - create it in detached mode
-	try {
-		await execAsync(`tmux new-session -d -s "${sessionName}" "${command}"`);
+	// Session doesn't exist - create it in detached mode
+	try {
+		await new Promise<void>((resolve, reject) => {
+			const child = spawn("tmux", ["new-session", "-d", "-s", sessionName, command], {
+				stdio: "ignore",
+				detached: false,
+			});
+
+			child.on("error", (error) => reject(error));
+			child.on("exit", (code) => {
+				if (code === 0) {
+					resolve();
+				} else {
+					reject(new Error(`tmux new-session exited with code ${code ?? "null"}`));
+				}
+			});
+		});
🤖 Prompt for AI Agents
In apps/cli/src/lib/launch/run.ts (context shown lines 1-7; fix applies at line
~91), the code uses execAsync with a shell-interpolated string which allows
shell injection; replace the execAsync call await execAsync(`tmux new-session -d
-s "${sessionName}" "${command}"`) with a spawn-based invocation that does not
use a shell: call spawn('tmux', ['new-session', '-d', '-s', sessionName, '--',
...args], { stdio: 'inherit' }) where args is a safe array of the command and
its arguments (if getLaunchCommand returns a single string, split it into args
using a proper tokenizer or adapt getLaunchCommand to return an array), await
the child process completion (wrap in a Promise that resolves/rejects on
'exit'/'error') and do not set shell: true; this removes shell interpolation and
prevents injection.

Copy link
Copy Markdown
Contributor

@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: 1

♻️ Duplicate comments (2)
apps/cli/src/commands/agent.tsx (2)

430-476: Replace any workspace typing with concrete Workspace to fix lint and improve safety

Line 434 and the startAgents signature still use any:

const [workspace, setWorkspace] = React.useState<any>(null); // Line 434

const startAgents = async (ws: any, agents: AgentType[]) => { // Line 476

This is exactly what CI is flagging (lint/suspicious/noExplicitAny) and it loses type-safety even though you already have a Workspace type available via the orchestrators.

A minimal fix:

  • Import the Workspace type (adjust path to wherever it’s defined, e.g. ../types/workspace).
  • Update the state and helper signature:
-import React from "react";
+import React from "react";
+import type { Workspace } from "../types/workspace"; // adjust path if needed

-const [workspace, setWorkspace] = React.useState<any>(null);
+const [workspace, setWorkspace] = React.useState<Workspace | null>(null);

-const startAgents = async (ws: any, agents: AgentType[]) => {
+const startAgents = async (ws: Workspace, agents: AgentType[]) => {
  • Add a small null guard when invoking startAgents from handleAgentSelect:
-		if (item.value === "done") {
-			if (selectedAgents.length === 0) {
-				setError("Please select at least one agent to start");
-				return;
-			}
-			setStep(StartStep.STARTING);
-			startAgents(workspace, selectedAgents);
+		if (item.value === "done") {
+			if (selectedAgents.length === 0) {
+				setError("Please select at least one agent to start");
+				return;
+			}
+			if (!workspace) {
+				setError("Workspace not loaded. Please try again.");
+				return;
+			}
+			setStep(StartStep.STARTING);
+			startAgents(workspace, selectedAgents);

This should resolve the lint error and make the flow safer by guaranteeing a real Workspace is passed into startAgents. (Based on relevant orchestrator snippets.)

Also applies to: 533-541


476-528: Don’t report a green “success” when zero agents actually start

The startAgents helper always transitions to StartStep.COMPLETE and the UI always shows:

<Text color="green">
  ✓ Started {startedAgents.length} agent(s) successfully!
</Text>

even when created.length === 0 (all launchAgent calls failed), because only successful ones are pushed into created but you don’t branch on that value. Users will see a green “Started 0 agent(s) successfully!” and may assume everything is fine.

Consider at least handling the “no agents started” case explicitly before setting COMPLETE:

-			// Update workspace lastUsedAt and set as current if not already
-			await workspaceOrchestrator.use(ws.id);
-
-			setStartedAgents(created);
-			setStep(StartStep.COMPLETE);
+			// Update workspace lastUsedAt and set as current if not already
+			await workspaceOrchestrator.use(ws.id);
+
+			// If no agents actually started, surface a clear error instead of success
+			if (created.length === 0) {
+				setError(
+					"Failed to start any agents. Check the error output above for details.",
+				);
+				return;
+			}
+
+			setStartedAgents(created);
+			setStep(StartStep.COMPLETE);

You could go further and distinguish partial success (e.g., “Started N agent(s); M failed — see logs above”), but even just avoiding the “0 agents successfully” green state would make the UX much less misleading. (Based on earlier review feedback.)

Also applies to: 596-620

🧹 Nitpick comments (4)
apps/cli/src/commands/panels.tsx (2)

104-131: Guard navigation when lists are empty to avoid negative indices.

The wrap‑around logic uses length - 1 on both data.workspaces and filteredAgents. When a list is empty, pressing ↑ in that panel will set the corresponding selected index to -1. The rest of the code happens to tolerate this, but it’s a brittle invariant and can lead to confusing selection state.

Consider explicitly guarding on list emptiness before changing indices, e.g.:

-		const agents = data.processes.filter((p) => p.type === ProcessType.AGENT);
-		const selectedWorkspace = data.workspaces[selectedWorkspaceIndex];
-		const filteredAgents = selectedWorkspace
+		const agents = data.processes.filter((p) => p.type === ProcessType.AGENT);
+		const selectedWorkspace = data.workspaces[selectedWorkspaceIndex];
+		const filteredAgents = selectedWorkspace
 			? agents.filter((a) => a.workspaceId === selectedWorkspace.id)
 			: agents;
+		const hasWorkspaces = data.workspaces.length > 0;
+		const hasAgents = filteredAgents.length > 0;
@@
-		if (key.upArrow || input === "k") {
-			if (activePanel === "workspaces") {
+		if (key.upArrow || input === "k") {
+			if (activePanel === "workspaces" && hasWorkspaces) {
 				setSelectedWorkspaceIndex((prev) =>
 					prev > 0 ? prev - 1 : data.workspaces.length - 1,
 				);
-			} else if (activePanel === "agents") {
+			} else if (activePanel === "agents" && hasAgents) {
 				setSelectedAgentIndex((prev) =>
 					prev > 0 ? prev - 1 : filteredAgents.length - 1,
 				);
 			}
-		} else if (key.downArrow || input === "j") {
-			if (activePanel === "workspaces") {
+		} else if (key.downArrow || input === "j") {
+			if (activePanel === "workspaces" && hasWorkspaces) {
 				setSelectedWorkspaceIndex((prev) =>
 					prev < data.workspaces.length - 1 ? prev + 1 : 0,
 				);
-			} else if (activePanel === "agents") {
+			} else if (activePanel === "agents" && hasAgents) {
 				setSelectedAgentIndex((prev) =>
 					prev < filteredAgents.length - 1 ? prev + 1 : 0,
 				);
 			}

22-24: onComplete prop is unused; either wire it into exit paths or drop it.

PanelsProps exposes onComplete, but the component only destructures it as _onComplete and never calls it. That makes the public API misleading and harder to integrate with higher‑level CLI flows.

Consider either:

  • Invoking onComplete in success/exit paths instead of (or in addition to) process.exit, or
  • Removing the prop from this component until it’s needed.

Also applies to: 31-37

apps/cli/src/commands/agent.tsx (2)

276-361: Workspace-scoped stop logic is solid; consider minor UX cleanup

The new AgentStopAll flow correctly:

  • Resolves a target workspace (explicit workspaceId or getCurrent() fallback),
  • Filters to running ProcessType.AGENT processes for that workspace, and
  • Stops each via processOrchestrator.stop, with a clear “no running agents” error when count === 0.

Two small polish points you might consider:

  1. Success workspace label
    In the success view (Lines 353-360), you only show the workspace when workspaceId prop is provided, but not when the current workspace is auto-detected. If you want consistent feedback, you could track targetWorkspaceId in state and always render the workspace identifier from that, regardless of whether it came from props or getCurrent().

  2. Redundant setLoading(false)
    In the count === 0 branch you call setLoading(false) before returning, and it will run again in finally. It’s harmless but can be simplified by relying on the finally block.

These are optional; the current implementation is functionally correct.


482-504: Hoist dynamic launchAgent import outside the loop for clarity and tiny perf win

Inside startAgents you dynamically import launchAgent on every iteration:

for (const agentType of agents) {
  
  const agent = process as import("../types/process").Agent;
  const { launchAgent } = await import("../lib/launch/run");
  const result = await launchAgent(agent, { attach: false });
  
}

Since the module is cached after the first import, this works, but it’s slightly noisier and does redundant awaits. You can hoist the import once before the loop:

-		const created: Process[] = [];
-		for (const agentType of agents) {
+		const { launchAgent } = await import("../lib/launch/run");
+
+		const created: Process[] = [];
+		for (const agentType of agents) {
 			// Get default launch command for this agent type
 			const launchCommand = getDefaultLaunchCommand(agentType);
 			…
-				const agent = process as import("../types/process").Agent;
-				const { launchAgent } = await import("../lib/launch/run");
-				const result = await launchAgent(agent, { attach: false });
+				const agent = process as import("../types/process").Agent;
+				const result = await launchAgent(agent, { attach: false });

Purely an optional cleanup; behavior stays the same.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e20bb6 and 64fbac1.

📒 Files selected for processing (4)
  • apps/cli/src/commands/agent.tsx (5 hunks)
  • apps/cli/src/commands/dashboard.tsx (1 hunks)
  • apps/cli/src/commands/panels.tsx (1 hunks)
  • apps/cli/src/lib/launch/run.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/cli/src/lib/launch/run.ts
  • apps/cli/src/commands/dashboard.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
apps/cli/src/commands/panels.tsx (4)
apps/cli/src/types/process.ts (3)
  • Process (15-29)
  • ProcessOrchestrator (47-62)
  • Agent (41-45)
apps/cli/src/lib/db.ts (1)
  • getDb (9-18)
apps/cli/src/lib/orchestrators/process-orchestrator.ts (1)
  • ProcessOrchestrator (50-325)
apps/cli/src/lib/launch/run.ts (1)
  • launchAgent (47-138)
apps/cli/src/commands/agent.tsx (6)
apps/cli/src/lib/orchestrators/process-orchestrator.ts (2)
  • stopAll (272-309)
  • ProcessOrchestrator (50-325)
apps/cli/src/lib/db.ts (1)
  • getDb (9-18)
apps/cli/src/types/process.ts (2)
  • ProcessOrchestrator (47-62)
  • Process (15-29)
apps/cli/src/lib/orchestrators/workspace-orchestrator.ts (1)
  • WorkspaceOrchestrator (14-224)
apps/cli/src/lib/launch/config.ts (1)
  • getDefaultLaunchCommand (48-58)
apps/cli/src/lib/launch/run.ts (1)
  • launchAgent (47-138)
🪛 GitHub Actions: CI
apps/cli/src/commands/agent.tsx

[error] 434-434: lint/suspicious/noExplicitAny: Unexpected any. Specify a different type.

🔇 Additional comments (1)
apps/cli/src/commands/agent.tsx (1)

626-711: AgentAttach resolution and failure handling look robust

The attach flow is well thought through:

  • Tries get(id) first, then falls back to finding by sessionName among AGENT processes.
  • Validates the process type before attaching.
  • Exits Ink before invoking launchAgent so tmux can own stdin.
  • On failure, it best-effort updates the agent status to STOPPED with endedAt, and surfaces clear console errors plus a non-zero exit code.

No issues from my side here; this segment looks solid.

Comment on lines +82 to +89
useInput((input, key) => {
if (!data) return;

if (key.escape || input === "q" || (key.ctrl && input === "c")) {
exit();
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Exit keys are ignored while data is null (loading/error state).

Because of if (!data) return;, Esc, q, and Ctrl+C do nothing until data is populated. If loadData fails and only error is set, the user may not be able to exit via these keys at all.

Move the exit-handling branch before the !data guard so quitting always works:

-	useInput((input, key) => {
-		if (!data) return;
-
-		if (key.escape || input === "q" || (key.ctrl && input === "c")) {
-			exit();
-			return;
-		}
+	useInput((input, key) => {
+		if (key.escape || input === "q" || (key.ctrl && input === "c")) {
+			exit();
+			return;
+		}
+
+		if (!data) return;
🤖 Prompt for AI Agents
In apps/cli/src/commands/panels.tsx around lines 82 to 89, the current useInput
handler returns early when data is falsy, which prevents exit keys from working
during loading or error states; move the branch that checks for exit keys
(Escape, "q", Ctrl+C) to the top of the handler so those keys call exit()
immediately regardless of data, then keep the existing guard (if !data return)
after that to skip other input handling when data is not available.

Copy link
Copy Markdown
Contributor

@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: 0

♻️ Duplicate comments (1)
apps/cli/src/commands/agent.tsx (1)

430-440: Fix the explicit any types to resolve pipeline failures and improve type safety.

The workspace state (line 434) and ws variable (line 447) use explicit or implicit any types, which trigger noExplicitAny and noImplicitAnyLet lint errors and weaken type safety.

Import the Workspace type and apply these fixes:

+import type { Workspace } from "../types/workspace";

 export function AgentStart({ workspaceId, onComplete }: AgentStartProps) {
 	const { exit } = useApp();
 	const [step, setStep] = React.useState(StartStep.LOADING);
 	const [error, setError] = React.useState<string | null>(null);
-	const [workspace, setWorkspace] = React.useState<any>(null);
+	const [workspace, setWorkspace] = React.useState<Workspace | null>(null);
 	const [selectedAgents, setSelectedAgents] = React.useState<AgentType[]>([]);
 	const [startedAgents, setStartedAgents] = React.useState<Process[]>([]);
 		const loadWorkspace = async () => {
 			try {
 				const db = getDb();
 				const workspaceOrchestrator = new WorkspaceOrchestrator(db);
 
-				let ws;
+				let ws: Workspace;
 				if (workspaceId) {
 					ws = await workspaceOrchestrator.get(workspaceId);
 				} else {

Also applies to: 447-447, 479-479

🧹 Nitpick comments (2)
apps/cli/src/commands/agent.tsx (2)

644-647: Refactor the type assertion for better type safety.

Line 646 uses (a as any).agentType which bypasses type checking. Since startedAgents is typed as Process[] and agents have the agentType field, cast to the Agent type instead.

+import type { Agent } from "../types/process";

 					{successCount > 0 && (
 						<Text dimColor color="green">
-							Success: {startedAgents.map((a) => (a as any).agentType).join(", ")}
+							Success: {startedAgents.map((a) => (a as Agent).agentType).join(", ")}
 						</Text>
 					)}

738-741: Consider reusing the outer scope imports instead of re-importing.

Lines 738-741 dynamically re-import getDb and ProcessOrchestrator inside the error handler, but these modules are already imported and available at the outer scope (lines 5, 7).

Reuse the existing imports to simplify the error handler:

 					if (!result.success) {
 						// Update agent status to STOPPED on failure
 						try {
-							const db = (await import("../lib/db")).getDb();
-							const { ProcessOrchestrator } = await import(
-								"../lib/orchestrators/process-orchestrator"
-							);
-							const orchestrator = new ProcessOrchestrator(db);
+							const orchestrator = new ProcessOrchestrator(getDb());
 							await orchestrator.update(agent.id, {
 								status: ProcessStatus.STOPPED,
 								endedAt: new Date(),
 							});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 64fbac1 and d75f92e.

📒 Files selected for processing (1)
  • apps/cli/src/commands/agent.tsx (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/cli/src/commands/agent.tsx (6)
apps/cli/src/lib/orchestrators/process-orchestrator.ts (2)
  • stopAll (272-309)
  • ProcessOrchestrator (50-325)
apps/cli/src/lib/db.ts (1)
  • getDb (9-18)
apps/cli/src/types/process.ts (2)
  • ProcessOrchestrator (47-62)
  • Process (15-29)
apps/cli/src/lib/orchestrators/workspace-orchestrator.ts (1)
  • WorkspaceOrchestrator (14-224)
apps/cli/src/lib/launch/config.ts (1)
  • getDefaultLaunchCommand (48-58)
apps/cli/src/lib/launch/run.ts (1)
  • launchAgent (47-138)
🪛 GitHub Actions: CI
apps/cli/src/commands/agent.tsx

[error] 434-434: lint/suspicious/noExplicitAny: Unexpected any. Use a more specific type (e.g., Workspace or unknown).


[error] 447-447: lint/suspicious/noImplicitAnyLet: Variable 'ws' implicitly has any type. Provide a type annotation.

🔇 Additional comments (3)
apps/cli/src/commands/agent.tsx (3)

276-365: LGTM! Workspace-scoped agent stopping implemented correctly.

The updated AgentStopAll component properly handles workspace-scoped operations with good fallback to the current workspace, clear error messages when no workspace is set or no agents are running, and informative success feedback including the stopped count.


536-553: Good fix! Proper error handling when all agents fail to start.

The logic now correctly checks created.length > 0 before transitioning to the COMPLETE step and displays a detailed error message when all agents fail, addressing the issue from the previous review where success was reported even with zero started agents.


687-770: Excellent implementation of the agent attach flow with robust cleanup and error handling.

The attach logic correctly handles:

  • Lookup by ID or sessionName with clear error messaging
  • Validation that the process is an agent
  • Exiting Ink before tmux attachment to prevent stdin conflicts (critical)
  • Deferring the actual attachment with setImmediate
  • Updating agent status to STOPPED on attachment failure
  • Proper exit codes for success/failure paths

Copy link
Copy Markdown
Contributor

@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: 4

♻️ Duplicate comments (3)
apps/cli/src/commands/agent.tsx (1)

418-477: Replace any usages in AgentStart with Workspace for type safety (and to fix lint)

The CI warnings at Line 434 (noExplicitAny) and Line 447 (noImplicitAnyLet) come from:

const [workspace, setWorkspace] = React.useState<any>(null);

let ws;

const startAgents = async (ws: any, agents: AgentType[]) => {

This also weakens type safety for workspace handling. You can fix both lint and typings by using the concrete Workspace type and adding a small guard:

+import type { Workspace } from "../types/workspace";
@@
- const [workspace, setWorkspace] = React.useState<any>(null);
+ const [workspace, setWorkspace] = React.useState<Workspace | null>(null);
@@
- let ws;
+ let ws: Workspace | null = null;
@@
- const startAgents = async (ws: any, agents: AgentType[]) => {
+ const startAgents = async (ws: Workspace, agents: AgentType[]) => {
@@
- if (item.value === "done") {
-   if (selectedAgents.length === 0) {
+ if (item.value === "done") {
+   if (selectedAgents.length === 0) {
      setError("Please select at least one agent to start");
      return;
+   }
+   if (!workspace) {
+     setError("Workspace not loaded; please try again.");
+     return;
    }
-   setStep(StartStep.STARTING);
-   startAgents(workspace, selectedAgents);
+   setStep(StartStep.STARTING);
+   startAgents(workspace, selectedAgents);

This keeps the runtime behaviour the same, clears the lint warnings, and makes it harder to accidentally call startAgents with an invalid object. Note this also addresses the earlier review feedback about avoiding any here.

Also applies to: 479-557, 559-577, 583-672

apps/cli/src/lib/launch/run.ts (1)

1-7: Avoid interpolating env-derived commands into shell strings; use spawn/sanitization instead

createSessionWithRetry and commandExists are currently vulnerable to shell-quoting issues and potential injection because they interpolate command/binary into shell strings:

const execAsync = promisify(exec);

await execAsync(`tmux new-session -d -s "${sessionName}" "${command}"`, { timeout: 10000 });

execSync(`command -v ${binary}`, { stdio: "ignore" });

Since command originates from getLaunchCommand(agent) and can be configured via SUPERSET_AGENT_LAUNCH_*, a malicious or simply oddly formatted value (embedded quotes, ;, &&, etc.) can break tmux startup or execute unintended shell fragments. This is effectively the same issue flagged in the earlier review.

Consider tightening this in two places:

  1. Use spawn (no shell) to create the tmux session

Pass the launch command as a single argument so tmux, not your local shell, interprets it:

-import { exec, execSync, spawn } from "node:child_process";
-import { promisify } from "node:util";
+import { execSync, spawn } from "node:child_process";
@@
-const execAsync = promisify(exec);
@@
-  await execAsync(`tmux new-session -d -s "${sessionName}" "${command}"`, {
-    timeout: 10000,
-  });
+  await new Promise<void>((resolve, reject) => {
+    const child = spawn(
+      "tmux",
+      ["new-session", "-d", "-s", sessionName, command],
+      { stdio: "ignore" },
+    );
+
+    child.on("error", reject);
+    child.on("exit", (code) => {
+      code === 0
+        ? resolve()
+        : reject(new Error(`tmux new-session exited with code ${code ?? "null"}`));
+    });
+  });
  1. Harden commandExists against injection

Either avoid the shell entirely (e.g. by probing with spawnSync) or at least reject unsafe binaries:

-function commandExists(binary: string): boolean {
-  try {
-    execSync(`command -v ${binary}`, { stdio: "ignore" });
-    return true;
-  } catch {
-    return false;
-  }
-}
+function commandExists(binary: string): boolean {
+  // Only allow simple command names to avoid shell metacharacters.
+  if (!/^[a-zA-Z0-9._-]+$/.test(binary)) {
+    return false;
+  }
+  try {
+    execSync(`command -v ${binary}`, { stdio: "ignore" });
+    return true;
+  } catch {
+    return false;
+  }
+}

This preserves current behaviour while eliminating the obvious injection surface and making env-configured launch commands more robust.

Also applies to: 76-83, 89-137, 172-200, 232-239

apps/cli/src/commands/panels.tsx (1)

77-84: Exit keys are still gated by data being non-null (cannot quit on loading/error)

Because of if (!data) return; at the top of the handler, Esc, q, and Ctrl+C here only run after data is populated. During loading or when loadData fails and only error is set, the user cannot exit via these keys. This matches a prior comment and is still present.

Reorder the checks so exit keys always work:

-	useInput((input, key) => {
-		if (!data) return;
-
-		if (key.escape || input === "q" || (key.ctrl && input === "c")) {
-			exit();
-			return;
-		}
+	useInput((input, key) => {
+		if (key.escape || input === "q" || (key.ctrl && input === "c")) {
+			exit();
+			return;
+		}
+
+		if (!data) return;
🧹 Nitpick comments (6)
apps/cli/src/lib/persistence_docs/EXAMPLE.md (1)

117-124: stopAll docs now match agent-only behaviour; consider showing the returned count

The updated comments and example correctly reflect that stopAll only stops agents (not terminals) and that endedAt is set on stopped agents. You may want to also demonstrate the numeric return value for completeness, e.g.:

const stoppedCount = await processOrch.stopAll();
console.log(`Stopped ${stoppedCount} agent(s)`);

This would document the updated signature more explicitly.

apps/cli/src/commands/agent.tsx (1)

276-341: AgentStopAll per-workspace logic is correct; tweak success message workspace label

The per-workspace stop logic looks good:

  • Resolves target workspace from workspaceId prop or getCurrent(), with a clear error if neither is available.
  • Filters to running agents only (!p.endedAt && p.type === ProcessType.AGENT) and calls stop per agent.
  • Returns an informative error if nothing was stopped.

One minor UX nit: in the success state you only show the workspace label when workspaceId prop was provided. If the workspace was inferred from the current workspace, that label won’t render even though targetWorkspaceId is known. Consider basing the label on the actual workspace used in the effect (e.g., by putting targetWorkspaceId into state) so superset agent stop-all without --workspace still shows which workspace was affected.

Also applies to: 351-361

apps/cli/src/commands/panels.tsx (2)

99-126: Selection indices can become negative or out of range when lists are empty

When workspaces.length or filteredAgents.length is 0, the up/down handlers can set indices to -1 or keep stale indices from previous data, leading to “no item highlighted” behavior. Rendering is safe, but UX is a bit odd.

You can defensively clamp / early-return when the active list is empty:

		// Navigation
		if (key.upArrow || input === "k") {
-			if (activePanel === "workspaces") {
+			if (activePanel === "workspaces") {
+				if (data.workspaces.length === 0) return;
 				setSelectedWorkspaceIndex((prev) =>
 					prev > 0 ? prev - 1 : data.workspaces.length - 1,
 				);
-			} else if (activePanel === "agents") {
+			} else if (activePanel === "agents") {
+				if (filteredAgents.length === 0) return;
 				setSelectedAgentIndex((prev) =>
 					prev > 0 ? prev - 1 : filteredAgents.length - 1,
 				);
 			}
		} else if (key.downArrow || input === "j") {
-			if (activePanel === "workspaces") {
+			if (activePanel === "workspaces") {
+				if (data.workspaces.length === 0) return;
 				setSelectedWorkspaceIndex((prev) =>
 					prev < data.workspaces.length - 1 ? prev + 1 : 0,
 				);
-			} else if (activePanel === "agents") {
+			} else if (activePanel === "agents") {
+				if (filteredAgents.length === 0) return;
 				setSelectedAgentIndex((prev) =>
 					prev < filteredAgents.length - 1 ? prev + 1 : 0,
 				);
 			}

26-35: onComplete prop is unused – consider invoking it on exit

Panels accepts an onComplete callback but renames it to _onComplete and never calls it. If this matches other commands’ patterns (e.g., used in tests or higher-level orchestration), it’d be useful to call it when the panel flow finishes (on explicit exit or after a successful attach).

Example:

-export function Panels({ onComplete: _onComplete }: PanelsProps) {
+export function Panels({ onComplete }: PanelsProps) {
@@
-	const { exit } = useApp();
+	const { exit } = useApp();
+
+	const finish = React.useCallback(() => {
+		onComplete?.();
+		exit();
+	}, [exit, onComplete]);
@@
-		if (key.escape || input === "q" || (key.ctrl && input === "c")) {
-			exit();
+		if (key.escape || input === "q" || (key.ctrl && input === "c")) {
+			finish();
 			return;
 		}
@@
-				// Exit Ink to stop useInput before tmux takes over stdin
-				exit();
+				// Exit Ink to stop useInput before tmux takes over stdin
+				finish();

Adjust attach flow as needed if you want onComplete to run only on “normal” exits.

apps/cli/src/commands/init.tsx (2)

100-135: Normalize the branch string once to avoid subtle duplicate / storage mismatches

You validate with state.branch.trim() and use that for duplicate detection, but you pass raw state.branch into create, which may still contain whitespace. That can cause situations where "main " passes validation, matches an existing "main" in the duplicate check, but still stores "main " in the new workspace.

Consider normalizing once:

const handleBranchSubmit = async () => {
-	if (!state.branch.trim()) {
+	const branch = state.branch.trim();
+	if (!branch) {
@@
-			const duplicate = workspaces.find(
-				(w) => "branch" in w && w.branch === state.branch.trim(),
-			);
+			const duplicate = workspaces.find(
+				(w) => "branch" in w && w.branch === branch,
+			);
@@
-		setState((s) => ({
-			...s,
-			error: undefined,
-			step: InitStep.INPUT_NAME,
-		}));
+		setState((s) => ({
+			...s,
+			branch,
+			error: undefined,
+			step: InitStep.INPUT_NAME,
+		}));

Then createWorkspace can safely use state.branch as already-normalized.

Also applies to: 187-196


58-69: Explicitly decide whether empty path should mean CWD

handlePathSubmit maps an empty state.path to resolve(""), i.e., the current working directory. That may be intentional (default to CWD) or surprising given the prompt text.

If you don’t want that behavior, add a simple guard:

	const handlePathSubmit = async () => {
-		const expandedPath = state.path.replace(/^~/, process.env.HOME || "~");
+		if (!state.path.trim()) {
+			setState((s) => ({
+				...s,
+				error: "Path cannot be empty",
+			}));
+			return;
+		}
+
+		const expandedPath = state.path.replace(/^~/, process.env.HOME || "~");

If defaulting to CWD is desired, consider mentioning it in the prompt text to make it clear to users.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d75f92e and 8eec9bf.

📒 Files selected for processing (10)
  • apps/cli/readme.md (1 hunks)
  • apps/cli/src/cli.tsx (8 hunks)
  • apps/cli/src/commands/agent.tsx (5 hunks)
  • apps/cli/src/commands/init.tsx (1 hunks)
  • apps/cli/src/commands/panels.tsx (1 hunks)
  • apps/cli/src/lib/launch/run.ts (1 hunks)
  • apps/cli/src/lib/orchestrators/__tests__/process-orchestrator.test.ts (14 hunks)
  • apps/cli/src/lib/orchestrators/process-orchestrator.ts (7 hunks)
  • apps/cli/src/lib/persistence_docs/EXAMPLE.md (1 hunks)
  • apps/cli/src/lib/persistence_docs/SUMMARY.md (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/cli/src/lib/persistence_docs/SUMMARY.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/cli/src/lib/orchestrators/tests/process-orchestrator.test.ts
🧰 Additional context used
🧬 Code graph analysis (6)
apps/cli/src/lib/launch/run.ts (2)
apps/cli/src/types/process.ts (1)
  • Agent (41-45)
apps/cli/src/lib/launch/config.ts (1)
  • getLaunchCommand (19-42)
apps/cli/src/commands/agent.tsx (4)
apps/cli/src/lib/orchestrators/process-orchestrator.ts (2)
  • stopAll (277-314)
  • ProcessOrchestrator (50-330)
apps/cli/src/types/process.ts (2)
  • ProcessOrchestrator (47-62)
  • Process (15-29)
apps/cli/src/lib/launch/config.ts (1)
  • getDefaultLaunchCommand (48-58)
apps/cli/src/lib/launch/run.ts (1)
  • launchAgent (210-263)
apps/cli/src/commands/init.tsx (3)
apps/cli/src/lib/db.ts (1)
  • getDb (9-18)
apps/cli/src/lib/orchestrators/workspace-orchestrator.ts (1)
  • WorkspaceOrchestrator (14-224)
apps/cli/src/types/workspace.ts (1)
  • WorkspaceOrchestrator (28-50)
apps/cli/src/lib/orchestrators/process-orchestrator.ts (3)
apps/cli/src/types/process.ts (2)
  • Agent (41-45)
  • Process (15-29)
apps/cli/src/lib/launch/config.ts (1)
  • getDefaultLaunchCommand (48-58)
apps/cli/src/types/workspace.ts (1)
  • Workspace (6-16)
apps/cli/src/cli.tsx (7)
apps/cli/src/commands/init.tsx (1)
  • Init (37-401)
apps/cli/src/commands/dashboard.tsx (1)
  • Dashboard (28-539)
apps/cli/src/commands/panels.tsx (1)
  • Panels (26-420)
apps/cli/src/commands/workspace.tsx (1)
  • WorkspaceUse (232-280)
apps/cli/src/commands/agent.tsx (3)
  • AgentStart (430-675)
  • AgentAttach (682-781)
  • AgentStopAll (281-365)
apps/cli/src/lib/db.ts (1)
  • getDb (9-18)
apps/cli/src/lib/orchestrators/workspace-orchestrator.ts (1)
  • WorkspaceOrchestrator (14-224)
apps/cli/src/commands/panels.tsx (4)
apps/cli/src/types/process.ts (3)
  • Process (15-29)
  • ProcessOrchestrator (47-62)
  • Agent (41-45)
apps/cli/src/lib/db.ts (1)
  • getDb (9-18)
apps/cli/src/lib/orchestrators/process-orchestrator.ts (1)
  • ProcessOrchestrator (50-330)
apps/cli/src/lib/launch/run.ts (1)
  • launchAgent (210-263)
🪛 GitHub Actions: CI
apps/cli/src/commands/agent.tsx

[warning] 434-434: lint/suspicious/noExplicitAny: Unexpected any. Specify a different type.


[warning] 447-447: lint/suspicious/noImplicitAnyLet: This variable implicitly has the any type. Declare a type or initialize.

apps/cli/src/cli.tsx

[warning] 4-4: lint/correctness/noUnusedImports: Unused import React detected.

🪛 LanguageTool
apps/cli/readme.md

[style] ~43-~43: To form a complete sentence, be sure to include a subject.
Context: ...nment and state.currentWorkspaceId. - Can be overridden with `SUPERSET_CLI_DATA_D...

(MISSING_IT_THERE)

🔇 Additional comments (4)
apps/cli/readme.md (1)

1-48: Well-structured documentation for expanded CLI surface.

The README provides clear organization with practical Quick start, Core concepts, Common commands, and operational sections. Commands are well-documented with concrete examples, and tmux/storage integration details are helpful for users.

apps/cli/src/cli.tsx (1)

42-65: CLI command wiring and default action look coherent

The added commands (init, dashboard, panels, workspace use, agent start, agent attach, agent stop-all, and change group) are wired consistently:

  • Each command renders the corresponding Ink component with appropriate props.
  • Mutating commands pass onComplete={() => process.exit(0)} so the process terminates cleanly after UI completion.
  • agent start and agent attach rely on their components to control exit, which matches their more interactive flows.
  • The default program.action gives a helpful overview and attempts to show the current workspace, while safely swallowing DB-init errors.

No functional issues stand out here.

Also applies to: 66-178, 201-217, 271-284, 295-334, 335-381

apps/cli/src/commands/agent.tsx (1)

682-781: AgentAttach flow and failure handling look solid

The attach flow is robust:

  • Resolves the target process by ID first, then falls back to sessionName lookup among agents.
  • Validates that the located process is an agent before proceeding.
  • Exits Ink before attaching, then calls launchAgent(agent, { attach: true }) in setImmediate, which avoids conflicting with Ink’s input handling.
  • On attach failure, attempts to mark the agent STOPPED with an endedAt timestamp, logging a warning if persistence fails, and exits with a non-zero code.

No obvious logic or lifecycle issues here; the approach is consistent with the tmux-based orchestration elsewhere.

apps/cli/src/lib/orchestrators/process-orchestrator.ts (1)

1-45: ProcessOrchestrator tmux integration and stopAll semantics look consistent

The new tmux-related helpers and backfill logic are coherent:

  • backfillDefaults sets sane defaults for status and timestamps and upgrades legacy agent launchCommand / sessionName values.
  • needsPersist and syncAgentStatus gate persistence and keep agent.status in sync with tmux reality without over-writing healthy records.
  • create now generates deterministic IDs and session names and starts processes in IDLE, which aligns with the AgentStart flow.
  • stop and stopAll correctly:
    • kill tmux sessions for agents with a sessionName, ignoring already-dead sessions;
    • set endedAt/updatedAt and mark agents as STOPPED;
    • and stopAll returns the count of agents stopped and deliberately ignores terminals.

This matches the updated docs and CLI descriptions about stopping only agents, not terminals.

Also applies to: 53-94, 103-190, 192-222, 243-270, 272-314

Comment thread apps/cli/readme.md
Comment thread apps/cli/src/cli.tsx
@@ -3,23 +3,29 @@ import { Command } from "commander";
import { render } from "ink";
import React from "react";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Remove unused React import to satisfy lint

React is not referenced in this file; JSX elements are only passed to render. You can safely drop this import to clear the noUnusedImports warning:

-import React from "react";

Assuming your TS config uses the modern JSX transform (jsx: "react-jsx"), this will still compile.


🏁 Script executed:

#!/bin/bash
# Verify React import and usage in cli.tsx
echo "=== Examining apps/cli/src/cli.tsx ==="
cat apps/cli/src/cli.tsx | head -20
echo ""
echo "=== Searching for 'React' references in file ==="
rg -n 'React' apps/cli/src/cli.tsx || echo "No React references found"
echo ""
echo "=== Checking for JSX syntax ==="
rg -n 'JSX|<[A-Z]|</' apps/cli/src/cli.tsx | head -10

Length of output: 927


Remove unused React import from line 4

Verified: React is imported but never referenced in the file. The file uses JSX extensively (<Init />, <Dashboard />, <Panels />, etc.), but none of these reference the React object directly. With the modern JSX transform, this import is not required and can be safely removed:

-import React from "react";
📝 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
import React from "react";
🧰 Tools
🪛 GitHub Actions: CI

[warning] 4-4: lint/correctness/noUnusedImports: Unused import React detected.

🤖 Prompt for AI Agents
In apps/cli/src/cli.tsx around line 4, remove the unused import "import React
from \"react\";" because the file never references the React identifier and the
project uses the modern JSX transform; simply delete that import line and ensure
no other references to React exist so the file still compiles.

Comment on lines +145 to +160
const handleAgentsSelect = (item: { value: string }) => {
if (item.value === "done") {
createWorkspace();
} else if (item.value === "skip") {
setState((s) => ({ ...s, defaultAgents: [] }));
createWorkspace();
} else {
// Toggle agent selection
setState((s) => ({
...s,
defaultAgents: s.defaultAgents.includes(item.value as AgentType)
? s.defaultAgents.filter((a) => a !== item.value)
: [...s.defaultAgents, item.value as AgentType],
}));
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Skip default agents path still uses previously selected agents

In handleAgentsSelect, for "skip" you clear defaultAgents via setState and then immediately call createWorkspace(). Because React state updates are async, createWorkspace still sees the old state.defaultAgents, so the created workspace will keep those agents instead of none.

} else if (item.value === "skip") {
  setState((s) => ({ ...s, defaultAgents: [] }));
  createWorkspace();
}

But createWorkspace reads from the closed-over state:

const workspace = await orchestrator.create(
  envId,
  state.workspaceType!,
  {
    path: state.path || undefined,
    branch: state.branch || undefined,
    name: state.name || undefined,
    defaultAgents: state.defaultAgents,
  },
);

Consider passing the effective config into createWorkspace, or calculating it inside createWorkspace from the latest state passed via functional update:

- const createWorkspace = async () => {
+ const createWorkspace = async (overrides?: Partial<Pick<InitState, "defaultAgents">>) => {
+   const effective = { ...state, ...overrides };
    setState((s) => ({ ...s, step: InitStep.CREATING }));
@@
-   const workspace = await orchestrator.create(
-     envId,
-     state.workspaceType!,
-     {
-       path: state.path || undefined,
-       branch: state.branch || undefined,
-       name: state.name || undefined,
-       defaultAgents: state.defaultAgents,
-     },
-   );
+   const workspace = await orchestrator.create(
+     envId,
+     effective.workspaceType!,
+     {
+       path: effective.path || undefined,
+       branch: effective.branch || undefined,
+       name: effective.name || undefined,
+       defaultAgents: effective.defaultAgents,
+     },
+   );
@@
-} else if (item.value === "skip") {
-  setState((s) => ({ ...s, defaultAgents: [] }));
-  createWorkspace();
+} else if (item.value === "skip") {
+  createWorkspace({ defaultAgents: [] });

Also applies to: 162-215


I'll help verify this review comment by examining the codebase. Let me investigate the code structure and the specific issue identified.


---

🏁 Script executed:

```shell
#!/bin/bash
# First, let's view the full context of the handleAgentsSelect function and createWorkspace
cat apps/cli/src/commands/init.tsx | head -250 | tail -120

Length of output: 2987


🏁 Script executed:

#!/bin/bash
# Search for all createWorkspace definitions and calls to understand the closure issue
rg -n "createWorkspace" apps/cli/src/commands/init.tsx -B2 -A5

Length of output: 629


🏁 Script executed:

#!/bin/bash
# Check the exact lines around 145-160 and 162-215 for context
wc -l apps/cli/src/commands/init.tsx

Length of output: 34


🏁 Script executed:

#!/bin/bash
# Look for setState calls followed by function calls that might have the same issue
rg -n "setState.*defaultAgents" apps/cli/src/commands/init.tsx -B1 -A3

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Get full view of the file to understand the structure
head -250 apps/cli/src/commands/init.tsx

Length of output: 6021


I need to check if there are other similar patterns with the async state issue:


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for all setState calls in handleAgentsSelect and surrounding code
rg -n "setState" apps/cli/src/commands/init.tsx -B1 -A2

Length of output: 2210


🏁 Script executed:

#!/bin/bash
# Check if there are other handler functions that might have similar async issues
sed -n '45,250p' apps/cli/src/commands/init.tsx | grep -n "const handle\|setState\|createWorkspace\|async"

Length of output: 738


🏁 Script executed:

#!/bin/bash
# Check the entire file for any async function calls immediately after setState
rg -n "setState.*\);\s*(await|createWorkspace|orchestrator\.|envOrchestrator\.)" apps/cli/src/commands/init.tsx

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at handleBranchSubmit to understand the trim issue mentioned
sed -n '95,135p' apps/cli/src/commands/init.tsx

Length of output: 843


Async state update causes skip to preserve previously selected agents

In handleAgentsSelect, when the user selects "skip", you call setState({ defaultAgents: [] }) followed immediately by createWorkspace(). Because React state updates are asynchronous, createWorkspace reads state.defaultAgents from the closure before the state change applies, so the workspace is created with the previously selected agents instead of an empty list.

} else if (item.value === "skip") {
  setState((s) => ({ ...s, defaultAgents: [] }));
  createWorkspace();
}

Pass the effective value explicitly to createWorkspace:

- const createWorkspace = async () => {
+ const createWorkspace = async (overrides?: Partial<Pick<InitState, "defaultAgents">>) => {
+   const effective = { ...state, ...overrides };
    setState((s) => ({ ...s, step: InitStep.CREATING }));

    const workspace = await orchestrator.create(
      envId,
      effective.workspaceType!,
      {
        path: effective.path || undefined,
        branch: effective.branch || undefined,
        name: effective.name || undefined,
        defaultAgents: effective.defaultAgents,
      },
    );

- } else if (item.value === "skip") {
-   setState((s) => ({ ...s, defaultAgents: [] }));
-   createWorkspace();
+ } else if (item.value === "skip") {
+   createWorkspace({ defaultAgents: [] });
🤖 Prompt for AI Agents
In apps/cli/src/commands/init.tsx around lines 145 to 160, the code sets
defaultAgents to [] then immediately calls createWorkspace(), but because
setState is async the old agents may still be used; instead compute the intended
agents value and pass it explicitly to createWorkspace (for example determine
agents = item.value === "skip" ? [] : current/updated list) then call setState
with the new defaultAgents and call createWorkspace(agents) (or otherwise update
createWorkspace to accept the agents list) so the workspace is created with the
correct effective agents.

Comment on lines +39 to +63
const loadData = React.useCallback(async () => {
try {
const db = getDb();
const workspaceOrchestrator = new WorkspaceOrchestrator(db);
const processOrchestrator = new ProcessOrchestrator(db);

const [workspaces, processes, currentWorkspace] = await Promise.all([
workspaceOrchestrator.list(),
processOrchestrator.list(),
workspaceOrchestrator.getCurrent(),
]);

setData({
workspaces,
processes,
currentWorkspaceId: currentWorkspace?.id,
lastRefresh: new Date(),
});

setLoading(false);
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
setLoading(false);
}
}, []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Error state never cleared after a successful loadData refresh

On a transient failure, setError(...) is called, but on later successful refreshes error is never reset, so the component will keep rendering the error screen even with fresh data.

Consider clearing the error on success:

 const loadData = React.useCallback(async () => {
 	try {
 		const db = getDb();
 		const workspaceOrchestrator = new WorkspaceOrchestrator(db);
 		const processOrchestrator = new ProcessOrchestrator(db);
@@
-		setData({
+		setData({
 			workspaces,
 			processes,
 			currentWorkspaceId: currentWorkspace?.id,
 			lastRefresh: new Date(),
 		});
-
-		setLoading(false);
+		setError(null);
+		setLoading(false);
 	} catch (err) {
 		setError(err instanceof Error ? err.message : "Unknown error");
 		setLoading(false);
 	}
 }, []);

🏁 Script executed:

#!/bin/bash
# Inspect the actual file to verify the loadData implementation
cat apps/cli/src/commands/panels.tsx | head -100

Length of output: 2862


🏁 Script executed:

#!/bin/bash
# Search for all setError calls and error-related state in the file
rg -n 'setError|error' apps/cli/src/commands/panels.tsx -C2

Length of output: 1133


🏁 Script executed:

#!/bin/bash
# Check if error is initialized and how the component handles it on initial load
rg -n 'useState.*error|const.*error' apps/cli/src/commands/panels.tsx -C2

Length of output: 344


🏁 Script executed:

#!/bin/bash
# Search for conditional rendering that might depend on error state
rg -n 'if.*error|error.*?' apps/cli/src/commands/panels.tsx -C1

Length of output: 871


🏁 Script executed:

#!/bin/bash
# Get the complete render/return section to verify error handling in output
tail -n +175 apps/cli/src/commands/panels.tsx | head -30

Length of output: 715


🏁 Script executed:

#!/bin/bash
# Search for any other places where error state might be reset
rg -n 'setError\(' apps/cli/src/commands/panels.tsx

Length of output: 69


Error state never cleared after successful loadData refresh

On transient failure, setError() is called (line 60), but on subsequent successful refreshes the error is never reset. Since the render checks if (error) before rendering data (lines 180–181), the component will keep displaying the stale error screen even with fresh successful data.

The suggested fix is correct: add setError(null) after successful data update:

 const loadData = React.useCallback(async () => {
 	try {
 		const db = getDb();
 		const workspaceOrchestrator = new WorkspaceOrchestrator(db);
 		const processOrchestrator = new ProcessOrchestrator(db);

 		const [workspaces, processes, currentWorkspace] = await Promise.all([
 			workspaceOrchestrator.list(),
 			processOrchestrator.list(),
 			workspaceOrchestrator.getCurrent(),
 		]);

 		setData({
 			workspaces,
 			processes,
 			currentWorkspaceId: currentWorkspace?.id,
 			lastRefresh: new Date(),
 		});
+		setError(null);
 		setLoading(false);
 	} catch (err) {
 		setError(err instanceof Error ? err.message : "Unknown error");
 		setLoading(false);
 	}
 }, []);
📝 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
const loadData = React.useCallback(async () => {
try {
const db = getDb();
const workspaceOrchestrator = new WorkspaceOrchestrator(db);
const processOrchestrator = new ProcessOrchestrator(db);
const [workspaces, processes, currentWorkspace] = await Promise.all([
workspaceOrchestrator.list(),
processOrchestrator.list(),
workspaceOrchestrator.getCurrent(),
]);
setData({
workspaces,
processes,
currentWorkspaceId: currentWorkspace?.id,
lastRefresh: new Date(),
});
setLoading(false);
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
setLoading(false);
}
}, []);
const loadData = React.useCallback(async () => {
try {
const db = getDb();
const workspaceOrchestrator = new WorkspaceOrchestrator(db);
const processOrchestrator = new ProcessOrchestrator(db);
const [workspaces, processes, currentWorkspace] = await Promise.all([
workspaceOrchestrator.list(),
processOrchestrator.list(),
workspaceOrchestrator.getCurrent(),
]);
setData({
workspaces,
processes,
currentWorkspaceId: currentWorkspace?.id,
lastRefresh: new Date(),
});
setError(null);
setLoading(false);
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
setLoading(false);
}
}, []);
🤖 Prompt for AI Agents
In apps/cli/src/commands/panels.tsx around lines 39 to 63, loadData sets an
error on failure but never clears it on success, causing stale error UI to
persist; update the success path to call setError(null) after successfully
setting data (before or alongside setLoading(false)) so any previous error is
cleared when new data loads.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
apps/cli/src/lib/launch/run.ts (1)

1-7: Critical shell injection vulnerability remains unaddressed

The past review identified a critical shell injection vulnerability at line 100 (previously line 91) where command and sessionName are interpolated into a shell string:

await execAsync(`tmux new-session -d -s "${sessionName}" "${command}"`, {

This issue was marked as "✅ Addressed in commit 64fbac1" but the vulnerable code pattern is still present. The command variable comes from environment variables (SUPERSET_AGENT_LAUNCH_*) and can contain shell metacharacters that will be interpreted by the shell.

The fix suggested in the previous review should be applied immediately: replace execAsync with spawn using an argument array to prevent shell interpolation.

Also applies to: 100-102

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8eec9bf and f928f75.

📒 Files selected for processing (1)
  • apps/cli/src/lib/launch/run.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/cli/src/lib/launch/run.ts (2)
apps/cli/src/types/process.ts (1)
  • Agent (41-45)
apps/cli/src/lib/launch/config.ts (1)
  • getLaunchCommand (19-42)
🔇 Additional comments (4)
apps/cli/src/lib/launch/run.ts (4)

8-12: LGTM!

Clean interface definition for launch results.


17-24: LGTM!

Safe implementation using a static command string with no interpolation.


210-267: LGTM with caveats!

The launch logic is well-structured with appropriate validation, preflight checks, and clear error messages. The shell wrapper detection (lines 234-236) is a thoughtful addition to avoid false negatives.

However, note that commandExists() called at line 238 has a critical shell injection vulnerability that must be addressed.


275-372: LGTM - demonstrates the correct spawn pattern!

This function correctly uses spawn with an argument array at line 317:

const child = spawn("tmux", ["attach", "-t", sessionName], {
	stdio: "inherit",
	detached: false,
});

This is the safe pattern that should be applied to the other tmux invocations in this file (lines 31, 45, 60, and especially line 100). No shell interpolation means no injection risk.

The retry logic and error handling are well-implemented, and the UI decoration provides good UX.

Comment on lines +29 to +38
function tmuxSessionExists(sessionName: string): boolean {
try {
execSync(`tmux has-session -t "${sessionName}" 2>/dev/null`, {
stdio: "ignore",
});
return true;
} catch {
return false;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential shell injection in session name handling

Three utility functions interpolate sessionName directly into shell strings:

  • Line 31: tmux has-session -t "${sessionName}"
  • Line 45: tmux kill-session -t "${sessionName}"
  • Line 60: tmux list-panes -t "${sessionName}"

While sessionName is typically derived from agent.sessionName or generated as agent-${agent.id.slice(0,6)}, if agent.sessionName is user-controlled and contains shell metacharacters (e.g., "; rm -rf /;"), these could become injection vectors.

Consider refactoring to use spawn with argument arrays instead of shell string interpolation for defense in depth.

Also applies to: 43-51, 57-71

🤖 Prompt for AI Agents
In apps/cli/src/lib/launch/run.ts around lines 29-38 (and similarly 43-51,
57-71), the code interpolates sessionName into shell command strings which
allows shell injection; replace execSync usage with a non-shell child_process
call (spawnSync or execFileSync) passing the tmux subcommand and session name as
separate arguments (e.g., ["has-session", "-t", sessionName]) and set shell:
false / use execFileSync so the sessionName is not interpreted by a shell;
additionally validate or constrain sessionName to an allowed pattern (e.g.,
alphanumeric, dash, underscore, max length) before invoking the process and
preserve existing stdio/error handling and return value semantics.

Comment on lines +76 to +83
function commandExists(binary: string): boolean {
try {
execSync(`command -v ${binary}`, { stdio: "ignore" });
return true;
} catch {
return false;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical shell injection in commandExists

Line 78 interpolates binary into a shell command without quotes:

execSync(`command -v ${binary}`, { stdio: "ignore" });

Since binary ultimately comes from user-controlled environment variables (SUPERSET_AGENT_LAUNCH_*), an attacker could inject shell commands. For example, if SUPERSET_AGENT_LAUNCH_FOO="evil; rm -rf /", then binary becomes "evil;" and arbitrary code executes.

Apply this diff to fix:

-		execSync(`command -v ${binary}`, { stdio: "ignore" });
+		execSync("command", ["-v", binary], { stdio: "ignore" });

Or better yet, use spawn:

-function commandExists(binary: string): boolean {
-	try {
-		execSync(`command -v ${binary}`, { stdio: "ignore" });
-		return true;
-	} catch {
-		return false;
-	}
-}
+function commandExists(binary: string): boolean {
+	try {
+		// Use which as a safer alternative that accepts arguments
+		execSync(`which "${binary.replace(/"/g, '\\"')}"`, { stdio: "ignore" });
+		return true;
+	} catch {
+		return false;
+	}
+}

Note: execSync doesn't accept an array as second parameter. Use which with escaped quotes as shown, or refactor to spawnSync.

🤖 Prompt for AI Agents
In apps/cli/src/lib/launch/run.ts around lines 76 to 83, commandExists currently
interpolates the user-controlled `binary` into a shell string causing a critical
shell-injection risk; replace the execSync call with a shell-argument-safe API
(e.g., use child_process.spawnSync or execFile/execFileSync) and call a
non-shell program like `which` (or `command` as an executable if available)
passing `binary` as an argument array so it is never interpreted by a shell;
return true when the child process exits with code 0 and false otherwise, and
optionally validate `binary` against a whitelist/allowed pattern before invoking
the child process.

@AviPeltz AviPeltz merged commit d0ca08a into main Nov 20, 2025
1 of 5 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Nov 21, 2025
5 tasks
@Kitenite Kitenite deleted the new-cli-update branch November 21, 2025 18:05
This was referenced Nov 21, 2025
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