Track terminal listening ports in activity events and thread state#43

closed16
main
codething/50e6e9da
openedFeb 14, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 4d ago

Summary

  • extend terminal activity payloads to include normalized runningPorts alongside hasRunningSubprocess
  • replace simple subprocess checks with subprocess inspection that detects process families and listening ports on both POSIX (lsof/ss) and Windows (PowerShell CIM/NetTCP)
  • persist per-terminal running port state in web thread store (runningTerminalPorts) and clear it on terminal restart/exit/error/close
  • surface detected ports in the sidebar terminal status indicator, including compact port labels
  • update shared contracts and tests across server, web, and contracts packages for port-aware activity behavior

Testing

  • apps/server/src/terminalManager.test.ts: verifies activity events emit on subprocess state and port changes, including port normalization and clearing when inactive
  • apps/web/src/store.test.ts: verifies reducer tracks runningTerminalPorts, updates on activity changes, and removes port state when terminals stop/exist
  • packages/contracts/src/terminal.test.ts: verifies terminalActivityEventSchema accepts runningPorts
  • Additional fixture/schema updates covered in:
    • apps/web/src/persistenceSchema.test.ts
    • apps/web/src/worktreeCleanup.test.ts
  • Lint: Not run
  • Full test suite: Not run

Open with Devin

Summary by CodeRabbit

  • New Features

    • Sidebar shows detected listening ports with a clickable web icon to open the primary port; threads track per-terminal running ports.
  • Public Events

    • Terminal activity events now include a runningPorts array alongside running-state info.
  • Behavior

    • Web-port detection ignores root 404 responses, caches results, and deduplicates in-flight probes.
  • Persistence

    • Thread state now persists per-terminal running ports.
  • Tests

    • Tests added/updated for port detection, 404 handling, merging/clearing ports, and lifecycle propagation.

Note

Medium Risk Adds periodic OS-level process/port inspection (ps/lsof/ss/PowerShell) and HTTP probing to terminal polling, which can impact runtime behavior and introduces platform-specific edge cases despite bounded timeouts and error throttling.

Overview Terminal activity events now carry normalized runningPorts alongside hasRunningSubprocess, with contracts updated to validate port ranges.

On the server, a new TerminalProcessInspectorLive replaces the old boolean subprocess check by collecting process families and scanning listening ports on POSIX (ps + lsof, fallback to ss) and Windows (PowerShell CIM + Get-NetTCPConnection); a new WebPortInspectorLive probes localhost over IPv4/IPv6 to decide which ports are likely web servers (404 ignored), with TTL caching and transient-failure tolerance. TerminalManager emits activity events when either the subprocess flag or the detected web-port set changes, and throttles repeated inspector error logs.

On the web side, thread terminal state now persists runningTerminalPorts per terminal, clears it on start/restart/exit/error/close, and the sidebar + terminal drawer show a clickable web indicator that opens http://localhost:<port> when a primary port is detected.

Reviewed by Cursor Bugbot for commit b0bc101eb6d4cf43c9211e77c5b21ba44eec9044. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Track listening ports in terminal activity events and display detected web ports in the UI

  • Adds cross-platform subprocess inspection (TerminalProcessInspectorLive) that discovers child PIDs and listening TCP ports via lsof/ss on POSIX and PowerShell on Windows, and a WebPortInspectorLive that probes localhost to confirm whether a port serves HTTP/HTML.
  • Extends TerminalActivityEvent in terminal.ts with a required runningPorts field; activity events are only emitted when hasRunningSubprocess or the port list changes. Web port detection uses a TTL cache so transient probe failures don't drop previously confirmed ports.
  • Updates ThreadTerminalState in terminalStateStore.ts to store a runningTerminalPorts map per terminal, normalized and deduplicated on each update.
  • Renders detected web ports in the UI: the sidebar thread row shows a clickable terminal status icon that opens http://localhost:<port>, and the terminal drawer tab shows a Globe icon button for the same action.
  • Repeated identical inspector errors are throttled to one log per 30 seconds per session.
  • Risk: runningPorts is now a required field in TerminalActivityEvent; any existing persisted or in-flight events without this field will fail schema decoding.

Macroscope summarized b0bc101.

Synced from GitHub

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a cross-platform subprocess inspector and web-port probing to TerminalManager, surfaces per-terminal running port lists in activity events, and propagates those fields through contracts, server modules, web state, persistence, UI, and tests.

Changes

Cohort / File(s)Summary
Server: TerminalManager core
apps/server/src/terminalManager.ts, apps/server/src/terminalManager.test.ts
Replaces boolean subprocessChecker with subprocessInspector returning { hasRunningSubprocess, runningPorts }. Emits activity events with runningPorts, integrates webPortInspector with TTL caching/in-flight dedupe, updates session state and tests (including ignoring HTTP 404 probes).
Server: subprocess inspection module
apps/server/src/terminalProcessInspector/index.ts, apps/server/src/terminalProcessInspector/types.ts, apps/server/src/terminalProcessInspector/utils.ts, apps/server/src/terminalProcessInspector/win32.ts, apps/server/src/terminalProcessInspector/posix.ts
New module implementing default inspectors: collect process families/children, detect listening ports (lsof/ss on POSIX, PowerShell on Win), normalize ports, compare lists, and provide a shim from old checker-to-inspector. Exports types/utilities.
Server: web-port probing
apps/server/src/webPortInspector.ts
New HTTP probe to heuristically detect web servers on localhost (probes 127.0.0.1 and ::1), with timeouts, max body limits, detection heuristics, and exported DEFAULT_WEB_PORT_PROBE_TTL_MS.
Web: state, normalization, reducers, tests
apps/web/src/types.ts, apps/web/src/store.ts, apps/web/src/store.test.ts, apps/web/src/worktreeCleanup.test.ts
Adds runningTerminalPorts: Record<string, number[]> to Thread and runningPorts: number[] to terminal activity events. Adds normalization utilities, integrates per-terminal port set/clear logic in reducer, and updates tests to validate port lifecycle.
Web: persistence & initialization
apps/web/src/persistenceSchema.ts, apps/web/src/persistenceSchema.test.ts, apps/web/src/App.tsx
Hydrates/initializes runningTerminalPorts as {} for persisted/new threads; test fixtures and initial thread payload updated.
Web: UI — Sidebar
apps/web/src/components/Sidebar.tsx
Aggregates per-terminal ports into deduplicated/sorted list, exposes primaryWebPort, updates TerminalStatusIndicator.label to string, renders clickable GlobeIcon for opening primary web port, and adds keyboard handlers.
Contracts: terminal event schema
packages/contracts/src/terminal.ts, packages/contracts/src/terminal.test.ts
Adds runningPorts: number[] to the terminalActivityEventSchema (ints 1–65535, default []); updates tests to include the new field.

Sequence Diagram

mermaid
1sequenceDiagram
2 participant TM as Server:TerminalManager
3 participant INS as Inspector (process + web)
4 participant CONTRACT as Contracts/Validator
5 participant STORE as Web:Store
6 participant UI as Sidebar
7
8 TM->>INS: inspectSubprocess(terminalPid)
9 INS->>INS: collect PIDs (children/family) and check listening ports
10 INS->>INS: probe HTTP on detected ports (webPortInspector)
11 INS-->>TM: { hasRunningSubprocess, runningPorts[] }
12 TM->>CONTRACT: emit TerminalActivityEvent (includes runningPorts)
13 CONTRACT-->>STORE: validated event
14 STORE->>STORE: normalizeRunningPorts(runningPorts) and update runningTerminalPorts[terminalId]
15 STORE-->>UI: state change (thread.runningTerminalPorts)
16 UI->>UI: compute portsLabel / primaryWebPort and render clickable UI

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 2.94% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the main change: tracking terminal listening ports in activity events and thread state. This matches the PR's primary objective of extending activity payloads with running ports and persisting port state.
Merge Conflict Detection✅ Passed✅ No merge conflicts detected when merging into main
Description check✅ PassedThe PR description comprehensively documents all major changes: cross-platform subprocess/port inspection, activity payload extensions, web-side state persistence, and UI updates. It clearly explains the problem (tracking listening ports) and the solution approach (platform-specific inspectors with caching). However, the description lacks before/after UI screenshots or videos as specified in the template.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codething/50e6e9da

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

Synced from GitHub

Track terminal web listening ports in activity events and surface a primary port in Sidebar and ThreadTerminalDrawer using TTL-cached probes (DEFAULT_WEB_PORT_PROBE_TTL_MS)

Add runningPorts to terminal activity, implement process/port inspection on POSIX and Windows, cache web port probes, and update web UI to display and open detected ports. Server wiring moves to terminalProcessInspector and webPortInspector, and store/schema track per-terminal ports.

📍Where to Start

Start with TerminalManager.pollSubprocessActivity and new helpers in terminalManager.ts, then review defaultSubprocessInspector in terminalProcessInspector/index.ts and defaultWebPortInspector in webPortInspector.ts.


Macroscope summarized d377e56.

Synced from GitHub

Greptile Summary

Extends terminal activity tracking to detect and expose web server ports running in terminal subprocesses. The implementation polls subprocess state every second, uses platform-specific tools (lsof/ss on POSIX, PowerShell on Windows) to detect listening ports, probes each port via HTTP to filter web servers from other listening services, and surfaces detected ports in the UI with clickable localhost shortcuts.

Major changes:

  • Created new terminalProcessInspector module with platform-specific implementations for detecting process trees and listening ports
  • Added webPortInspector to distinguish web servers from other TCP listeners by probing HTTP root (filters out 404 responses)
  • Extended TerminalManager to poll subprocess activity, cache web port probe results (10s TTL), and emit activity events with runningPorts
  • Updated web store to track runningTerminalPorts per terminal and clear state on terminal lifecycle events
  • Enhanced sidebar and terminal drawer UI to show port badges with web icons and one-click localhost shortcuts
  • Added comprehensive test coverage for activity events, port normalization, 404 filtering, and state management

Confidence Score: 4/5

  • This PR is generally safe to merge with one known platform parity issue on Windows
  • The implementation is well-tested and handles edge cases like 404 filtering and port normalization correctly. The core logic for POSIX systems recursively collects the full process tree, ensuring grandchild processes are detected. However, the Windows implementation only collects direct children (not the full descendant tree), which creates a platform behavior discrepancy already noted in previous review threads. The web port detection, caching, and UI integration are solid.
  • Pay close attention to apps/server/src/terminalProcessInspector/win32.ts - the Windows implementation only collects direct child processes, not the full process tree like the POSIX version

Important Files Changed

FilenameOverview
apps/server/src/terminalProcessInspector/posix.tsPOSIX-specific implementation using ps/lsof/ss to detect process trees and listening ports
apps/server/src/terminalProcessInspector/win32.tsWindows-specific implementation using PowerShell CIM/NetTCP for child process and port detection; only collects direct children, not full process tree
apps/server/src/webPortInspector.tsHTTP probe implementation to distinguish web servers from other listening ports; correctly filters out 404 responses
apps/server/src/terminalManager.tsExtended to track subprocess ports and emit activity events; includes web port caching and detection logic
apps/web/src/store.tsUpdated reducer to handle runningTerminalPorts on activity events and clear on terminal lifecycle events
apps/web/src/components/Sidebar.tsxAdded terminalStatusIndicator to display detected web ports with clickable shortcuts

Sequence Diagram

mermaid
1sequenceDiagram
2 participant TM as TerminalManager
3 participant PI as ProcessInspector
4 participant POSIX as POSIX/Win32
5 participant WP as WebPortInspector
6 participant Store as Web Store
7 participant UI as Sidebar UI
8
9 Note over TM: Terminal starts running
10 TM->>TM: Start subprocess polling (1s interval)
11
12 loop Every 1 second
13 TM->>PI: inspectSubprocess(terminalPid)
14 PI->>POSIX: collectProcessFamilyPids(pid)
15 POSIX-->>PI: [childPids]
16 PI->>POSIX: checkListeningPorts(childPids)
17 POSIX-->>PI: [rawPorts]
18 PI-->>TM: {hasRunningSubprocess, runningPorts}
19
20 TM->>TM: normalizeRunningPorts(ports)
21
22 loop For each detected port
23 TM->>WP: inspectWebPort(port)
24 Note over WP: Check cache (10s TTL)
25 WP->>WP: HTTP GET localhost:port/
26 WP-->>TM: isWeb (true if not 404)
27 end
28
29 TM->>TM: Filter to web ports only
30 TM->>Store: emit activity event{runningPorts}
31 Store->>Store: Update runningTerminalPorts map
32 Store->>UI: State change
33 UI->>UI: Show port badges with web icon
34 end
35
36 Note over TM: Terminal exits
37 TM->>TM: Clear runningSubprocessPorts
38 TM->>Store: emit exited event
39 Store->>Store: Remove from runningTerminalPorts

Last reviewed commit: c11027b

apps/server/src/terminalManager.tslines 193–197Outdated · 7926636Open thread
……@@ -129,11 +139,62 @@ function isRetryableShellSpawnError(error: unknown): boolean {
135- "if ($children) { exit 0 }",
136- "exit 1",
196+ "if (-not $children) { exit 0 }",
197+ "$children | Select-Object -ExpandProperty ProcessId",
Ggreptile-appsBoton lines 193–197Feb 14, 2026

Windows only collects direct children, not the full process tree

collectWindowsChildPids queries Win32_Process with ParentProcessId = ${terminalPid}, which only returns direct child processes. In contrast, collectPosixProcessFamilyPids recursively walks the entire process tree (BFS traversal building processFamily). This means on Windows, if a terminal runs npm start → node server.js → listens on port 3000, the port won't be detected because node is a grandchild.

Consider recursively collecting descendant PIDs on Windows as well — for example by iterating the initial children and querying their children in a loop, similar to the POSIX BFS approach.

apps/server/src/terminalManager.tslines 313–323Outdated · 7926636Open thread
……@@ -178,32 +256,134 @@ async function checkPosixSubprocessActivity(terminalPid: number): Promise<boolea
320+ }
321+ }
322+ return [...ports].toSorted((left, right) => left - right);
323+ }
Ggreptile-appsBoton lines 313–323Feb 14, 2026

lsof exit code 1 unnecessarily falls through to ss

lsof returns exit code 1 when no matching files/connections are found (i.e., none of the specified PIDs have listening sockets). This is a normal "no results" outcome, not a failure. With the current logic, result.code === 0 will be false, causing an unnecessary fallback to ss which will also find nothing. The ss fallback should only be needed when lsof is unavailable (the catch path).

Consider also returning early when result.code === 1:

suggestion
1 if (result.code === 0 || result.code === 1) {
2 for (const line of result.stdout.split(/\r?\n/g)) {
3 const match = line.match(/:(\d+)\s+\(LISTEN\)$/);
4 if (!match?.[1]) continue;
5 const port = Number(match[1]);
6 if (Number.isInteger(port) && port > 0 && port <= MAX_PORT_NUMBER) {
7 ports.add(port);
8 }
9 }
10 return [...ports].toSorted((left, right) => left - right);
11 }
apps/server/src/terminalManager.tsline 1000Outdated · 7926636Open thread
……@@ -807,9 +1000,12 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {
8071000 await Promise.all(
MmacroscopeappBoton line 1000Feb 14, 2026

src/terminalManager.ts:1000

Consider throttling or batching the subprocess activity checks. Running subprocessInspector concurrently for all sessions via Promise.all spawns heavy external processes (e.g., powershell.exe, ps) in parallel every polling interval, which could cause resource pressure with many open terminals.

No longer relevant as of 05a7018f0302121cfc15ce9bf2db72498f5e09d4

apps/server/src/terminalManager.tsline 371Outdated · 7926636Open thread
……@@ -178,32 +256,134 @@ async function checkPosixSubprocessActivity(terminalPid: number): Promise<boolea
369+
203370 if (process.platform === "win32") {
204- return checkWindowsSubprocessActivity(terminalPid);
371+ const childPids = await collectWindowsChildPids(terminalPid);
MmacroscopeappBoton line 371Feb 14, 2026

src/terminalManager.ts:371 On Windows, collectWindowsChildPids only retrieves immediate children, unlike the POSIX path which recursively collects the entire process family. Consider implementing recursive traversal (similar to collectPosixProcessFamilyPids) to detect ports opened by grandchild processes.

Code refactored as of 259251c84327b88fcc0bc31bb6b6be9212f2799b

CcoderabbitaiBotreviewed · 1 inline threadFeb 14, 2026
Synced from GitHub

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
apps/server/src/terminalManager.ts (1)

193-248: ⚠️ Potential issue | 🟠 Major

Windows process collection only detects direct children—nested listeners spawned by npm/pnpm will be missed.

The Windows implementation queries only direct children via Get-CimInstance Win32_Process -Filter "ParentProcessId = ${terminalPid}", while the POSIX equivalent recursively builds the full descendant tree (lines 276–288). When a dev server runs as a grandchild of the shell (e.g., npm → node → dev-server), the Windows path fails to detect its listening port. Consider implementing recursive process tree collection for Windows, similar to the POSIX approach.

🤖 Fix all issues with AI agents
1In `@apps/web/src/store.ts`:
2- Around line 706-734: The reducer mutates and reads action.event.runningPorts
3without ensuring events were validated by the terminalEventSchema;
4validate/parses incoming socket events through the Zod terminalEventSchema
5before dispatch so defaults (like runningPorts defaulting to []) and types are
6enforced. Update the WebSocket/event handler (e.g., in App.tsx or
7wsNativeApi.ts) to call terminalEventSchema.parse or safeParse on the raw
8message and only dispatch the parsed value to the reducer (which includes
9normalizeThreadTerminals/runningTerminalPorts logic), and handle parse failures
10(log/ignore) so the reducer never receives unvalidated events.
🧹 Nitpick comments (1)
apps/server/src/terminalManager.ts (1)

142-191: Use sort() instead of toSorted() for runtime compatibility.

The TypeScript config targets ES2023, but the project has no explicit Node.js version requirement in package.json or deployment config. toSorted() requires Node 20+; without a guaranteed minimum version, this will cause runtime failures on older LTS releases. Replace with sort() for broader compatibility.

Applies to lines 146, 322, and 357.

apps/web/src/store.tslines 706–734Outdated · 7926636Open thread
……@@ -674,24 +705,32 @@ export function reducer(state: AppState, action: Action): AppState {
693731 ...normalizedThread,
694732 runningTerminalIds: [...runningTerminalIdSet],
733+ runningTerminalPorts,
695734 });
CcoderabbitaiBoton lines 706–734Feb 14, 2026

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

shell
1# Search for terminal-related schema files in the contracts package
2fd -e ts -e tsx "terminal" packages/contracts/src/

Repository: pingdotgg/codething-mvp

Length of output: 141


🏁 Script executed:

shell
1# Search for the activity event schema and runningPorts definition
2rg "activity|runningPorts" packages/contracts/src/ -A 5 -B 2

Repository: pingdotgg/codething-mvp

Length of output: 1679


🏁 Script executed:

shell
1# Search for where terminal events are handled/validated in the store
2rg "terminalEventSchema|action\.event" apps/web/src/store.ts -B 5 -A 3

Repository: pingdotgg/codething-mvp

Length of output: 1667


🏁 Script executed:

shell
1# Check the full context of the event handling to understand the validation flow
2head -n 750 apps/web/src/store.ts | tail -n 100

Repository: pingdotgg/codething-mvp

Length of output: 3850


🏁 Script executed:

shell
1# Search for where APPLY_TERMINAL_EVENT is dispatched and how events are validated
2rg "APPLY_TERMINAL_EVENT|dispatch.*terminal" apps/web/src/ -B 3 -A 3

Repository: pingdotgg/codething-mvp

Length of output: 3673


🏁 Script executed:

shell
1# Look for event validation or parsing logic
2rg "terminalEventSchema\.parse|safeParse.*terminal" apps/web/src/ -B 2 -A 2

Repository: pingdotgg/codething-mvp

Length of output: 49


🏁 Script executed:

shell
1# Check the TerminalEvent type definition and where it comes from
2rg "type TerminalEvent|TerminalEvent =" apps/web/src/store.ts -B 2 -A 5

Repository: pingdotgg/codething-mvp

Length of output: 49


🏁 Script executed:

shell
1# Look for imports and type definition source
2head -n 100 apps/web/src/store.ts | grep -E "import|from"

Repository: pingdotgg/codething-mvp

Length of output: 427


🏁 Script executed:

shell
1# Check api.terminal.onEvent to see if it validates events
2rg "api\.terminal|onEvent" apps/web/src/ -B 3 -A 5

Repository: pingdotgg/codething-mvp

Length of output: 8040


🏁 Script executed:

shell
1# Check the WebSocket transport implementation for event validation
2rg "transport\.subscribe|WS_CHANNELS" apps/web/src/wsNativeApi.ts -B 5 -A 10

Repository: pingdotgg/codething-mvp

Length of output: 2640


🏁 Script executed:

shell
1# Look at the entire wsNativeApi.ts to understand the event flow
2wc -l apps/web/src/wsNativeApi.ts

Repository: pingdotgg/codething-mvp

Length of output: 98


🏁 Script executed:

shell
1# Check if there's validation in the transport layer
2rg "parse|validate|schema" apps/web/src/wsNativeApi.ts -B 2 -A 2

Repository: pingdotgg/codething-mvp

Length of output: 49


🏁 Script executed:

shell
1# Find and check the wsTransport implementation
2fd -e ts "wsTransport"

Repository: pingdotgg/codething-mvp

Length of output: 94


🏁 Script executed:

shell
1# Check wsTransport.ts for validation logic
2cat apps/web/src/wsTransport.ts

Repository: pingdotgg/codething-mvp

Length of output: 5454


🏁 Script executed:

shell
1# Check if there's any validation on the event before it reaches the reducer
2rg "terminalEventSchema|parse|validate" apps/web/src/ -B 2 -A 2

Repository: pingdotgg/codething-mvp

Length of output: 8717


🏁 Script executed:

shell
1# Look at the full event handling in App.tsx to see if validation happens anywhere
2grep -n "api.terminal.onEvent\|dispatch" apps/web/src/App.tsx -A 10 | head -40

Repository: pingdotgg/codething-mvp

Length of output: 1109


Ensure terminal events are validated against the Zod schema before being processed.

The code accesses action.event.runningPorts without defensive handling, but more critically, events flowing from the WebSocket are not validated against terminalEventSchema before reaching the reducer. This violates the requirement to use Zod schemas from packages/contracts for type contracts. Even though the schema defines runningPorts with a .default([]), that validation never executes in the current implementation. Add validation in the event handler (App.tsx or wsNativeApi.ts) to parse events through the appropriate Zod schema before dispatch.

🤖 Prompt for AI Agents
1In `@apps/web/src/store.ts` around lines 706 - 734, The reducer mutates and reads
2action.event.runningPorts without ensuring events were validated by the
3terminalEventSchema; validate/parses incoming socket events through the Zod
4terminalEventSchema before dispatch so defaults (like runningPorts defaulting to
5[]) and types are enforced. Update the WebSocket/event handler (e.g., in App.tsx
6or wsNativeApi.ts) to call terminalEventSchema.parse or safeParse on the raw
7message and only dispatch the parsed value to the reducer (which includes
8normalizeThreadTerminals/runningTerminalPorts logic), and handle parse failures
9(log/ignore) so the reducer never receives unvalidated events.
CcoderabbitaiBotreviewed · 1 inline threadFeb 14, 2026
Synced from GitHub

Actionable comments posted: 1

🤖 Fix all issues with AI agents
1In `@apps/server/src/terminalManager.test.ts`:
2- Around line 354-362: The listen callback used with notFoundServer.listen in
3terminalManager.test.ts incorrectly declares an error parameter; remove the
4unused parameter from the callback and instead attach an 'error' event listener
5on notFoundServer to reject the Promise on binding errors (or use once('error',
6...) before calling listen), keeping the Promise resolution in the listen
7callback (with no args). This updates the signature to match Node's
8server.listen behavior and ensures binding errors are handled via the 'error'
9event rather than a nonexistent callback argument.
🧹 Nitpick comments (3)
apps/web/src/components/Sidebar.tsx (2)

520-533: Consider extracting shared URL-opening logic to reduce duplication.

The onKeyDown handler (lines 523-533) duplicates the external URL opening logic already present in openWebPort. If the URL format or error handling changes, both locations must be updated.

Extract the core action into a shared handler:

♻️ Suggested refactor
diff
1+ const handleOpenWebPort = useCallback(
2+ (port: number) => {
3+ if (!api) return;
4+ void api.shell.openExternal(`http://localhost:${port}`).catch(() => undefined);
5+ },
6+ [api],
7+ );
8+
9 const openWebPort = useCallback(
10 (event: MouseEvent<HTMLElement>, port: number) => {
11 event.preventDefault();
12 event.stopPropagation();
13- if (!api) return;
14- void api.shell.openExternal(`http://localhost:${port}`).catch(() => undefined);
15+ handleOpenWebPort(port);
16 },
17- [api],
18+ [api, handleOpenWebPort],
19 );

Then in the onKeyDown handler:

diff
1 onKeyDown={(event) => {
2 if (event.key !== "Enter" && event.key !== " ") return;
3 event.preventDefault();
4 event.stopPropagation();
5- if (!api) return;
6- void api.shell
7- .openExternal(
8- `http://localhost:${terminalStatus.primaryWebPort!}`,
9- )
10- .catch(() => undefined);
11+ handleOpenWebPort(terminalStatus.primaryWebPort!);
12 }}

514-538: Add cursor-pointer for clickable element affordance.

The span with role="link" is interactive but lacks a pointer cursor, which may confuse users expecting visual feedback on hover.

🖱️ Suggested fix
diff
1 <span
2 role="link"
3 tabIndex={0}
4 aria-label={terminalStatus.label}
5 title={terminalStatus.label}
6- className={`inline-flex items-center justify-center ${terminalStatus.colorClass}`}
7+ className={`inline-flex cursor-pointer items-center justify-center ${terminalStatus.colorClass}`}
apps/server/src/terminalManager.ts (1)

1193-1198: Minor: redundant port normalization.

detectWebPorts already returns a sorted array (line 1153: .toSorted(...)), so the outer normalizeRunningPorts call adds no value. Not a functional issue.

♻️ Suggested simplification
diff
1 const runningPorts = hasRunningSubprocess
2- ? normalizeRunningPorts(
3- await this.detectWebPorts(normalizeRunningPorts(activity.runningPorts)),
4- )
5+ ? await this.detectWebPorts(normalizeRunningPorts(activity.runningPorts))
6 : [];
apps/server/src/terminalManager.test.tslines 354–362Outdated · c0578afOpen thread
……@@ -296,27 +307,106 @@ describe("TerminalManager", () => {
359+ }
360+ resolve();
361+ });
362+ });
CcoderabbitaiBoton lines 354–362Feb 14, 2026

⚠️ Potential issue | 🟡 Minor

Incorrect callback signature for server.listen().

The listen callback receives no arguments. Errors during binding are emitted via the 'error' event on the server, not passed to the callback. While this doesn't break the test (the parameter is simply unused), the signature is misleading.

🔧 Suggested fix
diff
1 await new Promise<void>((resolve, reject) => {
2- notFoundServer.listen(0, "127.0.0.1", (error?: Error) => {
3- if (error) {
4- reject(error);
5- return;
6- }
7- resolve();
8- });
9+ notFoundServer.once("error", reject);
10+ notFoundServer.listen(0, "127.0.0.1", () => {
11+ notFoundServer.removeListener("error", reject);
12+ resolve();
13+ });
14 });
📝 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.

suggestion
1 await new Promise<void>((resolve, reject) => {
2 notFoundServer.once("error", reject);
3 notFoundServer.listen(0, "127.0.0.1", () => {
4 notFoundServer.removeListener("error", reject);
5 resolve();
6 });
7 });
🤖 Prompt for AI Agents
1In `@apps/server/src/terminalManager.test.ts` around lines 354 - 362, The listen
2callback used with notFoundServer.listen in terminalManager.test.ts incorrectly
3declares an error parameter; remove the unused parameter from the callback and
4instead attach an 'error' event listener on notFoundServer to reject the Promise
5on binding errors (or use once('error', ...) before calling listen), keeping the
6Promise resolution in the listen callback (with no args). This updates the
7signature to match Node's server.listen behavior and ensures binding errors are
8handled via the 'error' event rather than a nonexistent callback argument.
apps/server/src/terminalManager.tslines 461–463Outdated · c0578afOpen thread
……@@ -178,32 +263,243 @@ async function checkPosixSubprocessActivity(terminalPid: number): Promise<boolea
460+ chunks.push(fragment);
461+ if (received >= WEB_PORT_PROBE_MAX_BODY_BYTES) {
462+ res.destroy();
463+ }
MmacroscopeappBoton lines 461–463Feb 14, 2026

src/terminalManager.ts:461 Calling res.destroy() prevents the 'end' event from firing, so the promise only resolves when the fallback timer fires (~550ms later). Consider calling settle() immediately after res.destroy().

Code refactored as of 259251c84327b88fcc0bc31bb6b6be9212f2799b

apps/server/src/terminalProcessInspector/win32.tslines 21–22Outdated · 259251cOpen thread
……@@ -0,0 +1,60 @@
19+ },
20+ );
21+ if (result.code !== 0) {
22+ return [];
MmacroscopeappBoton lines 21–22Feb 15, 2026

🟢 Low

terminalProcessInspector/win32.ts:21 When stdoutTruncated is true, the last PID may be partially cut off and parsed as a different valid number. Consider discarding the final line when truncation occurs, or switching to outputMode: "error" to reject truncated output entirely.

diff
1- if (result.code !== 0) {
2+ if (result.code !== 0 || result.stdoutTruncated) {
3 return [];

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
text
1In file apps/server/src/terminalProcessInspector/win32.ts around lines 21-22:
2
3When `stdoutTruncated` is true, the last PID may be partially cut off and parsed as a different valid number. Consider discarding the final line when truncation occurs, or switching to `outputMode: "error"` to reject truncated output entirely.
CcoderabbitaiBotreviewed · 3 inline threadsFeb 15, 2026
Synced from GitHub

Actionable comments posted: 3

🤖 Fix all issues with AI agents
1In `@apps/server/src/terminalProcessInspector/posix.ts`:
2- Around line 67-71: The current check in posix.ts returns [] for any non-zero
3lsof exit, preventing the ss fallback; change the logic so only result.code ===
41 is treated as "no results" and returns [], while any other non-zero
5result.code should not return and should allow the code to fall through to the
6ss fallback (or surface an error if you prefer). Locate the block referencing
7result.code and lsof and update the conditional to return [] only when
8result.code === 1, leaving other non-zero exit codes to proceed to the ss
9handling.
10
11In `@apps/server/src/terminalProcessInspector/utils.ts`:
12- Around line 3-8: The call to Array.prototype.toSorted in normalizeRunningPorts
13requires Node.js 20+, which may not be guaranteed; either declare "node": ">=20"
14in package.json engines or replace toSorted usage in normalizeRunningPorts with
15a runtime-compatible sort (e.g., copy array with slice() or spread and use
16.sort((a,b)=>a-b)) so the function still returns a deduplicated, ascending array
17and keeps the MAX_PORT_NUMBER check intact.
18
19In `@apps/server/src/webPortInspector.ts`:
20- Around line 66-75: When truncating the response in the res.on("data") handler
21(when received >= WEB_PORT_PROBE_MAX_BODY_BYTES), call the existing settle(...)
22logic with the partial body/chunks immediately before calling res.destroy() so
23the promise is resolved with the truncated payload; ensure settle() is
24idempotent (or guard it with a boolean like settled) so it is only invoked once
25(both from the normal 'end' handler and this truncation path). In short: inside
26the data handler for res.on("data") use the same settle(...) call that the 'end'
27handler uses (passing the current chunks/received data and status), then destroy
28the response to stop further data.
🧹 Nitpick comments (1)
apps/server/src/terminalManager.ts (1)

778-789: Consider bounding concurrent web‑port probes.

Promise.all over a large port list can spike outbound probes and latency. Batching keeps behavior more predictable under load.

♻️ Suggested batching approach
diff
1- const checks = await Promise.all(
2- runningPorts.map(async (port) => ({
3- port,
4- isWeb: await this.inspectWebPortCached(port),
5- })),
6- );
7+ const checks: Array<{ port: number; isWeb: boolean }> = [];
8+ const batchSize = 8;
9+ for (let index = 0; index < runningPorts.length; index += batchSize) {
10+ const batch = runningPorts.slice(index, index + batchSize);
11+ const results = await Promise.all(
12+ batch.map(async (port) => ({
13+ port,
14+ isWeb: await this.inspectWebPortCached(port),
15+ })),
16+ );
17+ checks.push(...results);
18+ }

As per coding guidelines: Maintain predictable behavior under load and during failures (session restarts, reconnects, partial streams).

apps/server/src/process/utils.tslines 3–8Outdated · 259251cOpen thread
……@@ -0,0 +1,52 @@
5+ return [...new Set(ports)]
6+ .filter((port) => Number.isInteger(port) && port > 0 && port <= MAX_PORT_NUMBER)
7+ .toSorted((left, right) => left - right);
8+}
CcoderabbitaiBoton lines 3–8Feb 15, 2026

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

shell
1#!/bin/bash
2
3# Find Node version config files and package.json
4echo "=== Node version config files ==="
5find . -type f \( -name ".nvmrc" -o -name ".node-version" \) 2>/dev/null | head -20
6echo ""
7
8# Check package.json for engines field
9echo "=== Checking engines field in package.json files ==="
10fd -t f 'package.json' | head -5 | xargs -I {} sh -c 'echo "File: {}"; cat {} | jq -r ".engines // empty" 2>/dev/null || echo "No engines field"'
11echo ""
12
13# Check for root package.json specifically
14echo "=== Root package.json engines field ==="
15cat package.json 2>/dev/null | jq '.engines' 2>/dev/null || echo "No engines field found"
16echo ""
17
18# Search for toSorted usage
19echo "=== toSorted usage in TypeScript files ==="
20rg -n "toSorted" --type ts --type tsx 2>/dev/null | head -20
21echo ""
22
23# Check Node version mentioned in CI/build configs
24echo "=== Node version in CI/build files ==="
25find . -maxdepth 3 -type f \( -name "*.yml" -o -name "*.yaml" -o -name ".npmrc" -o -name ".eslintrc*" \) 2>/dev/null | xargs grep -l "node" 2>/dev/null | head -5 | xargs -I {} sh -c 'echo "=== {} ==="; cat {}'

Repository: pingdotgg/codething-mvp

Length of output: 473


🏁 Script executed:

shell
1cat -n apps/server/src/terminalProcessInspector/utils.ts 2>/dev/null || echo "File not found or cannot be read"

Repository: pingdotgg/codething-mvp

Length of output: 1885


🏁 Script executed:

shell
1# Check for Bun specifics
2echo "=== Checking Bun vs Node runtime ==="
3cat package.json | jq '.packageManager // empty' 2>/dev/null
4
5# Check apps/server specifically
6echo ""
7echo "=== apps/server/package.json ==="
8cat apps/server/package.json | jq '.engines // .scripts // empty' 2>/dev/null
9
10# Look for runtime detection in build configs
11echo ""
12echo "=== Checking tsconfig or build configs ==="
13find apps/server -maxdepth 2 -name "tsconfig.json" -o -name "*.config.*" | head -10 | xargs cat 2>/dev/null | head -50
14
15# Check for Node references in server code
16echo ""
17echo "=== Checking for Node.js imports in server ==="
18rg "import.*node:|from.*node:" apps/server/src/ --type ts 2>/dev/null | head -10

Repository: pingdotgg/codething-mvp

Length of output: 1658


Verify Node.js version requirement for toSorted or add runtime compatibility fallback.

The code targets ES2023 with toSorted, which requires Node.js 20+. However, package.json engines specifies only bun: >=1.3.9 with no Node.js constraint. In production, this runs on node dist/index.mjs and will fail at runtime on Node < 20 with "toSorted is not a function", violating predictable behavior requirements.

Either add "node": ">=20" to the engines field or use .slice().sort(...) for compatibility:

Compatibility fallback
diff
1 return [...new Set(ports)]
2 .filter((port) => Number.isInteger(port) && port > 0 && port <= MAX_PORT_NUMBER)
3- .toSorted((left, right) => left - right);
4+ .slice()
5+ .sort((left, right) => left - right);
🤖 Prompt for AI Agents
1In `@apps/server/src/terminalProcessInspector/utils.ts` around lines 3 - 8, The
2call to Array.prototype.toSorted in normalizeRunningPorts requires Node.js 20+,
3which may not be guaranteed; either declare "node": ">=20" in package.json
4engines or replace toSorted usage in normalizeRunningPorts with a
5runtime-compatible sort (e.g., copy array with slice() or spread and use
6.sort((a,b)=>a-b)) so the function still returns a deduplicated, ascending array
7and keeps the MAX_PORT_NUMBER check intact.
apps/server/src/terminalProcessInspector/posix.tslines 67–71Outdated · 259251cOpen thread
……@@ -0,0 +1,119 @@
68+ // `lsof` returns 1 when there are no matching files/sockets.
69+ // This is a valid "no results" outcome; avoid falling back to `ss`.
70+ return [];
71+ }
CcoderabbitaiBoton lines 67–71Feb 15, 2026

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

shell
1# First, check the file structure and read the relevant lines
2fd "posix.ts" -t f | head -5

Repository: pingdotgg/codething-mvp

Length of output: 116


🏁 Script executed:

shell
1# Look at the full context of the file around lines 67-71
2cat -n apps/server/src/terminalProcessInspector/posix.ts | sed -n '50,90p'

Repository: pingdotgg/codething-mvp

Length of output: 1610


🌐 Web query:

lsof exit codes documentation meaning

💡 Result:

lsof’s exit status is primarily a “did my requested search succeed?” indicator (not just “did I print any lines?”):

  • 0: No errors detected, and lsof was able to list information for all specified search arguments (and if you didn’t specify any files to search for, success is also 0). [2] [3]
  • 1: Some error was detected including “search failure” cases such as failing to locate a requested name/address/PID/UID/file/etc., or not finding all specified files/filesystems. This often happens when your filters match nothing for one of the specified search items. [2] [3]
  • Repeat mode special case (-r with +): when repeat mode ends because “no open files are listed”, exit is 0 if any open files were ever listed in earlier cycles, otherwise 1. [1]

Related option:

  • -Q: makes “failed to find a search item” still exit 0, but it does not suppress exit 1 for generic/non-search execution errors. [2] [6]

Fall back to ss on lsof failures other than "no results".

Currently, any non-zero lsof exit returns [] and skips the ss fallback. The comment states that exit code 1 means "no matching files/sockets", but lsof exit code 1 actually indicates broader search failures—it can mean no matches, failed to locate requested items, or incomplete results. Other exit codes indicate errors like lsof being unavailable. Treat exit code 1 as a "no results" outcome and fall through to ss for other errors.

🛠️ Suggested fix
diff
1 if (result.code !== 0) {
2 // `lsof` returns 1 when there are no matching files/sockets.
3+ // Other codes indicate errors; fall back to ss.
4- // This is a valid "no results" outcome; avoid falling back to `ss`.
5- return [];
6+ if (result.code === 1) return [];
7+ break;
8 }

(Alternatively, throw an error to surface lsof failures explicitly.)

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

suggestion
1 if (result.code !== 0) {
2 // `lsof` returns 1 when there are no matching files/sockets.
3 // Other codes indicate errors; fall back to ss.
4 if (result.code === 1) return [];
5 break;
6 }
🤖 Prompt for AI Agents
1In `@apps/server/src/terminalProcessInspector/posix.ts` around lines 67 - 71, The
2current check in posix.ts returns [] for any non-zero lsof exit, preventing the
3ss fallback; change the logic so only result.code === 1 is treated as "no
4results" and returns [], while any other non-zero result.code should not return
5and should allow the code to fall through to the ss fallback (or surface an
6error if you prefer). Locate the block referencing result.code and lsof and
7update the conditional to return [] only when result.code === 1, leaving other
8non-zero exit codes to proceed to the ss handling.
apps/server/src/webPortInspector.tslines 66–75Outdated · 259251cOpen thread
……@@ -0,0 +1,114 @@
72+ if (received >= WEB_PORT_PROBE_MAX_BODY_BYTES) {
73+ res.destroy();
74+ }
75+ });
CcoderabbitaiBoton lines 66–75Feb 15, 2026

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

shell
1# First, let's examine the webPortInspector.ts file to understand the full context
2find . -name "webPortInspector.ts" -type f

Repository: pingdotgg/codething-mvp

Length of output: 104


🏁 Script executed:

shell
1# Also search for any web port related files
2fd -e ts -e js | grep -i "port" | grep -i "inspect"

Repository: pingdotgg/codething-mvp

Length of output: 102


🏁 Script executed:

shell
1# Read the webPortInspector.ts file to understand the full context
2cat -n ./apps/server/src/webPortInspector.ts

Repository: pingdotgg/codething-mvp

Length of output: 4004


🌐 Web query:

Node.js HTTP IncomingMessage res.destroy() event behavior end event

💡 Result:

http.IncomingMessage (the res you get in http.request() / https.request()) is a Readable stream.

What happens when you call res.destroy([err])

  • It immediately tears down the stream, drops any unread response data, and releases resources.
  • It will emit:
    • 'error' only if you pass an err to destroy(err)
    • 'close' (unless the stream was created with emitClose: false)
      This is standard Readable-stream destroy() behavior. [2]

Will 'end' fire?

  • Usually no. 'end' is for normal completion (“no more data to be consumed”) and is not emitted unless the response data is fully consumed. Destroying the stream is a premature termination, so you should not rely on 'end' after destroy(). [2]

HTTP-specific events you may see (http.ClientRequest + IncomingMessage)

If you destroy the request (req.destroy()) after the response is received, Node documents this typical sequence:

  • on res: 'aborted' → 'close' → 'error' (often ECONNRESET, message like “aborted”) → 'close' [1]

So for “destroyed early” cases, treat 'close' / 'aborted' / 'error' as the relevant signals, not 'end'. [1][2]

Sources: Node.js HTTP docs [1], Node.js Stream (Readable) docs [2].


Settle truncated responses immediately instead of waiting for timeout.

When the body cap is reached, res.destroy() is called without settling. Since Node.js HTTP responses do not emit the 'end' event after destroy(), the settle() function is never called for truncated responses. The promise then waits for the fallback timeout (550ms), causing large HTML responses to be misclassified as failures. Settle with the partial response before destroying.

🛠️ Suggested fix
diff
1 res.on("data", (chunk: string) => {
2 if (received >= WEB_PORT_PROBE_MAX_BODY_BYTES) return;
3 const remaining = WEB_PORT_PROBE_MAX_BODY_BYTES - received;
4 const fragment = chunk.slice(0, remaining);
5 received += fragment.length;
6 chunks.push(fragment);
7 if (received >= WEB_PORT_PROBE_MAX_BODY_BYTES) {
8+ settle({
9+ status: res.statusCode ?? 0,
10+ contentType: normalizeHeaderValue(res.headers["content-type"]),
11+ location: normalizeHeaderValue(res.headers.location),
12+ body: chunks.join(""),
13+ });
14 res.destroy();
15 }
16 });
🤖 Prompt for AI Agents
1In `@apps/server/src/webPortInspector.ts` around lines 66 - 75, When truncating
2the response in the res.on("data") handler (when received >=
3WEB_PORT_PROBE_MAX_BODY_BYTES), call the existing settle(...) logic with the
4partial body/chunks immediately before calling res.destroy() so the promise is
5resolved with the truncated payload; ensure settle() is idempotent (or guard it
6with a boolean like settled) so it is only invoked once (both from the normal
7'end' handler and this truncation path). In short: inside the data handler for
8res.on("data") use the same settle(...) call that the 'end' handler uses
9(passing the current chunks/received data and status), then destroy the response
10to stop further data.

Added 4 commits

  • Require all lsof filters when checking listening ports94a83ab
  • Track running terminal ports across terminal activity eventsf2ec777
  • Detect real web ports and add one-click localhost shortcut3d082fc
  • adaptersb3e967e
apps/server/src/terminalManager.tslines 844–844Outdated · b3e967eOpen thread
……@@ -834,18 +836,29 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {
841+ ? normalizeRunningPorts(
842+ await this.detectWebPorts(normalizeRunningPorts(activity.runningPorts)),
843+ )
844+ : [];
MmacroscopeappBoton lines 844–844Feb 16, 2026

src/terminalManager.ts:844

Stale update risk: after await this.detectWebPorts(...), the session may have restarted. Suggest re-checking that liveSession is still current (e.g., status === 'running' && pid === terminalPid) before updating subprocess state or emitting the activity event.

No longer relevant as of 05a7018f0302121cfc15ce9bf2db72498f5e09d4

CcoderabbitaiBotreviewed · 1 inline threadFeb 16, 2026
Synced from GitHub

Actionable comments posted: 1

🤖 Fix all issues with AI agents
1In `@apps/server/src/terminalManager.ts`:
2- Around line 796-800: Replace the double-normalization around detectWebPorts:
3remove the outer normalizeRunningPorts(...) call and instead call detectWebPorts
4with the normalized activity.runningPorts directly by invoking
5this.detectWebPorts(normalizeRunningPorts(activity.runningPorts)); update the
6call site where normalizeRunningPorts(await this.detectWebPorts(...)) is used so
7only normalizeRunningPorts(activity.runningPorts) is passed into detectWebPorts
8(referencing the functions normalizeRunningPorts and detectWebPorts and the
9variable activity.runningPorts).
🧹 Nitpick comments (4)
apps/server/src/terminalManager.ts (1)

839-844: Consider removing redundant normalizeRunningPorts call.

The outer normalizeRunningPorts() wrapping the result of detectWebPorts() is redundant since detectWebPorts() already returns a sorted, deduplicated array via .toSorted(). This doesn't cause bugs but adds unnecessary overhead.

♻️ Suggested simplification
diff
1 const runningPorts = hasRunningSubprocess
2- ? normalizeRunningPorts(
3- await this.detectWebPorts(normalizeRunningPorts(activity.runningPorts)),
4- )
5+ ? await this.detectWebPorts(normalizeRunningPorts(activity.runningPorts))
6 : [];
apps/web/src/components/Sidebar.tsx (3)

113-119: Consider extracting shared port normalization logic.

This port aggregation and normalization logic duplicates normalizeRunningPorts from store.ts. While the duplication is minor, consider importing or extracting a shared utility to ensure consistent behavior.


561-600: Minor: Refactor duplicated port-opening logic in onKeyDown handler.

The onKeyDown handler (Lines 572-581) duplicates the port-opening logic instead of reusing openWebPort. This creates maintenance overhead if the behavior needs to change.

♻️ Proposed refactor to reuse openWebPort
diff
1 onKeyDown={(event) => {
2 if (event.key !== "Enter" && event.key !== " ") return;
3- event.preventDefault();
4- event.stopPropagation();
5- if (!api) return;
6- void api.shell
7- .openExternal(
8- `http://localhost:${terminalStatus.primaryWebPort!}`,
9- )
10- .catch(() => undefined);
11+ openWebPort(
12+ event as unknown as MouseEvent<HTMLElement>,
13+ terminalStatus.primaryWebPort!,
14+ );
15 }}

Alternatively, extract a shared handler that accepts both event types:

typescript
1const handleOpenWebPort = useCallback(
2 (event: { preventDefault: () => void; stopPropagation: () => void }, port: number) => {
3 event.preventDefault();
4 event.stopPropagation();
5 if (!api) return;
6 void api.shell.openExternal(`http://localhost:${port}`).catch(() => undefined);
7 },
8 [api],
9);

563-587: Consider using role="button" instead of role="link" for the interactive port indicator.

The element behaves like a button (triggers an action) rather than a link (navigates to a URL). Using role="button" would be more semantically accurate for accessibility tools.

♻️ Proposed fix
diff
1 <span
2- role="link"
3+ role="button"
4 tabIndex={0}
apps/server/src/terminalManager.tslines 796–800Outdated · b3e967eOpen thread
……@@ -800,6 +757,48 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {
797+ .filter((entry) => entry.isWeb)
798+ .map((entry) => entry.port)
799+ .toSorted((left, right) => left - right);
800+ }
CcoderabbitaiBoton lines 796–800Feb 16, 2026

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

shell
1#!/bin/bash
2# Check Node.js version requirements in package.json files
3echo "=== Checking engines field in package.json files ==="
4fd -t f 'package.json' --max-depth 3 | xargs -I {} sh -c 'echo "=== {} ==="; cat {} | jq ".engines // empty" 2>/dev/null'
5
6echo ""
7echo "=== Checking .nvmrc or .node-version ==="
8cat .nvmrc 2>/dev/null || cat .node-version 2>/dev/null || echo "No version file found"

Repository: pingdotgg/codething-mvp

Length of output: 364


🏁 Script executed:

shell
1# View the specific lines in terminalManager.ts around 796-800
2echo "=== terminalManager.ts lines 790-805 ==="
3sed -n '790,805p' apps/server/src/terminalManager.ts
4
5echo ""
6echo "=== terminalManager.ts lines 835-850 (double normalization check) ==="
7sed -n '835,850p' apps/server/src/terminalManager.ts
8
9echo ""
10echo "=== Search for detectWebPorts method definition ==="
11rg -n "detectWebPorts" apps/server/src/terminalManager.ts -A 15

Repository: pingdotgg/codething-mvp

Length of output: 2739


🏁 Script executed:

shell
1# Check utils.ts for .toSorted() usage
2echo "=== Search for toSorted usage in utils.ts ==="
3rg -n "toSorted" apps/server/src/
4
5echo ""
6echo "=== Check if Array.prototype.toSorted is polyfilled or available ==="
7rg -n "toSorted|Array.*sort" packages/ -t ts -t js --max-count 20

Repository: pingdotgg/codething-mvp

Length of output: 739


🏁 Script executed:

shell
1# Check what normalizeRunningPorts does
2echo "=== normalizeRunningPorts definition ==="
3rg -n "normalizeRunningPorts" apps/server/src/terminalManager.ts -B 2 -A 8

Repository: pingdotgg/codething-mvp

Length of output: 1073


🏁 Script executed:

shell
1# Check if there's any documentation about project runtime requirements
2echo "=== Check README for runtime requirements ==="
3head -50 README.md 2>/dev/null || echo "No README at root"
4
5echo ""
6echo "=== Check apps/server for runtime info ==="
7head -30 apps/server/README.md 2>/dev/null || echo "No README in apps/server"
8
9echo ""
10echo "=== Check tsconfig to see ES2023 target ==="
11cat apps/server/tsconfig.json 2>/dev/null | grep -A 5 -B 5 "target" || echo "No tsconfig found"

Repository: pingdotgg/codething-mvp

Length of output: 2080


🏁 Script executed:

shell
1# Find and examine normalizeRunningPorts implementation
2echo "=== Search for normalizeRunningPorts implementation ==="
3rg -n "export.*normalizeRunningPorts|function normalizeRunningPorts|const normalizeRunningPorts" apps/server/src/ -A 10
4
5echo ""
6echo "=== Check terminalProcessInspector imports ==="
7head -40 apps/server/src/terminalProcessInspector/index.ts 2>/dev/null || echo "Checking for the file..."
8
9echo ""
10echo "=== Find terminalProcessInspector directory structure ==="
11find apps/server/src/terminalProcessInspector -type f -name "*.ts" 2>/dev/null

Repository: pingdotgg/codething-mvp

Length of output: 2822


🏁 Script executed:

shell
1# Check if there's a .node-version or engines requirement we missed
2echo "=== Check all package.json files for engines field ==="
3find . -name "package.json" -type f -exec grep -l "engines" {} \;
4
5echo ""
6echo "=== Check apps/server/package.json completely ==="
7cat apps/server/package.json | head -40

Repository: pingdotgg/codething-mvp

Length of output: 933


Remove redundant normalizeRunningPorts() call at line 841.

The double normalization is unnecessary. The inner call at line 842 normalizes activity.runningPorts, then detectWebPorts() returns an already-sorted array via .toSorted(), making the outer normalizeRunningPorts() at line 841 redundant. Simplify to: await this.detectWebPorts(normalizeRunningPorts(activity.runningPorts)).

🤖 Prompt for AI Agents
1In `@apps/server/src/terminalManager.ts` around lines 796 - 800, Replace the
2double-normalization around detectWebPorts: remove the outer
3normalizeRunningPorts(...) call and instead call detectWebPorts with the
4normalized activity.runningPorts directly by invoking
5this.detectWebPorts(normalizeRunningPorts(activity.runningPorts)); update the
6call site where normalizeRunningPorts(await this.detectWebPorts(...)) is used so
7only normalizeRunningPorts(activity.runningPorts) is passed into detectWebPorts
8(referencing the functions normalizeRunningPorts and detectWebPorts and the
9variable activity.runningPorts).

Added 1 commit

  • Show terminal runtime status and web port actions in thread UIc11027b
packages/contracts/src/terminal.tslines 92–92Outdated · c11027bOpen thread
……@@ -89,6 +89,7 @@ export const terminalRestartedEventSchema = terminalEventBaseSchema.extend({
8989export const terminalActivityEventSchema = terminalEventBaseSchema.extend({
9090 type: z.literal("activity"),
9191 hasRunningSubprocess: z.boolean(),
92+ runningPorts: z.array(z.number().int().min(1).max(65_535)).default([]),
MmacroscopeappBoton lines 92–92Feb 16, 2026

src/terminal.ts:92

.default([]) shares the same array instance across all parsed objects. Consider using .default(() => []) to return a fresh array each time.

No longer relevant as of 05a7018f0302121cfc15ce9bf2db72498f5e09d4

apps/server/src/terminalManager.tsline 839Outdated · c11027bOpen thread
……@@ -834,18 +836,29 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> {
835837 return;
836838 }
837- if (liveSession.hasRunningSubprocess === hasRunningSubprocess) {
839+ const hasRunningSubprocess = activity.hasRunningSubprocess === true;
MmacroscopeappBoton line 839Feb 16, 2026

src/terminalManager.ts:839 The detectWebPorts call on line 842 isn't wrapped in a try/catch like the subprocessInspector call above. If it throws, the unhandled rejection could crash the process. Consider wrapping it similarly.

✅ Addressed in c11027be4d9183c9028f91bf68c0abb5934b103b

Added 3 commits

  • Detect root-process ports and harden web port probing38644b0
  • Merge origin/main into codething/50e6e9dad377e56
  • Merge origin/main into codething/50e6e9da05a7018
apps/web/src/components/ChatView.tsxfileOutdated · 05a7018Open thread

🟢 Low

https://github.com/pingdotgg/t3code/blob/05a7018f0302121cfc15ce9bf2db72498f5e09d4/apps/web/src/components/ChatView.tsx#L435

selectThreadTerminalState returns raw persisted ThreadTerminalState without normalization, so when old persisted data lacks runningTerminalPorts, terminalState.runningTerminalPorts is undefined. This undefined is then passed directly to ThreadTerminalDrawer at line 562, likely causing runtime errors when the component expects a Record<string, number[]>. The persist middleware has no migration to add the missing field to existing data.

✅ Resolved in 037a7719fa567006c0ddafe4137a7b162326593e

Synced from GitHub

Approvability

Verdict: Needs human review

This PR introduces a substantial new feature for detecting and displaying terminal listening ports, including spawning external processes (lsof/ss/powershell), HTTP probing of localhost ports, caching logic, and UI updates with click-to-open functionality. Multiple unresolved review comments identify potential bugs including wrong icons and lsof fallback issues. The scope and complexity warrant human review.

You can customize Macroscope's approvability policy. Learn more.

CcursorBotreviewed · 3 inline threadsApr 4, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Windows misses grandchild ports unlike POSIX full-tree traversal
    • Replaced collectWindowsChildPids (direct-children-only) with collectWindowsProcessFamilyPids that fetches all process PIDs with parent info and performs BFS traversal matching the POSIX implementation.
  • ✅ Fixed: Exported subprocessCheckerToInspector is never imported or used
    • Removed the unused subprocessCheckerToInspector function and its unused local type imports from index.ts.
  • ✅ Fixed: Triplicated normalizeRunningPorts across web app files
    • Exported the existing normalizeRunningPorts from terminalStateStore.ts and replaced the duplicate definitions in Sidebar.tsx and ThreadTerminalDrawer.tsx with imports.

Create PR

Or push these changes by commenting:

1@cursor push 755bf3aa0d
Preview (pingdotgg/t3code@755bf3aa0d)
diff
1diff --git a/apps/server/src/terminalProcessInspector/index.ts b/apps/server/src/terminalProcessInspector/index.ts
2--- a/apps/server/src/terminalProcessInspector/index.ts
3+++ b/apps/server/src/terminalProcessInspector/index.ts
4@@ -1,11 +1,6 @@
5 import { collectPosixProcessFamilyPids, checkPosixListeningPorts } from "./posix";
6-import {
7- type TerminalSubprocessActivity,
8- type TerminalSubprocessChecker,
9- type TerminalSubprocessInspector,
10- type TerminalWebPortInspector,
11-} from "./types";
12-import { checkWindowsListeningPorts, collectWindowsChildPids } from "./win32";
13+import { type TerminalSubprocessActivity } from "./types";
14+import { checkWindowsListeningPorts, collectWindowsProcessFamilyPids } from "./win32";
15
16 export { arePortListsEqual, normalizeRunningPorts } from "./utils";
17
18@@ -24,11 +19,15 @@
19 }
20
21 if (process.platform === "win32") {
22- const childPids = await collectWindowsChildPids(terminalPid);
23- const processPidsForPortScan = [terminalPid, ...childPids];
24- const runningPorts = await checkWindowsListeningPorts(processPidsForPortScan);
25+ const processFamilyPids = await collectWindowsProcessFamilyPids(terminalPid);
26+ if (processFamilyPids.length === 0) {
27+ return { hasRunningSubprocess: false, runningPorts: [] };
28+ }
29+
30+ const subprocessPids = processFamilyPids.filter((pid) => pid !== terminalPid);
31+ const runningPorts = await checkWindowsListeningPorts(processFamilyPids);
32 return {
33- hasRunningSubprocess: childPids.length > 0 || runningPorts.length > 0,
34+ hasRunningSubprocess: subprocessPids.length > 0 || runningPorts.length > 0,
35 runningPorts,
36 };
37 }
38@@ -45,12 +44,3 @@
39 runningPorts,
40 };
41 }
42-
43-export function subprocessCheckerToInspector(
44- subprocessChecker: TerminalSubprocessChecker,
45-): TerminalSubprocessInspector {
46- return async (terminalPid: number) => ({
47- hasRunningSubprocess: await subprocessChecker(terminalPid),
48- runningPorts: [],
49- });
50-}
51
52diff --git a/apps/server/src/terminalProcessInspector/win32.ts b/apps/server/src/terminalProcessInspector/win32.ts
53--- a/apps/server/src/terminalProcessInspector/win32.ts
54+++ b/apps/server/src/terminalProcessInspector/win32.ts
55@@ -1,27 +1,56 @@
56 import { runProcess } from "../processRunner";
57-import { parsePidList, parsePortList } from "./utils";
58+import { parsePortList } from "./utils";
59
60-export async function collectWindowsChildPids(terminalPid: number): Promise<number[]> {
61+export async function collectWindowsProcessFamilyPids(terminalPid: number): Promise<number[]> {
62 const command = [
63- `$children = Get-CimInstance Win32_Process -Filter "ParentProcessId = ${terminalPid}" -ErrorAction SilentlyContinue`,
64- "if (-not $children) { exit 0 }",
65- "$children | Select-Object -ExpandProperty ProcessId",
66+ "$procs = Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId -ErrorAction SilentlyContinue",
67+ "if (-not $procs) { exit 0 }",
68+ '$procs | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId)" }',
69 ].join("; ");
70 try {
71 const result = await runProcess(
72 "powershell.exe",
73 ["-NoProfile", "-NonInteractive", "-Command", command],
74 {
75- timeoutMs: 1_500,
76+ timeoutMs: 2_000,
77 allowNonZeroExit: true,
78- maxBufferBytes: 32_768,
79+ maxBufferBytes: 262_144,
80 outputMode: "truncate",
81 },
82 );
83 if (result.code !== 0) {
84 return [];
85 }
86- return parsePidList(result.stdout);
87+
88+ const childrenByParentPid = new Map<number, number[]>();
89+ for (const line of result.stdout.split(/\r?\n/g)) {
90+ const [pidRaw, ppidRaw] = line.trim().split(/\s+/g);
91+ const pid = Number(pidRaw);
92+ const ppid = Number(ppidRaw);
93+ if (!Number.isInteger(pid) || !Number.isInteger(ppid)) continue;
94+ const children = childrenByParentPid.get(ppid);
95+ if (children) {
96+ children.push(pid);
97+ } else {
98+ childrenByParentPid.set(ppid, [pid]);
99+ }
100+ }
101+
102+ const processFamily = new Set<number>([terminalPid]);
103+ const pendingParents = [terminalPid];
104+ while (pendingParents.length > 0) {
105+ const parentPid = pendingParents.shift();
106+ if (!parentPid) continue;
107+ const childPids = childrenByParentPid.get(parentPid);
108+ if (!childPids || childPids.length === 0) continue;
109+ for (const childPid of childPids) {
110+ if (processFamily.has(childPid)) continue;
111+ processFamily.add(childPid);
112+ pendingParents.push(childPid);
113+ }
114+ }
115+
116+ return [...processFamily];
117 } catch {
118 return [];
119 }
120
121diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx
122--- a/apps/web/src/components/Sidebar.tsx
123+++ b/apps/web/src/components/Sidebar.tsx
124@@ -61,7 +61,11 @@
125 import { isTerminalFocused } from "../lib/terminalFocus";
126 import { isLinuxPlatform, isMacPlatform, newCommandId, newProjectId } from "../lib/utils";
127 import { useStore } from "../store";
128-import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";
129+import {
130+ normalizeRunningPorts,
131+ selectThreadTerminalState,
132+ useTerminalStateStore,
133+} from "../terminalStateStore";
134 import { useUiStateStore } from "../uiStateStore";
135 import {
136 resolveShortcutCommand,
137@@ -204,13 +208,6 @@
138 );
139 }
140
141-function normalizeRunningPorts(rawPorts: number[] | undefined): number[] {
142- if (!rawPorts) return [];
143- return [...new Set(rawPorts)]
144- .filter((port) => Number.isInteger(port) && port > 0 && port <= 65_535)
145- .toSorted((left, right) => left - right);
146-}
147-
148 function terminalStatusFromTerminalState(
149 runningTerminalIds: string[],
150 runningTerminalPorts: Record<string, number[]>,
151
152diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx
153--- a/apps/web/src/components/ThreadTerminalDrawer.tsx
154+++ b/apps/web/src/components/ThreadTerminalDrawer.tsx
155@@ -33,7 +33,11 @@
156 type ThreadTerminalGroup,
157 } from "../types";
158 import { readNativeApi } from "~/nativeApi";
159-import { selectTerminalEventEntries, useTerminalStateStore } from "../terminalStateStore";
160+import {
161+ normalizeRunningPorts,
162+ selectTerminalEventEntries,
163+ useTerminalStateStore,
164+} from "../terminalStateStore";
165
166 const MIN_DRAWER_HEIGHT = 180;
167 const MAX_DRAWER_HEIGHT_RATIO = 0.75;
168@@ -81,13 +85,6 @@
169 extraWebPortCount: number;
170 }
171
172-function normalizeRunningPorts(rawPorts: number[] | undefined): number[] {
173- if (!rawPorts) return [];
174- return [...new Set(rawPorts)]
175- .filter((port) => Number.isInteger(port) && port > 0 && port <= 65_535)
176- .toSorted((left, right) => left - right);
177-}
178-
179 function terminalRuntimeStatus(
180 terminalId: string,
181 runningTerminalIds: Set<string>,
182
183diff --git a/apps/web/src/terminalStateStore.ts b/apps/web/src/terminalStateStore.ts
184--- a/apps/web/src/terminalStateStore.ts
185+++ b/apps/web/src/terminalStateStore.ts
186@@ -62,7 +62,7 @@
187 .filter((id) => id.length > 0 && validTerminalIdSet.has(id));
188 }
189
190-function normalizeRunningPorts(ports: readonly number[] | undefined): number[] {
191+export function normalizeRunningPorts(ports: readonly number[] | undefined): number[] {
192 if (!ports || ports.length === 0) return [];
193 return [...new Set(ports)]
194 .filter((port) => Number.isInteger(port) && port > 0 && port <= 65_535)

You can send follow-ups to the cloud agent here.

apps/web/src/components/Sidebar.tsxline 212Outdated · 05a7018Open thread
……@@ -202,16 +204,39 @@ function ThreadStatusLabel({
209+ return [...new Set(rawPorts)]
210+ .filter((port) => Number.isInteger(port) && port > 0 && port <= 65_535)
211+ .toSorted((left, right) => left - right);
212+}
CcursorBoton line 212Apr 4, 2026

Triplicated normalizeRunningPorts across web app files

Low Severity

normalizeRunningPorts is independently defined with near-identical logic in Sidebar.tsx, ThreadTerminalDrawer.tsx, and terminalStateStore.ts. All three deduplicate, filter, and sort port arrays the same way. If the normalization rules change (e.g., port range), three places need updating, risking inconsistency.

Additional Locations (2)

Reviewed by Cursor Bugbot for commit 05a7018f0302121cfc15ce9bf2db72498f5e09d4. Configure here.

apps/server/src/terminalProcessInspector/index.tsline 56Outdated · 05a7018Open thread
……@@ -0,0 +1,56 @@
53+ hasRunningSubprocess: await subprocessChecker(terminalPid),
54+ runningPorts: [],
55+ });
56+}
CcursorBoton line 56Apr 4, 2026

Exported subprocessCheckerToInspector is never imported or used

Low Severity

subprocessCheckerToInspector is exported from the terminalProcessInspector module but is never imported anywhere in the codebase. Manager.ts (the sole consumer of this module) implements the same adapter inline when options.subprocessChecker is provided. This is dead code that also pulls in the unused TerminalSubprocessChecker type from types.ts.

Reviewed by Cursor Bugbot for commit 05a7018f0302121cfc15ce9bf2db72498f5e09d4. Configure here.

apps/server/src/terminalProcessInspector/win32.tsline 28Outdated · 05a7018Open thread
……@@ -0,0 +1,58 @@
25+ } catch {
26+ return [];
27+ }
28+}
CcursorBoton line 28Apr 4, 2026

Windows misses grandchild ports unlike POSIX full-tree traversal

Medium Severity

collectWindowsChildPids only queries direct children via ParentProcessId = ${terminalPid}, while collectPosixProcessFamilyPids performs a full BFS traversal to collect the entire descendant tree. In a typical dev workflow (e.g., terminal → npm → node listening on port 3000), the Windows path misses the grandchild node process, so checkWindowsListeningPorts won't detect port 3000. The PR description states it "detects process families" on both platforms, but only POSIX actually walks the tree.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 05a7018f0302121cfc15ce9bf2db72498f5e09d4. Configure here.

Added 1 commit

  • Extract terminal inspectors into Effect services75ae12f
apps/server/src/process/Layers/TerminalProcessInspector.test.tslines 20–38Outdated · 75ae12fOpen thread
……@@ -0,0 +1,162 @@
35+ clearTimeout(timeout);
36+ resume(Effect.void);
37+ });
38+ });
MmacroscopeappBoton lines 20–38Apr 4, 2026

🟢 Low terminalProcessInspector/index.test.ts:20

If the effect returned by stopProcess is interrupted before the process exits, the setTimeout at line 28 keeps running and the exit listener at line 34 remains attached — the cleanup function at line 86 isn't returned, so interruption leaks resources. Consider returning Effect.sync(cleanup) so interruption cancels the timeout and removes the listener.

No longer relevant as of bc30ac51f5dd298fd122031d1a70989b6b6a242f

apps/server/src/webPortInspector.test.tslines 24–31Outdated · 75ae12fOpen thread
……@@ -0,0 +1,109 @@
28+ resume(Effect.fail(error));
29+ return;
30+ }
31+
MmacroscopeappBoton lines 24–31Apr 4, 2026

🟢 Low src/webPortInspector.test.ts:24

The server.listen callback signature is wrong: Node.js passes no arguments to the success callback, so error?: Error will always be undefined. Listen failures (e.g., port already in use) emit an 'error' event instead, which this code doesn't handle. The Effect will hang forever on listen failures because resume is never invoked.

No longer relevant as of 6aac79caff618d7affa32076be551dea38b49819

Added 1 commit

  • Consolidate terminal inspectors under processbc30ac5
CcursorBotreviewed · 1 inline threadApr 4, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Web port probe cache grows without bound
    • Added a cleanup pass in inspectWebPortCached that deletes all expired entries from the map whenever a cache miss occurs, preventing unbounded growth.

Create PR

Or push these changes by commenting:

1@cursor push 2d3f25de8a
Preview (pingdotgg/t3code@2d3f25de8a)
diff
1diff --git a/apps/server/src/terminal/Layers/Manager.ts b/apps/server/src/terminal/Layers/Manager.ts
2--- a/apps/server/src/terminal/Layers/Manager.ts
3+++ b/apps/server/src/terminal/Layers/Manager.ts
4@@ -613,6 +613,12 @@
5 return cached.isWeb;
6 }
7
8+ for (const [p, entry] of webPortProbeCache) {
9+ if (now - entry.checkedAt > webPortProbeCacheTtlMs) {
10+ webPortProbeCache.delete(p);
11+ }
12+ }
13+
14 const isWeb = yield* webPortInspector(port).pipe(Effect.catch(() => Effect.succeed(false)));
15 webPortProbeCache.set(port, {
16 isWeb,

You can send follow-ups to the cloud agent here.

apps/server/src/terminal/Layers/Manager.tsline 622Outdated · bc30ac5Open thread
……@@ -695,6 +605,44 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
619+ checkedAt: Date.now(),
620+ });
621+ return isWeb;
622+ });
CcursorBoton line 622Apr 4, 2026

Web port probe cache grows without bound

Low Severity

The webPortProbeCache map is never pruned — entries are added on each probe but never removed, even when their TTL expires. Over a long-running server session with many ephemeral dev-server ports, stale entries accumulate indefinitely. Adding eviction of expired entries during cache reads or periodically would keep memory bounded.

Reviewed by Cursor Bugbot for commit bc30ac51f5dd298fd122031d1a70989b6b6a242f. Configure here.

Added 1 commit

  • Use Effect HTTP client for web port probing6aac79c
CcursorBotreviewed · 2 inline threadsApr 4, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 5 total unresolved issues (including 3 from previous reviews).

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Timeout errors double-wrapped by subsequent mapError
    • Added a _tag check in mapError to pass through errors that are already WebPortInspectionError instances, preserving the original timeout detail message.
  • ✅ Fixed: extraWebPortCount computed but never used in rendering
    • Removed the unused extraWebPortCount field from the TerminalRuntimeStatus interface and its computation in terminalRuntimeStatus().

Create PR

Or push these changes by commenting:

1@cursor push e11ef1b119
Preview (pingdotgg/t3code@e11ef1b119)
diff
1diff --git a/apps/server/src/process/Layers/WebPortInspector.ts b/apps/server/src/process/Layers/WebPortInspector.ts
2--- a/apps/server/src/process/Layers/WebPortInspector.ts
3+++ b/apps/server/src/process/Layers/WebPortInspector.ts
4@@ -127,15 +127,22 @@
5 onSome: Effect.succeed,
6 }),
7 ),
8- Effect.mapError(
9- (cause) =>
10- new WebPortInspectionError({
11- port,
12- host,
13- detail: "Failed to execute HTTP probe request.",
14- cause,
15- }),
16- ),
17+ Effect.mapError((cause) => {
18+ if (
19+ typeof cause === "object" &&
20+ cause !== null &&
21+ "_tag" in cause &&
22+ cause._tag === "WebPortInspectionError"
23+ ) {
24+ return cause as WebPortInspectionError;
25+ }
26+ return new WebPortInspectionError({
27+ port,
28+ host,
29+ detail: "Failed to execute HTTP probe request.",
30+ cause,
31+ });
32+ }),
33 );
34 });
35
36
37diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx
38--- a/apps/web/src/components/ThreadTerminalDrawer.tsx
39+++ b/apps/web/src/components/ThreadTerminalDrawer.tsx
40@@ -78,7 +78,6 @@
41 interface TerminalRuntimeStatus {
42 label: string;
43 primaryWebPort: number | null;
44- extraWebPortCount: number;
45 }
46
47 function normalizeRunningPorts(rawPorts: number[] | undefined): number[] {
48@@ -99,7 +98,6 @@
49
50 const runningPorts = normalizeRunningPorts(runningTerminalPorts[terminalId]);
51 const primaryWebPort = runningPorts[0] ?? null;
52- const extraWebPortCount = runningPorts.length > 1 ? runningPorts.length - 1 : 0;
53 const label =
54 runningPorts.length === 0
55 ? "Terminal process running"
56@@ -110,7 +108,6 @@
57 return {
58 label,
59 primaryWebPort,
60- extraWebPortCount,
61 };
62 }

You can send follow-ups to the cloud agent here.

apps/web/src/components/ThreadTerminalDrawer.tsxline 114Outdated · 6aac79cOpen thread
……@@ -74,6 +75,45 @@ export function selectPendingTerminalEventEntries(
111+ label,
112+ primaryWebPort,
113+ extraWebPortCount,
114+ };
CcursorBoton line 114Apr 4, 2026

extraWebPortCount computed but never used in rendering

Low Severity

The TerminalRuntimeStatus interface declares extraWebPortCount (line 81) and terminalRuntimeStatus computes it (line 102), but it's never consumed in the rendering code. The runtimeStatus object is only accessed for primaryWebPort and label in the JSX, making extraWebPortCount dead code.

Reviewed by Cursor Bugbot for commit 6aac79caff618d7affa32076be551dea38b49819. Configure here.

apps/server/src/process/Layers/WebPortInspector.tsline 138Outdated · 6aac79cOpen thread
……@@ -0,0 +1,179 @@
135+ detail: "Failed to execute HTTP probe request.",
136+ cause,
137+ }),
138+ ),
CcursorBoton line 138Apr 4, 2026

Timeout errors double-wrapped by subsequent mapError

Medium Severity

The Effect.mapError at line 130 catches all errors flowing through the pipeline, including the WebPortInspectionError already created by the timeout handler at line 120–126. When a probe times out, the error with detail "HTTP probe timed out." gets wrapped again into a new WebPortInspectionError with detail "Failed to execute HTTP probe request.", burying the original message in cause. This obscures the real failure reason in logs and error reporting.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 6aac79caff618d7affa32076be551dea38b49819. Configure here.

Added 1 commit

  • Move process inspector types beside their services0236cc8
CcursorBotreviewed · 1 inline threadApr 4, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 6 total unresolved issues (including 5 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Windows subprocess inspection misses grandchild processes unlike POSIX
    • Changed collectWindowsChildPids to fetch the full process table (ProcessId + ParentProcessId) and perform a BFS traversal from the terminal PID, matching the POSIX implementation's behavior for discovering all descendant processes.

Create PR

Or push these changes by commenting:

1@cursor push 89b175a1cb
Preview (pingdotgg/t3code@89b175a1cb)
diff
1diff --git a/apps/server/src/process/win32.ts b/apps/server/src/process/win32.ts
2--- a/apps/server/src/process/win32.ts
3+++ b/apps/server/src/process/win32.ts
4@@ -1,7 +1,7 @@
5 import { Effect } from "effect";
6
7 import type { TerminalProcessInspectionError } from "./Services/TerminalProcessInspector";
8-import { parsePidList, parsePortList } from "./utils";
9+import { parsePortList } from "./utils";
10
11 interface InspectorCommandResult {
12 readonly stdout: string;
13@@ -27,9 +27,9 @@
14 runCommand: WindowsRunCommand,
15 ): Effect.fn.Return<number[], TerminalProcessInspectionError> {
16 const command = [
17- `$children = Get-CimInstance Win32_Process -Filter "ParentProcessId = ${terminalPid}" -ErrorAction SilentlyContinue`,
18- "if (-not $children) { exit 0 }",
19- "$children | Select-Object -ExpandProperty ProcessId",
20+ "$procs = Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId -ErrorAction SilentlyContinue",
21+ "if (-not $procs) { exit 0 }",
22+ '$procs | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId)" }',
23 ].join("; ");
24 const result = yield* runCommand({
25 operation: "TerminalProcessInspector.collectWindowsChildPids",
26@@ -37,12 +37,40 @@
27 command: "powershell.exe",
28 args: ["-NoProfile", "-NonInteractive", "-Command", command],
29 timeoutMs: 1_500,
30- maxOutputBytes: 32_768,
31+ maxOutputBytes: 262_144,
32 });
33 if (result.exitCode !== 0) {
34 return [];
35 }
36- return parsePidList(result.stdout);
37+
38+ const childrenByParentPid = new Map<number, number[]>();
39+ for (const line of result.stdout.split(/\r?\n/g)) {
40+ const [pidRaw, ppidRaw] = line.trim().split(/\s+/g);
41+ const pid = Number(pidRaw);
42+ const ppid = Number(ppidRaw);
43+ if (!Number.isInteger(pid) || !Number.isInteger(ppid)) continue;
44+ const children = childrenByParentPid.get(ppid);
45+ if (children) {
46+ children.push(pid);
47+ } else {
48+ childrenByParentPid.set(ppid, [pid]);
49+ }
50+ }
51+
52+ const family = new Set<number>();
53+ const pending = [terminalPid];
54+ while (pending.length > 0) {
55+ const parentPid = pending.shift()!;
56+ const childPids = childrenByParentPid.get(parentPid);
57+ if (!childPids || childPids.length === 0) continue;
58+ for (const childPid of childPids) {
59+ if (family.has(childPid)) continue;
60+ family.add(childPid);
61+ pending.push(childPid);
62+ }
63+ }
64+
65+ return [...family];
66 });
67
68 export const checkWindowsListeningPorts = Effect.fn("process.checkWindowsListeningPorts")(

You can send follow-ups to the cloud agent here.

apps/server/src/process/win32.tsline 46Outdated · 0236cc8Open thread
……@@ -0,0 +1,78 @@
43+ return [];
44+ }
45+ return parsePidList(result.stdout);
46+});
CcursorBoton line 46Apr 4, 2026

Windows subprocess inspection misses grandchild processes unlike POSIX

Medium Severity

collectWindowsChildPids only queries direct children via ParentProcessId = ${terminalPid}, while collectPosixProcessFamilyPids performs a full BFS traversal of the process tree. On Windows, grandchild processes (e.g., shell → npm → node listening on a port) won't be detected for subprocess activity or port scanning, causing inconsistent behavior across platforms.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 0236cc845e0621a281ad3cac36c5bd341594236f. Configure here.

Added 1 commit

  • Normalize terminal state and harden process probingda81a16
CcursorBotreviewed · 3 inline threadsApr 5, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Duplicated normalizeRunningPorts across server and web packages
    • Extracted normalizeRunningPorts into @t3tools/shared/port and updated both server and web packages to import from the shared module.
  • ✅ Fixed: Duplicated port label construction in sidebar and drawer
    • Extracted portStatusLabel helper into @t3tools/shared/port and updated both ThreadTerminalDrawer.tsx and Sidebar.tsx to use it.
  • ✅ Fixed: Negative web port probe results are cached for TTL
    • Modified inspectWebPortCached to only cache successful probe results, so transient failures are re-probed on the next poll cycle.

Create PR

Or push these changes by commenting:

1@cursor push b1ad5fac52
Preview (pingdotgg/t3code@b1ad5fac52)
diff
1diff --git a/apps/server/src/process/utils.ts b/apps/server/src/process/utils.ts
2--- a/apps/server/src/process/utils.ts
3+++ b/apps/server/src/process/utils.ts
4@@ -1,11 +1,6 @@
5-export const MAX_PORT_NUMBER = 65_535;
6+import { MAX_PORT_NUMBER, normalizeRunningPorts } from "@t3tools/shared/port";
7
8-export function normalizeRunningPorts(ports: number[]): number[] {
9- if (ports.length === 0) return [];
10- return [...new Set(ports)]
11- .filter((port) => Number.isInteger(port) && port > 0 && port <= MAX_PORT_NUMBER)
12- .toSorted((left, right) => left - right);
13-}
14+export { MAX_PORT_NUMBER, normalizeRunningPorts };
15
16 export function parsePidList(stdout: string): number[] {
17 const pids: number[] = [];
18
19diff --git a/apps/server/src/terminal/Layers/Manager.ts b/apps/server/src/terminal/Layers/Manager.ts
20--- a/apps/server/src/terminal/Layers/Manager.ts
21+++ b/apps/server/src/terminal/Layers/Manager.ts
22@@ -613,12 +613,17 @@
23 return cached.isWeb;
24 }
25
26- const isWeb = yield* webPortInspector(port).pipe(Effect.catch(() => Effect.succeed(false)));
27- webPortProbeCache.set(port, {
28- isWeb,
29- checkedAt: Date.now(),
30- });
31- return isWeb;
32+ const isWeb = yield* webPortInspector(port).pipe(
33+ Effect.map((result) => ({ result, probeSucceeded: true }) as const),
34+ Effect.catch(() => Effect.succeed({ result: false, probeSucceeded: false } as const)),
35+ );
36+ if (isWeb.probeSucceeded) {
37+ webPortProbeCache.set(port, {
38+ isWeb: isWeb.result,
39+ checkedAt: Date.now(),
40+ });
41+ }
42+ return isWeb.result;
43 });
44
45 const detectWebPorts = (runningPorts: number[]) =>
46
47diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx
48--- a/apps/web/src/components/Sidebar.tsx
49+++ b/apps/web/src/components/Sidebar.tsx
50@@ -61,11 +61,8 @@
51 import { isTerminalFocused } from "../lib/terminalFocus";
52 import { isLinuxPlatform, isMacPlatform, newCommandId, newProjectId } from "../lib/utils";
53 import { useStore } from "../store";
54-import {
55- normalizeRunningPorts,
56- selectThreadTerminalState,
57- useTerminalStateStore,
58-} from "../terminalStateStore";
59+import { normalizeRunningPorts, portStatusLabel } from "@t3tools/shared/port";
60+import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";
61 import { useUiStateStore } from "../uiStateStore";
62 import {
63 resolveShortcutCommand,
64@@ -219,15 +216,10 @@
65 const runningPorts = normalizeRunningPorts(
66 runningTerminalIds.flatMap((terminalId) => runningTerminalPorts[terminalId] ?? []),
67 );
68- const primaryWebPort = runningPorts[0] ?? null;
69+ const { label, primaryWebPort } = portStatusLabel(runningPorts);
70
71 return {
72- label:
73- primaryWebPort === null
74- ? "Terminal process running"
75- : runningPorts.length === 1
76- ? `Open web server: http://localhost:${primaryWebPort}`
77- : `Open web server: http://localhost:${primaryWebPort} (detected web ports: ${runningPorts.join(", ")})`,
78+ label,
79 colorClass:
80 primaryWebPort === null
81 ? "text-teal-600 dark:text-teal-300/90"
82
83diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx
84--- a/apps/web/src/components/ThreadTerminalDrawer.tsx
85+++ b/apps/web/src/components/ThreadTerminalDrawer.tsx
86@@ -33,11 +33,8 @@
87 type ThreadTerminalGroup,
88 } from "../types";
89 import { readNativeApi } from "~/nativeApi";
90-import {
91- normalizeRunningPorts,
92- selectTerminalEventEntries,
93- useTerminalStateStore,
94-} from "../terminalStateStore";
95+import { normalizeRunningPorts, portStatusLabel, type PortStatusLabel } from "@t3tools/shared/port";
96+import { selectTerminalEventEntries, useTerminalStateStore } from "../terminalStateStore";
97
98 const MIN_DRAWER_HEIGHT = 180;
99 const MAX_DRAWER_HEIGHT_RATIO = 0.75;
100@@ -79,33 +76,17 @@
101 return entries.filter((entry) => entry.id > lastAppliedTerminalEventId);
102 }
103
104-interface TerminalRuntimeStatus {
105- label: string;
106- primaryWebPort: number | null;
107-}
108-
109 function terminalRuntimeStatus(
110 terminalId: string,
111 runningTerminalIds: Set<string>,
112 runningTerminalPorts: Record<string, number[]>,
113-): TerminalRuntimeStatus | null {
114+): PortStatusLabel | null {
115 if (!runningTerminalIds.has(terminalId)) {
116 return null;
117 }
118
119 const runningPorts = normalizeRunningPorts(runningTerminalPorts[terminalId]);
120- const primaryWebPort = runningPorts[0] ?? null;
121- const label =
122- runningPorts.length === 0
123- ? "Terminal process running"
124- : runningPorts.length === 1
125- ? `Open web server: http://localhost:${primaryWebPort}`
126- : `Open web server: http://localhost:${primaryWebPort} (detected web ports: ${runningPorts.join(", ")})`;
127-
128- return {
129- label,
130- primaryWebPort,
131- };
132+ return portStatusLabel(runningPorts);
133 }
134
135 function terminalThemeFromApp(): ITheme {
136
137diff --git a/apps/web/src/terminalStateStore.ts b/apps/web/src/terminalStateStore.ts
138--- a/apps/web/src/terminalStateStore.ts
139+++ b/apps/web/src/terminalStateStore.ts
140@@ -6,6 +6,8 @@
141 */
142
143 import { ThreadId, type TerminalEvent } from "@t3tools/contracts";
144+import { normalizeRunningPorts } from "@t3tools/shared/port";
145+export { normalizeRunningPorts } from "@t3tools/shared/port";
146 import { create } from "zustand";
147 import { createJSONStorage, persist } from "zustand/middleware";
148 import { resolveStorage } from "./lib/storage";
149@@ -62,13 +64,6 @@
150 .filter((id) => id.length > 0 && validTerminalIdSet.has(id));
151 }
152
153-export function normalizeRunningPorts(ports: readonly number[] | undefined): number[] {
154- if (!ports || ports.length === 0) return [];
155- return [...new Set(ports)]
156- .filter((port) => Number.isInteger(port) && port > 0 && port <= 65_535)
157- .toSorted((left, right) => left - right);
158-}
159-
160 function normalizeRunningTerminalPorts(
161 runningTerminalPorts: Record<string, number[]> | undefined,
162 terminalIds: string[],
163
164diff --git a/packages/shared/package.json b/packages/shared/package.json
165--- a/packages/shared/package.json
166+++ b/packages/shared/package.json
167@@ -51,6 +51,10 @@
168 "./projectScripts": {
169 "types": "./src/projectScripts.ts",
170 "import": "./src/projectScripts.ts"
171+ },
172+ "./port": {
173+ "types": "./src/port.ts",
174+ "import": "./src/port.ts"
175 }
176 },
177 "scripts": {
178
179diff --git a/packages/shared/src/port.ts b/packages/shared/src/port.ts
180new file mode 100644
181--- /dev/null
182+++ b/packages/shared/src/port.ts
183@@ -1,0 +1,24 @@
184+export const MAX_PORT_NUMBER = 65_535;
185+
186+export function normalizeRunningPorts(ports: readonly number[] | undefined): number[] {
187+ if (!ports || ports.length === 0) return [];
188+ return [...new Set(ports)]
189+ .filter((port) => Number.isInteger(port) && port > 0 && port <= MAX_PORT_NUMBER)
190+ .toSorted((left, right) => left - right);
191+}
192+
193+export interface PortStatusLabel {
194+ label: string;
195+ primaryWebPort: number | null;
196+}
197+
198+export function portStatusLabel(runningPorts: readonly number[]): PortStatusLabel {
199+ const primaryWebPort = runningPorts[0] ?? null;
200+ const label =
201+ runningPorts.length === 0
202+ ? "Terminal process running"
203+ : runningPorts.length === 1
204+ ? `Open web server: http://localhost:${primaryWebPort}`
205+ : `Open web server: http://localhost:${primaryWebPort} (detected web ports: ${runningPorts.join(", ")})`;
206+ return { label, primaryWebPort };
207+}

You can send follow-ups to the cloud agent here.

apps/server/src/terminal/Layers/Manager.tsline 622Outdated · da81a16Open thread
……@@ -695,6 +599,50 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
619+ checkedAt: Date.now(),
620+ });
621+ return isWeb;
622+ });
CcursorBoton line 622Apr 5, 2026

Negative web port probe results are cached for TTL

Low Severity

inspectWebPortCached catches all webPortInspector failures and caches the result as isWeb: false for the full TTL (default 10 seconds). If a web server is detected as listening but its first HTTP probe fails transiently (e.g., brief startup race, temporary overload), the port won't be classified as a web port until the cache entry expires — even though subsequent probes would succeed.

Reviewed by Cursor Bugbot for commit da81a167eac8b111b129cc7ed989b0b7c8f62dde. Configure here.

apps/web/src/terminalStateStore.tsline 70Outdated · da81a16Open thread
……@@ -61,6 +62,30 @@ function normalizeRunningTerminalIds(
67+ return [...new Set(ports)]
68+ .filter((port) => Number.isInteger(port) && port > 0 && port <= 65_535)
69+ .toSorted((left, right) => left - right);
70+}
CcursorBoton line 70Apr 5, 2026

Duplicated normalizeRunningPorts across server and web packages

Low Severity

normalizeRunningPorts is implemented twice with nearly identical logic — once in apps/server/src/process/utils.ts and again in apps/web/src/terminalStateStore.ts. Both deduplicate, filter invalid ports, and sort. Since the @t3tools/contracts package already defines TerminalPortSchema with the same validation range (1–65535), this shared utility belongs there to avoid divergence over time.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit da81a167eac8b111b129cc7ed989b0b7c8f62dde. Configure here.

apps/web/src/components/ThreadTerminalDrawer.tsxline 109Outdated · da81a16Open thread
……@@ -74,6 +79,35 @@ export function selectPendingTerminalEventEntries(
106+ label,
107+ primaryWebPort,
108+ };
109+}
CcursorBoton line 109Apr 5, 2026

Duplicated port label construction in sidebar and drawer

Low Severity

The port label format string logic (including the ternary for single vs. multiple ports and the http://localhost:${port} template) is duplicated across terminalRuntimeStatus in ThreadTerminalDrawer.tsx and terminalStatusFromTerminalState in Sidebar.tsx. If the label format needs to change, both locations must be updated in lockstep.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit da81a167eac8b111b129cc7ed989b0b7c8f62dde. Configure here.

Added 3 commits

  • Refine terminal subprocess inspection and status tooltips07252e5
  • Merge origin/main into codething/50e6e9da037a771
  • Refactor process inspector control flowb0bc101
……@@ -1281,6 +1336,59 @@ export default function ThreadTerminalDrawer({
1391+ ))}
12841392 {normalizedTerminalIds.length > 1 && (
12851393 <Popover>
12861394 <PopoverTrigger
MmacroscopeappBoton line 1394Apr 17, 2026

🟢 Low components/ThreadTerminalDrawer.tsx:1394

The close terminal button's XIcon is placed outside the render prop's button element, inside PopoverTrigger instead. This renders an empty button with no visible icon. Move <XIcon className="size-2.5" /> inside the button element in the render prop, following the pattern used for the Globe and TerminalSquare icons on lines 1369-1378 and 1344-1351.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file apps/web/src/components/ThreadTerminalDrawer.tsx around line 1394:
2
3The close terminal button's `XIcon` is placed outside the `render` prop's button element, inside `PopoverTrigger` instead. This renders an empty button with no visible icon. Move `<XIcon className="size-2.5" />` inside the button element in the `render` prop, following the pattern used for the Globe and TerminalSquare icons on lines 1369-1378 and 1344-1351.
4
5Evidence trail:
6apps/web/src/components/ThreadTerminalDrawer.tsx lines 1394-1407 (REVIEWED_COMMIT): XIcon is a child of PopoverTrigger, but the button in the render prop is self-closing with no children.
7
8Compare with lines 1369-1378 (Globe pattern): Globe icon is inside the button element in the render prop.
9
10Compare with lines 1344-1351 (TerminalSquare pattern): TerminalSquare icon is inside the span element in the render prop.
CcursorBotreviewed · 2 inline threadsApr 17, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Sidebar web port button shows wrong icon
    • Imported Globe from lucide-react and replaced TerminalIcon with Globe in the web-port button branch of the sidebar.
  • ✅ Fixed: Unreachable fallback in web port error propagation
    • Removed the always-true ipv6Result._tag === "Failure" branch and its dead code fallback, now directly propagating the IPv4 error which is tried first and more commonly relevant.

Create PR

Or push these changes by commenting:

1@cursor push 5d70913a22
Preview (pingdotgg/t3code@5d70913a22)
diff
1diff --git a/apps/server/src/process/Layers/WebPortInspector.ts b/apps/server/src/process/Layers/WebPortInspector.ts
2--- a/apps/server/src/process/Layers/WebPortInspector.ts
3+++ b/apps/server/src/process/Layers/WebPortInspector.ts
4@@ -164,10 +164,6 @@
5 return false;
6 }
7
8- if (ipv6Result._tag === "Failure") {
9- return yield* Effect.failCause(ipv6Result.cause);
10- }
11-
12 return yield* Effect.failCause(ipv4Result.cause);
13 });
14
15
16diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx
17--- a/apps/web/src/components/Sidebar.tsx
18+++ b/apps/web/src/components/Sidebar.tsx
19@@ -8,6 +8,7 @@
20 SearchIcon,
21 SettingsIcon,
22 SquarePenIcon,
23+ Globe,
24 TerminalIcon,
25 TriangleAlertIcon,
26 } from "lucide-react";
27@@ -674,7 +675,7 @@
28 className={`inline-flex cursor-pointer items-center justify-center rounded-sm ${terminalStatus.colorClass} outline-hidden transition-colors hover:bg-accent hover:text-sky-700 focus-visible:ring-1 focus-visible:ring-ring dark:hover:text-sky-200`}
29 onClick={handleTerminalStatusClick}
30 >
31- <TerminalIcon className={`size-3 ${terminalStatus.pulse ? "animate-pulse" : ""}`} />
32+ <Globe className={`size-3 ${terminalStatus.pulse ? "animate-pulse" : ""}`} />
33 </button>
34 ))}
35 <div className="flex min-w-12 justify-end">

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit b0bc101eb6d4cf43c9211e77c5b21ba44eec9044. Configure here.

……@@ -0,0 +1,179 @@
168+ return yield* Effect.failCause(ipv6Result.cause);
169+ }
170+
171+ return yield* Effect.failCause(ipv4Result.cause);
CcursorBoton line 171Apr 17, 2026

Unreachable fallback in web port error propagation

Low Severity

After the check at line 163, both ipv4Result and ipv6Result must be failures, so ipv6Result._tag === "Failure" at line 167 is always true and the final return yield* Effect.failCause(ipv4Result.cause) at line 171 is dead code. This also means the IPv6 error is always propagated even though IPv4 was tried first, which is unintuitive.

Reviewed by Cursor Bugbot for commit b0bc101eb6d4cf43c9211e77c5b21ba44eec9044. Configure here.

……@@ -606,16 +656,27 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
674+ className={`inline-flex cursor-pointer items-center justify-center rounded-sm ${terminalStatus.colorClass} outline-hidden transition-colors hover:bg-accent hover:text-sky-700 focus-visible:ring-1 focus-visible:ring-ring dark:hover:text-sky-200`}
675+ onClick={handleTerminalStatusClick}
676+ >
677+ <TerminalIcon className={`size-3 ${terminalStatus.pulse ? "animate-pulse" : ""}`} />
CcursorBoton line 677Apr 17, 2026

Sidebar web port button shows wrong icon

Medium Severity

The sidebar's web-port button renders TerminalIcon instead of Globe. The ThreadTerminalDrawer correctly imports and uses Globe for the web-port case, and the sidebar already has all the conditional rendering infrastructure (different color class, clickable button, handleTerminalStatusClick), but the icon inside the button branch was never changed. Globe is not imported in Sidebar.tsx at all.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit b0bc101eb6d4cf43c9211e77c5b21ba44eec9044. Configure here.