Skip to content

feat(desktop): add static port configuration via ports.json#683

Merged
Kitenite merged 9 commits intosuperset-sh:mainfrom
steven-terrana:feat/603
Jan 9, 2026
Merged

feat(desktop): add static port configuration via ports.json#683
Kitenite merged 9 commits intosuperset-sh:mainfrom
steven-terrana:feat/603

Conversation

@steven-terrana
Copy link
Copy Markdown
Contributor

@steven-terrana steven-terrana commented Jan 8, 2026

Links

Summary

  • Users can define static ports in .superset/ports.json to replace dynamic port discovery
  • Static ports display with custom labels in the sidebar, with live reload when the config file changes
  • Marketing documentation added at /ports explaining the feature

Why / Context

Superset automatically detects ports from terminal processes, but some users need:

  • Ports that aren't auto-detected (databases, external services running outside terminals)
  • Meaningful labels for team documentation
  • Consistent port configuration across branches

Static ports allow users to define their own port list with descriptive labels that the whole team can use.

How It Works

  1. Detection: When a workspace is active, we check for .superset/ports.json in the worktree
  2. Loading: If found, parse and validate the JSON (port numbers 1-65535, non-empty labels)
  3. Display: Static ports replace dynamic ports in the sidebar with badge showing label
  4. Live reload: File watcher detects changes and invalidates tRPC queries automatically
  5. Error handling: Invalid configs show toast notification with specific error message

Key files:

  • static-ports/loader.ts - JSON parsing and validation
  • static-ports/watcher.ts - fs.watch with debouncing for live reload
  • ports.ts (router) - tRPC procedures for hasStaticConfig, getStatic, subscribeStatic
  • PortsList.tsx - Conditional rendering based on static vs dynamic mode

Manual QA Checklist

Static Config Detection

  • Workspace without ports.json shows dynamic ports as before
  • Creating .superset/ports.json switches to static mode (may need to refocus workspace)
  • Deleting ports.json switches back to dynamic mode

Static Ports Display

  • Static ports appear as clickable badges in sidebar
  • Hovering shows custom label in tooltip
  • Clicking external link icon opens localhost:PORT in browser
  • Ports are sorted by port number

Live Reload

  • Editing ports.json updates display without app restart
  • Adding new port entries updates immediately
  • Removing port entries updates immediately

Error Handling

  • Invalid JSON syntax shows toast: "Invalid JSON in ports.json: ..."
  • Missing ports key shows toast: "ports.json is missing required field 'ports'"
  • Invalid port number shows toast: "ports[N].port must be between 1 and 65535"
  • Empty label shows toast: "ports[N].label cannot be empty"
  • No ports displayed when config is invalid (no fallback to dynamic)

Marketing Page

Testing

  • bun run typecheck
  • bun run lint
  • bun test apps/desktop/src/main/lib/static-ports/loader.test.ts - 24 tests passing

Design Decisions

  • Why static replaces dynamic (not merges): Users opting for static config likely want full control. Merging would create unpredictable behavior when dynamic ports change.
  • Why toast for errors instead of inline: Consistent with other config errors in the app. Toast is dismissible and non-blocking.
  • Why workspace-scoped: Different branches can have different port configs, which is useful for teams working on different features with different service requirements.

Known Limitations

  • Static ports don't show process/PID info (by design - static ports may not be running)
  • File watcher relies on fs.watch which may have platform-specific quirks

Follow-ups

  • Consider adding port status indicator (green if listening, gray if not) in future PR

Summary by CodeRabbit

  • New Features

    • Static ports via a ports.json file, merged with detected ports and shown in the sidebar with updated badges and browser-open actions.
    • Live watching of ports.json with immediate UI updates and deduplicated error notifications.
  • Bug Fixes

    • Stronger validation and clearer error reporting for malformed port configurations.
  • Documentation

    • New guided docs page with schema examples and configuration guidance.
  • Tests

    • Unit tests covering loading, validation, and watcher behavior.

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

Users can now define static ports in a `.superset/ports.json` file within their
workspace. When present, static ports replace dynamic port discovery in the
sidebar.

Features:
- New static-ports module with loader and file watcher
- tRPC procedures: hasStaticConfig, getStatic, subscribeStatic
- Auto-reload when ports.json changes
- Toast notifications for config errors
- Marketing documentation page at /ports

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 8, 2026

Warning

Rate limit exceeded

@Kitenite has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 18 minutes and 51 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between a452140 and c017c96.

📒 Files selected for processing (4)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/WorkspacePortGroup/WorkspacePortGroup.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/WorkspacePortGroup/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/hooks/usePortsData.ts
📝 Walkthrough

Walkthrough

Adds static ports via a ports.json workflow: new types/constants, loader with validation, filesystem watcher, tRPC endpoints (hasStaticConfig/getStatic/getAllStatic/subscribeStatic), renderer store fields/actions, merge layer for static+dynamic ports, UI components/badges, tests, and a marketing docs page.

Changes

Cohort / File(s) Summary
Types & Constant
apps/desktop/src/shared/types/ports.ts, apps/desktop/src/shared/constants.ts
Added StaticPort, StaticPortsResult, MergedPort and exported PORTS_FILE_NAME = "ports.json".
Loader & API exports
apps/desktop/src/main/lib/static-ports/loader.ts, apps/desktop/src/main/lib/static-ports/index.ts
New loadStaticPorts(worktreePath) with schema/entry validation and hasStaticPortsConfig; re-exported loader and watcher APIs.
Loader Tests
apps/desktop/src/main/lib/static-ports/loader.test.ts
Comprehensive unit tests for missing/valid/malformed files, validation errors, boundary ports, empty arrays, and hasStaticPortsConfig.
Watcher
apps/desktop/src/main/lib/static-ports/watcher.ts
New StaticPortsWatcher singleton: per-workspace fs watchers, debounce, watch/unwatch/unwatchAll, and change events.
tRPC Router
apps/desktop/src/lib/trpc/routers/ports/ports.ts
Added procedures: hasStaticConfig, getStatic, getAllStatic, subscribeStatic integrating loader and watcher; existing endpoints preserved.
Renderer store
apps/desktop/src/renderer/stores/ports/store.ts
Added state/actions: staticPorts, staticPortsError, useStaticPorts, setStaticPorts, setStaticPortsError, setUseStaticPorts, clearStaticPorts.
PortsList UI & utilities
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx, .../MergedPortBadge/*, .../utils/merge-ports.ts, .../utils/index.ts
Introduced mergePorts to combine static and dynamic ports, replaced PortBadge with MergedPortBadge, merged grouping model, subscribe/getAllStatic usage, and error-toasting/deduplication.
Marketing & Plan
apps/marketing/src/app/ports/page.tsx, apps/desktop/plans/20260108-2251-static-ports-json.md
Added static-ports documentation/marketing page and planning/validation guidance.

Sequence Diagram(s)

sequenceDiagram
    participant UI as PortsList UI
    participant Store as Renderer Store
    participant tRPC as tRPC Router
    participant Loader as Static Ports Loader
    participant FS as File System
    participant Watcher as StaticPortsWatcher

    UI->>tRPC: hasStaticConfig(workspaceId)
    tRPC->>Loader: hasStaticPortsConfig(worktreePath)
    Loader->>FS: Stat .superset/ports.json
    FS-->>Loader: exists?/mtime
    Loader-->>tRPC: { hasStatic }
    tRPC-->>UI: hasStatic result

    alt Static Config Available
        UI->>tRPC: getStatic(workspaceId)
        tRPC->>Loader: loadStaticPorts(worktreePath)
        Loader->>FS: Read & parse ports.json
        Loader->>Loader: Validate schema & entries
        Loader-->>tRPC: { ports | error }
        tRPC-->>Store: setStaticPorts / setStaticPortsError
        Store-->>UI: render merged ports via mergePorts

        UI->>tRPC: subscribeStatic(workspaceId)
        tRPC->>Watcher: watch(workspaceId, worktreePath)
        Watcher->>FS: monitor .superset/ports.json
        FS-->>Watcher: file change event
        Watcher->>tRPC: emit change for workspaceId
        tRPC-->>UI: notify to reload
        UI->>tRPC: getStatic(workspaceId) (reload)
    else No Static Config
        UI->>tRPC: fallback to dynamic ports flow
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Hop-hop, I found a ports.json,

Labels trimmed and numbers bless,
Watchers hum and loaders parse,
Badges bloom across each workspace,
A rabbit cheers: static ports, success!

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding static port configuration via ports.json file to the desktop app.
Description check ✅ Passed The description comprehensively covers the feature, including context, implementation details, testing results, design decisions, and limitations.
Linked Issues check ✅ Passed The PR implements static port configuration via ports.json, which addresses the core goal of #603 (port-to-label mappings). However, the implementation differs from the original script-based proposal: static file instead of periodic command execution.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing static port configuration. No unrelated modifications detected.

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


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

❤️ Share

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

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

🤖 Fix all issues with AI agents
In @apps/desktop/plans/20260108-2251-static-ports-json.md:
- Around line 122-124: Update the "Toast Notifications" section to use the
correct import path: replace the reference to importing from "sonner" with
importing from "@superset/ui/sonner" and show using toast (e.g.,
toast.error("message")); ensure the section aligns with the "Surprises &
Discoveries" note and mentions the symbol "toast" imported from
"@superset/ui/sonner" instead of "sonner".
- Around line 200-217: Remove or strike through the "Milestone 4: Renderer Store
Updates" section that proposes adding staticPorts, staticPortsError,
useStaticPorts and related actions (setStaticPorts, setStaticPortsError,
setUseStaticPorts, clearStaticPorts) since the Decision Log indicates the
implementation uses tRPC queries directly and the ports store was not changed;
update the markdown to reflect that the store changes were intentionally skipped
to avoid confusion.
🧹 Nitpick comments (6)
apps/desktop/src/shared/types/ports.ts (1)

11-21: Consider defining a separate public type for clarity.

The types are functionally correct. However, using Omit<StaticPort, "workspaceId"> for the public API adds cognitive overhead. Consider defining a separate PublicStaticPort type to make the distinction explicit:

export interface PublicStaticPort {
  port: number;
  label: string;
}

export interface StaticPort extends PublicStaticPort {
  workspaceId: string;
}

export interface StaticPortsResult {
  exists: boolean;
  ports: PublicStaticPort[] | null;
  error: string | null;
}

This approach makes the public vs internal distinction clearer and is easier to understand at a glance. The current Omit approach works but requires readers to mentally compute the resulting type.

♻️ Alternative type structure
+export interface PublicStaticPort {
+  port: number;
+  label: string;
+}
+
-export interface StaticPort {
+export interface StaticPort extends PublicStaticPort {
-  port: number;
-  label: string;
   workspaceId: string;
 }

 export interface StaticPortsResult {
   exists: boolean;
-  ports: Omit<StaticPort, "workspaceId">[] | null;
+  ports: PublicStaticPort[] | null;
   error: string | null;
 }
apps/desktop/src/main/lib/static-ports/loader.test.ts (1)

6-9: Consider extracting magic strings to constants.

The test constants reference .superset and ports.json as strings. While this works, consider importing PROJECT_SUPERSET_DIR_NAME and PORTS_FILE_NAME from @/shared/constants to ensure consistency with the implementation and avoid duplication.

♻️ Use shared constants
+import { PROJECT_SUPERSET_DIR_NAME, PORTS_FILE_NAME } from "@/shared/constants";
 import { join } from "node:path";
 import { hasStaticPortsConfig, loadStaticPorts } from "./loader";
 
 const TEST_DIR = join(__dirname, ".test-tmp");
 const WORKTREE_PATH = join(TEST_DIR, "worktree");
-const SUPERSET_DIR = join(WORKTREE_PATH, ".superset");
-const PORTS_FILE = join(SUPERSET_DIR, "ports.json");
+const SUPERSET_DIR = join(WORKTREE_PATH, PROJECT_SUPERSET_DIR_NAME);
+const PORTS_FILE = join(SUPERSET_DIR, PORTS_FILE_NAME);
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx (1)

326-353: Consider extracting StaticPortBadge to its own file.

Per coding guidelines, each file should contain one component. StaticPortBadge is a distinct, reusable component that could be extracted to maintain consistency with project conventions.

That said, given it's tightly coupled to PortsList and relatively small, this is a minor concern that could be addressed in a follow-up.

apps/desktop/src/main/lib/static-ports/loader.ts (1)

128-139: Consider validating for duplicate port numbers.

If a user accidentally defines the same port twice in their ports.json, both entries will be loaded. This could cause confusion in the UI. Consider adding a check for duplicate ports and returning an appropriate error.

♻️ Proposed fix to detect duplicate ports
 	const validatedPorts: Array<{ port: number; label: string }> = [];
+	const seenPorts = new Set<number>();

 	for (let i = 0; i < parsed.ports.length; i++) {
 		const entry = parsed.ports[i] as PortEntry;
 		const result = validatePortEntry(entry, i);

 		if (!result.valid) {
 			return { exists: true, ports: null, error: result.error };
 		}

+		if (seenPorts.has(result.port)) {
+			return {
+				exists: true,
+				ports: null,
+				error: `ports[${i}].port ${result.port} is a duplicate`,
+			};
+		}
+		seenPorts.add(result.port);
+
 		validatedPorts.push({ port: result.port, label: result.label });
 	}
apps/desktop/src/main/lib/static-ports/watcher.ts (1)

103-112: Extract debounce delay to a named constant.

Per coding guidelines, magic numbers should be extracted to named constants at module top.

♻️ Proposed fix
+const DEBOUNCE_DELAY_MS = 100;
+
 /**
  * Watches for changes to ports.json files across workspaces.

Then at line 112:

-			}, 100);
+			}, DEBOUNCE_DELAY_MS);
apps/desktop/src/lib/trpc/routers/ports/ports.ts (1)

52-56: Consider extracting workspace lookup to a helper.

The pattern of looking up workspace by ID and getting its path is repeated in all three new procedures. A helper function would reduce duplication.

♻️ Proposed refactor

Add a helper at the top of the file or in a utils module:

function getWorkspacePathById(workspaceId: string): string | null {
  const workspace = localDb
    .select()
    .from(workspaces)
    .where(eq(workspaces.id, workspaceId))
    .get();

  if (!workspace) return null;
  return getWorkspacePath(workspace);
}

Then simplify each procedure:

 hasStaticConfig: publicProcedure
   .input(z.object({ workspaceId: z.string() }))
   .query(({ input }): { hasStatic: boolean } => {
-    const workspace = localDb
-      .select()
-      .from(workspaces)
-      .where(eq(workspaces.id, input.workspaceId))
-      .get();
-
-    if (!workspace) {
-      return { hasStatic: false };
-    }
-
-    const workspacePath = getWorkspacePath(workspace);
+    const workspacePath = getWorkspacePathById(input.workspaceId);
     if (!workspacePath) {
       return { hasStatic: false };
     }

     return { hasStatic: hasStaticPortsConfig(workspacePath) };
   }),

Also applies to: 75-79, 116-120

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 366a083 and 1724d4f.

📒 Files selected for processing (10)
  • apps/desktop/plans/20260108-2251-static-ports-json.md
  • apps/desktop/src/lib/trpc/routers/ports/ports.ts
  • apps/desktop/src/main/lib/static-ports/index.ts
  • apps/desktop/src/main/lib/static-ports/loader.test.ts
  • apps/desktop/src/main/lib/static-ports/loader.ts
  • apps/desktop/src/main/lib/static-ports/watcher.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
  • apps/desktop/src/shared/constants.ts
  • apps/desktop/src/shared/types/ports.ts
  • apps/marketing/src/app/ports/page.tsx
🧰 Additional context used
📓 Path-based instructions (7)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/shared/types/ports.ts
  • apps/desktop/src/shared/constants.ts
  • apps/desktop/src/main/lib/static-ports/loader.ts
  • apps/desktop/src/main/lib/static-ports/watcher.ts
  • apps/desktop/src/lib/trpc/routers/ports/ports.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
  • apps/desktop/src/main/lib/static-ports/loader.test.ts
  • apps/desktop/src/main/lib/static-ports/index.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • apps/desktop/src/shared/types/ports.ts
  • apps/desktop/src/shared/constants.ts
  • apps/marketing/src/app/ports/page.tsx
  • apps/desktop/src/main/lib/static-ports/loader.ts
  • apps/desktop/src/main/lib/static-ports/watcher.ts
  • apps/desktop/src/lib/trpc/routers/ports/ports.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
  • apps/desktop/src/main/lib/static-ports/loader.test.ts
  • apps/desktop/src/main/lib/static-ports/index.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/shared/types/ports.ts
  • apps/desktop/src/shared/constants.ts
  • apps/marketing/src/app/ports/page.tsx
  • apps/desktop/src/main/lib/static-ports/loader.ts
  • apps/desktop/src/main/lib/static-ports/watcher.ts
  • apps/desktop/src/lib/trpc/routers/ports/ports.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
  • apps/desktop/src/main/lib/static-ports/loader.test.ts
  • apps/desktop/src/main/lib/static-ports/index.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • apps/desktop/src/shared/types/ports.ts
  • apps/desktop/src/shared/constants.ts
  • apps/marketing/src/app/ports/page.tsx
  • apps/desktop/src/main/lib/static-ports/loader.ts
  • apps/desktop/src/main/lib/static-ports/watcher.ts
  • apps/desktop/src/lib/trpc/routers/ports/ports.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
  • apps/desktop/src/main/lib/static-ports/loader.test.ts
  • apps/desktop/src/main/lib/static-ports/index.ts
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

One component per file - do not create multi-component files

Files:

  • apps/marketing/src/app/ports/page.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
apps/desktop/src/renderer/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never import Node.js modules (fs, path, os, net) in renderer process or shared code - they are externalized for browser compatibility

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Co-locate tests with implementation files using .test.ts or .test.tsx suffix

Files:

  • apps/desktop/src/main/lib/static-ports/loader.test.ts
🧠 Learnings (2)
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/desktop/src/renderer/**/*.{ts,tsx} : Never import Node.js modules (fs, path, os, net) in renderer process or shared code - they are externalized for browser compatibility

Applied to files:

  • apps/desktop/src/main/lib/static-ports/index.ts
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/desktop/src/lib/electron-router-dom.ts : Do not import Node.js modules like node:path or dotenv in electron-router-dom.ts and similar shared files - they run in both main and renderer processes

Applied to files:

  • apps/desktop/src/main/lib/static-ports/index.ts
🧬 Code graph analysis (4)
apps/desktop/src/main/lib/static-ports/loader.ts (3)
apps/desktop/src/main/lib/static-ports/index.ts (2)
  • loadStaticPorts (1-1)
  • hasStaticPortsConfig (1-1)
apps/desktop/src/shared/types/ports.ts (1)
  • StaticPortsResult (17-21)
apps/desktop/src/shared/constants.ts (2)
  • PROJECT_SUPERSET_DIR_NAME (33-33)
  • PORTS_FILE_NAME (36-36)
apps/desktop/src/lib/trpc/routers/ports/ports.ts (4)
apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts (1)
  • getWorkspacePath (22-43)
apps/desktop/src/main/lib/static-ports/loader.ts (2)
  • hasStaticPortsConfig (150-157)
  • loadStaticPorts (69-142)
apps/desktop/src/shared/types/ports.ts (1)
  • StaticPort (11-15)
apps/desktop/src/main/lib/static-ports/watcher.ts (1)
  • staticPortsWatcher (155-155)
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx (4)
packages/ui/src/components/ui/sonner.tsx (1)
  • toast (40-40)
apps/desktop/src/shared/types/ports.ts (1)
  • StaticPort (11-15)
packages/ui/src/components/ui/tooltip.tsx (3)
  • Tooltip (76-76)
  • TooltipTrigger (76-76)
  • TooltipContent (76-76)
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/constants.ts (1)
  • STROKE_WIDTH (2-2)
apps/desktop/src/main/lib/static-ports/loader.test.ts (1)
apps/desktop/src/main/lib/static-ports/loader.ts (2)
  • loadStaticPorts (69-142)
  • hasStaticPortsConfig (150-157)
🪛 markdownlint-cli2 (0.18.1)
apps/desktop/plans/20260108-2251-static-ports-json.md

257-257: Code block style
Expected: indented; Actual: fenced

(MD046, code-block-style)

🔇 Additional comments (16)
apps/desktop/src/shared/constants.ts (1)

36-36: LGTM! Constant follows established patterns.

The constant is properly named, well-placed near related configuration constants, and follows the existing conventions in this file.

apps/desktop/src/main/lib/static-ports/index.ts (1)

1-2: LGTM! Appropriate use of barrel exports for public API.

This barrel export file provides a clean, controlled public API surface for the static-ports module. While the coding guidelines generally discourage barrel files, this is an appropriate exception as it:

  • Defines a clear module boundary
  • Exposes only the intended public functions
  • Shows no risk of circular dependencies (loader and watcher are independent)
apps/marketing/src/app/ports/page.tsx (2)

1-237: LGTM! Documentation is accurate and well-structured.

The marketing page provides clear, comprehensive documentation that accurately reflects the implementation. The content is well-organized into logical sections, and all technical details match the actual behavior defined in the loader and types.

Key strengths:

  • Accurate schema documentation matching StaticPort type
  • Correct file paths and validation rules
  • Clear explanation of workspace-scoped behavior
  • Appropriate use of code examples

156-156: Toast notification implementation already exists in the codebase.

The error toast for malformed ports.json is fully implemented in apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx. When static ports fail to load, the component displays an error toast with toast.error("Failed to load ports.json", { description: ... }) and prevents duplicate notifications using a ref tracker. This matches and validates the documentation claim at line 156 of the marketing page.

Likely an incorrect or invalid review comment.

apps/desktop/src/main/lib/static-ports/loader.test.ts (1)

1-285: LGTM! Comprehensive test coverage with excellent quality.

The test suite demonstrates exceptional thoroughness:

Coverage highlights:

  • ✅ All success paths (missing file, valid configs, edge cases)
  • ✅ All validation error paths (JSON syntax, schema, types, ranges)
  • ✅ Boundary testing (ports 1 and 65535, empty arrays, whitespace)
  • ✅ Error message specificity (including array indices)
  • ✅ Both public functions (loadStaticPorts and hasStaticPortsConfig)

Test quality:

  • Clear, descriptive test names
  • Proper test isolation with setup/teardown
  • Specific assertions on result structure
  • Covers both positive and negative cases
  • Matches loader implementation validation rules exactly

The 24 passing tests provide strong confidence in the loader's correctness.

apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx (3)

31-59: LGTM! Clean tRPC integration for static ports.

The static ports data flow is well-structured:

  • hasStaticConfig query gates the feature
  • getStatic query fetches ports data when enabled
  • subscribeStatic subscription handles live reload by invalidating queries

The subscription being always enabled (when workspace exists) is appropriate since it needs to detect file creation, not just changes.


61-77: Good error deduplication pattern.

Using a ref to track the last error prevents duplicate toasts on re-renders while still showing new errors. The cleanup when error clears is also handled correctly.


186-198: Clean conditional rendering between static and dynamic modes.

The ternary clearly separates the two rendering paths. Static ports render as a flat list while dynamic ports maintain workspace grouping. This aligns with the design decision that static ports are workspace-scoped.

apps/desktop/src/main/lib/static-ports/loader.ts (2)

15-61: Thorough port entry validation.

The validation function handles all edge cases well:

  • Type checks before accessing properties
  • Explicit field presence checks
  • Integer validation for port numbers
  • Valid port range (1-65535)
  • Non-empty string validation for labels
  • Label trimming for cleaner output

The indexed error messages (ports[${index}]) are helpful for users to locate issues in their config.


69-142: Clean loader implementation with proper error handling.

The loader follows a clear pattern:

  1. Check file existence
  2. Read file with error handling
  3. Parse JSON with error handling
  4. Validate structure progressively

Each error path returns a descriptive message that will help users fix their configuration.

apps/desktop/src/main/lib/static-ports/watcher.ts (3)

38-56: Well-designed multi-level watch strategy.

The three-tier approach handles all scenarios:

  1. Watch ports.json directly if it exists
  2. Watch .superset dir for file creation
  3. Watch worktree root for .superset creation

The recursive call to watch() when directory structure changes (lines 73, 110) elegantly promotes the watcher to the most specific target.


76-95: Good use of mtime comparison to filter spurious events.

The fs.watch API is notoriously noisy on some platforms. Comparing mtimeMs values prevents false change events triggered by access time updates when reading the file. This is a robust approach.


129-143: Comprehensive cleanup in unwatch().

Properly closes the watcher, clears debounce timers, and removes stored mtime. This prevents resource leaks and stale state.

apps/desktop/src/lib/trpc/routers/ports/ports.ts (2)

47-68: Clean implementation of hasStaticConfig procedure.

The procedure correctly:

  • Validates input with zod
  • Handles missing workspace gracefully
  • Uses getWorkspacePath utility for path resolution
  • Returns a typed response object

111-147: Correct use of observable pattern for subscription.

Per coding guidelines for tRPC subscriptions with trpc-electron, the observable pattern from @trpc/server/observable is required. The implementation correctly:

  • Returns an observable that emits change events
  • Sets up the file watcher on subscription start
  • Properly cleans up listener and watcher on unsubscription
apps/desktop/plans/20260108-2251-static-ports-json.md (1)

1-404: Good planning documentation overall.

The plan effectively documents:

  • Feature scope and user impact
  • Design decisions with rationale
  • Implementation milestones
  • Validation criteria

This will be valuable for future maintainers understanding why certain choices were made.

Comment thread apps/desktop/plans/20260108-2251-static-ports-json.md Outdated
Comment thread apps/desktop/plans/20260108-2251-static-ports-json.md Outdated
steven-terrana and others added 2 commits January 8, 2026 18:43
Adds JSDoc documentation to the validatePortEntry helper function
to satisfy the docstring coverage threshold.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update toast import path from `sonner` to `@superset/ui/sonner`
- Mark Milestone 4 (Zustand store updates) as skipped with note
  referencing the Decision Log entry

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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

🧹 Nitpick comments (3)
apps/desktop/src/main/lib/static-ports/loader.ts (2)

76-149: Consider validating for duplicate port numbers.

The validation doesn't check if the same port number appears multiple times in the array. While not necessarily invalid, duplicate ports could indicate a configuration error and might confuse users.

💡 Optional enhancement to detect duplicate ports

After line 145, before pushing to validatedPorts:

 	for (let i = 0; i < parsed.ports.length; i++) {
 		const entry = parsed.ports[i] as PortEntry;
 		const result = validatePortEntry(entry, i);
 
 		if (!result.valid) {
 			return { exists: true, ports: null, error: result.error };
 		}
 
+		// Check for duplicate port numbers
+		if (validatedPorts.some((p) => p.port === result.port)) {
+			return {
+				exists: true,
+				ports: null,
+				error: `Duplicate port number ${result.port} at ports[${i}]`,
+			};
+		}
+
 		validatedPorts.push({ port: result.port, label: result.label });
 	}

157-164: Extract path construction to eliminate duplication.

The path construction logic (lines 158-162) is duplicated from loadStaticPorts (lines 77-81). Extract this to a helper function to follow the DRY principle.

♻️ Proposed refactor to extract path construction

Add a helper function at the top of the file after the type definitions:

+/**
+ * Get the path to the ports.json file for a worktree.
+ *
+ * @param worktreePath - Path to the workspace's worktree directory
+ * @returns Full path to .superset/ports.json
+ */
+function getPortsFilePath(worktreePath: string): string {
+	return join(worktreePath, PROJECT_SUPERSET_DIR_NAME, PORTS_FILE_NAME);
+}
+
 /**
  * Validate a single port entry from the ports.json configuration.

Then update both functions to use it:

 export function loadStaticPorts(worktreePath: string): StaticPortsResult {
-	const portsPath = join(
-		worktreePath,
-		PROJECT_SUPERSET_DIR_NAME,
-		PORTS_FILE_NAME,
-	);
+	const portsPath = getPortsFilePath(worktreePath);
 
 	if (!existsSync(portsPath)) {
 export function hasStaticPortsConfig(worktreePath: string): boolean {
-	const portsPath = join(
-		worktreePath,
-		PROJECT_SUPERSET_DIR_NAME,
-		PORTS_FILE_NAME,
-	);
+	const portsPath = getPortsFilePath(worktreePath);
 	return existsSync(portsPath);
 }
apps/desktop/plans/20260108-2251-static-ports-json.md (1)

242-250: Address markdown linting issue with code block style.

The static analysis tool flagged this code block. The project's markdown configuration expects indented code blocks, but this uses fenced style.

📝 Fix markdown code block style

Change from fenced to indented code block:

-   ```json
-   {
-     "ports": [
-       { "port": 3000, "label": "Frontend Dev Server" },
-       { "port": 8080, "label": "API Server" },
-       { "port": 5432, "label": "PostgreSQL" }
-     ]
-   }
-   ```
+       {
+         "ports": [
+           { "port": 3000, "label": "Frontend Dev Server" },
+           { "port": 8080, "label": "API Server" },
+           { "port": 5432, "label": "PostgreSQL" }
+         ]
+       }

Or update the markdownlint configuration to allow fenced code blocks if that's the preferred style.

Based on static analysis hints.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1724d4f and 0fbf141.

📒 Files selected for processing (2)
  • apps/desktop/plans/20260108-2251-static-ports-json.md
  • apps/desktop/src/main/lib/static-ports/loader.ts
🧰 Additional context used
📓 Path-based instructions (4)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/main/lib/static-ports/loader.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • apps/desktop/src/main/lib/static-ports/loader.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/main/lib/static-ports/loader.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • apps/desktop/src/main/lib/static-ports/loader.ts
🧬 Code graph analysis (1)
apps/desktop/src/main/lib/static-ports/loader.ts (2)
apps/desktop/src/shared/types/ports.ts (1)
  • StaticPortsResult (17-21)
apps/desktop/src/shared/constants.ts (2)
  • PROJECT_SUPERSET_DIR_NAME (33-33)
  • PORTS_FILE_NAME (36-36)
🪛 markdownlint-cli2 (0.18.1)
apps/desktop/plans/20260108-2251-static-ports-json.md

242-242: Code block style
Expected: indented; Actual: fenced

(MD046, code-block-style)

🔇 Additional comments (2)
apps/desktop/src/main/lib/static-ports/loader.ts (2)

1-13: LGTM: Clean imports and type-safe validation approach.

The use of unknown types for pre-validation interfaces (PortEntry, PortsConfig) is a type-safe pattern that allows proper narrowing during validation, avoiding the pitfalls of any.


15-68: LGTM: Comprehensive validation logic.

The validation correctly enforces the port schema: integer ports within the valid TCP range (1-65535), non-empty string labels, and provides clear error messages with array indices for debugging. Trimming the label is a nice touch.

@Kitenite
Copy link
Copy Markdown
Collaborator

Kitenite commented Jan 9, 2026

I have a few improvements just checking w/ your requirements before i merge @steven-terrana.

  1. When a static ports are defined, should we still show dynamic ports? My thinking is yes and that we should merge static and dynamic when possible
  2. Right now we are fetching static port for the current workspace, my thinking is we should fetch the ports for all workspaces.

What are you thoughts?

Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite left a comment

Choose a reason for hiding this comment

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

Approve with some follow up comments above @steven-terrana. Overall nice works, it works very well :)

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

🤖 Fix all issues with AI agents
In
@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx:
- Around line 83-93: When rendering the active-port block in MergedPortBadge,
guard against null/undefined values for port.pid and port.processName so you
don't render "null (pid null)"; update the JSX that checks port.isActive to
substitute safe fallbacks (e.g., empty string, "unknown" or "—") for
port.processName and port.pid and only render the pid segment if pid is present,
and keep the canJumpToTerminal text unchanged; locate the conditional rendering
inside the MergedPortBadge component where port.isActive is used and replace
direct references to port.processName and port.pid with these fallback-aware
expressions.
🧹 Nitpick comments (2)
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx (2)

178-218: Extract MergedWorkspacePortGroup to its own file.

Per coding guidelines, there should be one component per file. MergedWorkspacePortGroup should be moved to its own file under components/.

Suggested structure

Create a new file at components/MergedWorkspacePortGroup/MergedWorkspacePortGroup.tsx:

import { trpc } from "renderer/lib/trpc";
import type { MergedPort } from "shared/types";
import { MergedPortBadge } from "../MergedPortBadge";

export interface MergedWorkspaceGroup {
  workspaceId: string;
  workspaceName: string;
  isCurrentWorkspace: boolean;
  ports: MergedPort[];
}

interface MergedWorkspacePortGroupProps {
  group: MergedWorkspaceGroup;
}

export function MergedWorkspacePortGroup({ group }: MergedWorkspacePortGroupProps) {
  // ... current implementation
}

Then export via components/MergedWorkspacePortGroup/index.ts and import in PortsList.tsx.


71-87: Error deduplication ref accumulates indefinitely.

The shownErrorsRef Set is never cleared, so if a user fixes their ports.json and then breaks it again with the same error, the toast won't appear. Consider clearing the Set when errors are resolved, or using a time-based expiry.

Possible approach
+	// Clear shown errors when the error list changes to allow re-showing
+	const previousErrorKeys = useRef<Set<string>>(new Set());
+
 	useEffect(() => {
 		const errors = allStaticPortsData?.errors ?? [];
+		const currentErrorKeys = new Set(
+			errors.map(({ workspaceId, error }) => `${workspaceId}:${error}`)
+		);
+		
+		// Clear keys for errors that are no longer present
+		for (const key of shownErrorsRef.current) {
+			if (!currentErrorKeys.has(key)) {
+				shownErrorsRef.current.delete(key);
+			}
+		}
+
 		for (const { workspaceId, error } of errors) {
 			const errorKey = `${workspaceId}:${error}`;
 			if (!shownErrorsRef.current.has(errorKey)) {
 				shownErrorsRef.current.add(errorKey);
 				const workspaceName =
 					workspaceNames[workspaceId] || "Unknown workspace";
 				toast.error(`Failed to load ports.json in ${workspaceName}`, {
 					description: error,
 				});
 			}
 		}
 	}, [allStaticPortsData?.errors, workspaceNames]);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fbf141 and 1b37470.

📒 Files selected for processing (7)
  • apps/desktop/src/lib/trpc/routers/ports/ports.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/merge-ports.ts
  • apps/desktop/src/shared/types/ports.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/shared/types/ports.ts
🧰 Additional context used
📓 Path-based instructions (6)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/merge-ports.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx
  • apps/desktop/src/lib/trpc/routers/ports/ports.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/merge-ports.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx
  • apps/desktop/src/lib/trpc/routers/ports/ports.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
apps/desktop/src/renderer/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never import Node.js modules (fs, path, os, net) in renderer process or shared code - they are externalized for browser compatibility

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/merge-ports.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/merge-ports.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx
  • apps/desktop/src/lib/trpc/routers/ports/ports.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/merge-ports.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx
  • apps/desktop/src/lib/trpc/routers/ports/ports.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

One component per file - do not create multi-component files

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
🧠 Learnings (3)
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/desktop/src/renderer/**/*.{ts,tsx} : Never import Node.js modules (fs, path, os, net) in renderer process or shared code - they are externalized for browser compatibility

Applied to files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/index.ts
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/desktop/src/lib/*.ts : Never import Node.js modules in shared code like electron-router-dom.ts - it runs in both main and renderer processes

Applied to files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/index.ts
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/desktop/src/lib/electron-router-dom.ts : Do not import Node.js modules like node:path or dotenv in electron-router-dom.ts and similar shared files - they run in both main and renderer processes

Applied to files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/index.ts
🧬 Code graph analysis (2)
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/merge-ports.ts (2)
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/index.ts (1)
  • mergePorts (1-1)
apps/desktop/src/shared/types/ports.ts (3)
  • StaticPort (11-15)
  • DetectedPort (1-9)
  • MergedPort (23-33)
apps/desktop/src/lib/trpc/routers/ports/ports.ts (7)
apps/desktop/src/main/lib/local-db/index.ts (1)
  • localDb (82-82)
packages/local-db/src/schema/schema.ts (1)
  • workspaces (80-118)
apps/desktop/src/lib/trpc/routers/workspaces/utils/worktree.ts (1)
  • getWorkspacePath (22-43)
apps/desktop/src/main/lib/static-ports/index.ts (3)
  • hasStaticPortsConfig (1-1)
  • loadStaticPorts (1-1)
  • staticPortsWatcher (2-2)
apps/desktop/src/main/lib/static-ports/loader.ts (2)
  • hasStaticPortsConfig (157-164)
  • loadStaticPorts (76-149)
apps/desktop/src/shared/types/ports.ts (1)
  • StaticPort (11-15)
apps/desktop/src/main/lib/static-ports/watcher.ts (1)
  • staticPortsWatcher (155-155)
🔇 Additional comments (8)
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/index.ts (1)

1-1: LGTM!

Clean barrel export for the utils module public API.

apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/index.ts (1)

1-1: LGTM!

Standard component barrel export.

apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/utils/merge-ports.ts (1)

12-61: LGTM!

Well-structured merge logic with clear documentation. The function correctly:

  • Builds efficient lookup structures (Map/Set) for O(1) access
  • Merges static port definitions with dynamic runtime data
  • Preserves static ports even when inactive
  • Adds dynamic-only ports that aren't in static config
  • Sorts the final result by port number

The caller is responsible for ensuring staticPorts are pre-filtered for the target workspace, which is handled correctly in PortsList.tsx (line 107-109).

apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx (1)

13-41: LGTM!

Clean component implementation with proper:

  • Async handling with appropriate guards
  • Store access patterns using zustand
  • tRPC mutation usage for workspace switching
  • Focus and accessibility considerations
apps/desktop/src/lib/trpc/routers/ports/ports.ts (2)

141-175: LGTM - well-structured subscription with proper cleanup.

The observable pattern is correctly used per coding guidelines. The cleanup function properly unsubscribes from the watcher and removes the listener.

One minor note: the early returns on lines 151-153 and 156-158 silently do nothing for invalid workspaces. Consider whether a debug log would help with troubleshooting subscription issues.


106-139: LGTM - good error aggregation pattern.

The bulk query correctly aggregates ports from all workspaces while collecting per-workspace errors separately. This allows the UI to display available ports even when some workspaces have invalid configurations.

apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx (2)

32-40: Static port subscription is scoped to active workspace only.

The subscribeStatic subscription only watches the active workspace's ports.json. Changes to other workspaces' configs won't trigger a refresh until the user switches to them. This aligns with the current implementation but may be relevant to the PR discussion about expanding fetch scope to all workspaces.

If all-workspace live updates are desired in the future, consider subscribing to each workspace or implementing a global watcher that emits changes for any workspace.


103-143: LGTM - efficient merged port computation.

Good use of useMemo with correct dependency array. The merge logic properly:

  • Filters static ports per workspace before merging
  • Sorts groups with current workspace first
  • Computes total count from merged results

@steven-terrana
Copy link
Copy Markdown
Contributor Author

I have a few improvements just checking w/ your requirements before i merge @steven-terrana.

  1. When a static ports are defined, should we still show dynamic ports? My thinking is yes and that we should merge static and dynamic when possible
  2. Right now we are fetching static port for the current workspace, my thinking is we should fetch the ports for all workspaces.

What are you thoughts?

My use case largely centers around a bun run up that runs docker compose up -d and then generates the ports.json locally (which is git ignored) because the ports are dynamically allocated and not caught because the process detaches.

so for me, the ports.json is different in every workspace

@Kitenite
Copy link
Copy Markdown
Collaborator

Kitenite commented Jan 9, 2026

so for me, the ports.json is different in every workspace

That would still be compatible, but it would just show for every existing ports.json. Perhaps it'll be a little verbose

image

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

Caution

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

⚠️ Outside diff range comments (2)
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx (2)

206-241: Extract MergedWorkspacePortGroup to its own file.

This file contains two components (PortsList and MergedWorkspacePortGroup), which violates the coding guideline requiring one component per file.

As per coding guidelines:

One component per file - do not create multi-component files

📁 Recommended file structure

Create a new file:

apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedWorkspacePortGroup/MergedWorkspacePortGroup.tsx

Move lines 202-241 to the new file, and add the corresponding barrel export:

apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedWorkspacePortGroup/index.ts

Then import it in PortsList.tsx:

import { MergedWorkspacePortGroup } from "./components/MergedWorkspacePortGroup";

210-215: Add error handling for workspace switching.

Similar to the issue in MergedPortBadge, this async mutation lacks error handling. If the workspace switch fails, the user won't receive feedback.

🛡️ Proposed fix with error handling
 const handleWorkspaceClick = async () => {
 	if (group.isCurrentWorkspace) return;
 
+	try {
 		await setActiveMutation.mutateAsync({ id: group.workspaceId });
 		await utils.workspaces.getActive.invalidate();
+	} catch (error) {
+		console.error('[PortsList/handleWorkspaceClick] Failed to switch workspace:', error);
+		toast.error('Failed to switch workspace', {
+			description: 'Could not activate the selected workspace'
+		});
+	}
 };

Note: toast is already imported at line 2.

🤖 Fix all issues with AI agents
In
@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx:
- Around line 39-52: Wrap the workspace switch logic in handleClick with a
try/catch around setActiveMutation.mutateAsync and the subsequent invalidate: if
mutateAsync throws, catch the error, show a user-facing toast (import toast from
@superset/ui/sonner) with a helpful message and return early. After a successful
mutation+invalidate, re-read panes from useTabsStore.getState() (do not reuse a
stale reference) before resolving pane with port.paneId; only call setActiveTab
and setFocusedPane when pane is present. Ensure errors in any of these steps are
caught and reported via toast.

In
@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx:
- Around line 75-91: The deduplication set shownErrorsRef never gets cleared so
recurring errors are suppressed; update the useEffect that processes
allStaticPortsData?.errors to reconcile shownErrorsRef with the current errors:
build a Set of current error keys (using `${workspaceId}:${error}`), remove any
keys from shownErrorsRef.current that are no longer present (or call
shownErrorsRef.current.clear() when there are no errors), then add and toast
only new keys as currently done; keep this logic inside the same useEffect that
references allStaticPortsData?.errors and workspaceNames so shownErrorsRef is
kept in sync with resolved/recurring errors.
🧹 Nitpick comments (1)
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx (1)

39-52: Consider early returns to reduce nesting.

The function has multiple conditional checks that could be flattened using early returns for improved readability.

♻️ Alternative structure with early returns
 const handleClick = async () => {
 	if (!canJumpToTerminal || !port.paneId) return;
 
 	try {
 		if (!isCurrentWorkspace) {
 			await setActiveMutation.mutateAsync({ id: port.workspaceId });
 			await utils.workspaces.getActive.invalidate();
 		}
 
 		const pane = useTabsStore.getState().panes[port.paneId];
-		if (!pane) return;
-
-		setActiveTab(port.workspaceId, pane.tabId);
-		setFocusedPane(pane.tabId, port.paneId);
+		if (!pane) {
+			console.warn('[MergedPortBadge/handleClick] Pane not found:', port.paneId);
+			return;
+		}
+
+		setActiveTab(port.workspaceId, pane.tabId);
+		setFocusedPane(pane.tabId, port.paneId);
 	} catch (error) {
 		console.error('[MergedPortBadge/handleClick] Failed to switch workspace:', error);
 		toast.error('Failed to open workspace', {
 			description: 'Could not switch to the target workspace'
 		});
 	}
 };
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1b37470 and a452140.

📒 Files selected for processing (2)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx
🧰 Additional context used
📓 Path-based instructions (6)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
apps/desktop/src/renderer/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never import Node.js modules (fs, path, os, net) in renderer process or shared code - they are externalized for browser compatibility

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

One component per file - do not create multi-component files

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx
🧬 Code graph analysis (1)
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx (3)
apps/desktop/src/shared/types/ports.ts (1)
  • MergedPort (23-33)
packages/ui/src/components/ui/tooltip.tsx (3)
  • Tooltip (76-76)
  • TooltipTrigger (76-76)
  • TooltipContent (76-76)
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/constants.ts (1)
  • STROKE_WIDTH (2-2)
🔇 Additional comments (4)
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/components/MergedPortBadge/MergedPortBadge.tsx (1)

1-112: Overall structure looks good.

The component properly:

  • Uses React 19 patterns (no forwardRef needed for ref prop)
  • Leverages Tooltip from @superset/ui correctly
  • Implements conditional styling and accessibility attributes
  • Uses null-safe checks (port.pid != null at line 96)
apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/PortsList/PortsList.tsx (3)

93-142: Excellent use of memoization for complex derived state.

The memoization strategy for allWorkspaceIds and workspacePortGroups is well-designed:

  • Dependencies are correctly specified
  • Expensive operations (filtering, merging, sorting) are cached
  • The merge logic properly combines static and dynamic ports per workspace

The sorting logic (current workspace first, then alphabetical) provides a good UX.


36-44: Good use of tRPC subscription for live reload.

The static ports subscription with query invalidation provides a clean reactive pattern for file system changes without manual polling.


153-189: Well-implemented help tooltip with docs link.

The help icon with tooltip provides good discoverability for the static port configuration feature. The reveal-on-hover pattern (opacity-0 group-hover:opacity-100) is a nice touch.

@Kitenite Kitenite merged commit 2654b6a into superset-sh:main Jan 9, 2026
11 of 13 checks passed
@Kitenite
Copy link
Copy Markdown
Collaborator

Kitenite commented Jan 9, 2026

@steven-terrana Merged with some of the improvements I talked about. Let me know if there's any of the changes you don't like when its published. Thanks for the PR!

saddlepaddle pushed a commit that referenced this pull request Jan 10, 2026
feat(desktop): add static port configuration via .superset/ports.json
Allow users to define static ports in a .superset/ports.json file within their workspace, replacing dynamic port discovery when present.

Add static-ports module with loader and file watcher
Add tRPC procedures: hasStaticConfig, getStatic, subscribeStatic
Auto-reload config on file changes with error toast notifications
Add marketing documentation page at /ports
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.

[feat] Port Labeling via User-Defined Scripts

2 participants