Skip to content

feat(web): detect agent-spawned local servers and surface in sidebar#2241

Open
Marve10s wants to merge 11 commits into
pingdotgg:mainfrom
Marve10s:feat/agent-server-status-overlay
Open

feat(web): detect agent-spawned local servers and surface in sidebar#2241
Marve10s wants to merge 11 commits into
pingdotgg:mainfrom
Marve10s:feat/agent-server-status-overlay

Conversation

@Marve10s
Copy link
Copy Markdown
Contributor

@Marve10s Marve10s commented Apr 20, 2026

Closes #1298

Summary

End-to-end visibility into localhost servers that coding agents start through any tool — terminal pty, Bash background tasks, Monitor tail, etc. The sidebar shows a pulsing terminal icon when a process is live, and clicking it opens a redesigned dialog with per-port Open and Stop controls.

Why

t3code's sidebar previously only tracked subprocesses spawned through its own pty. When an agent backgrounds pnpm dev via Bash run_in_background: true and tails it with Monitor, the dev server is real and listening — but the sidebar was blind to it. This PR closes that gap.

What changed

Server

  • New localProcesses.probePorts RPC + LocalProcessProbePorts* contracts. Reuses existing lsof / Get-NetTCPConnection to report listener state without killing.
  • ProviderRuntimeIngestion buffers command_output deltas per (thread, turn, item) and attaches a commandActivity payload (with parsed localhost URLs) to tool.updated / tool.completed. Shared toolActivity helpers extract the URLs.

Web

  • deriveSidebarAgentCommandStatus scans every tool.completed activity on the latest turn so URLs surfaced via Monitor/tail/log tools are captured (not just command_execution).
  • New useListeningPortProbe(environmentId, ports) hook: ref-counts probe requests per environment and polls every 5s. The sidebar only renders the live icon when a port is actually listening.
  • AgentCommandStatusIcon (sidebar): emerald + pulsing when live; clicking opens the dialog.
  • Dialog redesign:
    • Subtle backdrop matching the command palette (bg-background/60).
    • Single URL list with per-row Open + Stop (no more "Stop all").
    • "Also listening" strip for orphan ports gets per-port Stop.
    • DialogPopup gains forceBackdrop so the backdrop renders even when Base UI considers this dialog nested under the sidebar's menu stack.
  • Work-log URL chip restyled to a neutral pill with a small sky pulse and arrow icon (was the bright emerald pill).
  • Dismiss is persisted per thread/status; suppression is bypassed while a process is still observed live.

Note on diff size

The branch is currently based on feat/checkout-dirty-worktree-error-handling (open as #1785) for local context, so this PR's diff includes those commits until #1785 lands. The 30 files specific to this work are listed above; happy to rebase onto main once #1785 merges, or sooner if preferred.


Note

Medium Risk
Adds new WebSocket RPCs that execute lsof/PowerShell and send SIGTERM to listening PIDs, plus client-side polling every 5s; mistakes could impact performance or terminate unintended local processes (mitigated by PID/port validation and refusing to kill the current server PID).

Overview
Detects and surfaces agent-started localhost servers end-to-end, including those started outside the built-in terminal, by extracting localhost URLs from tool/command output and showing a live status indicator in the sidebar.

On the server, introduces a localProcesses module (probe + stop) backed by lsof/PowerShell with new localProcess.probePorts/localProcess.stopPorts WS RPCs and contracts, and enhances runtime ingestion to buffer command_output deltas (capped) and attach a normalized commandActivity (command, output preview, extracted localhost URLs) to relevant tool lifecycle activities.

On the web app, adds agentCommandStatus to SidebarThreadSummary, derives it from latest-turn activities, polls candidate ports via a shared useListeningPortProbe store, and updates the sidebar/timeline UI with an interactive status icon + dialog to open URLs and stop listeners; dismissal of stale badges is persisted in UI state and bypassed while a process is still observed live.

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

Note

Detect agent-spawned local servers and surface them in the sidebar with controls to probe and stop ports

  • Adds a SidebarAgentCommandStatus derived from tool activity in the latest turn, capturing detected localhost URLs and command context. Sidebar thread rows show an AgentCommandStatusIcon and open an AgentCommandStatusDialog for inspecting and stopping detected processes.
  • Introduces useListeningPortProbe, a React hook that periodically polls localProcesses.probePorts to check whether candidate ports are actively listening, suppressing stale dismissed statuses when no live process is found.
  • Adds probeLocalPorts and stopLocalPorts server utilities (lsof on Unix, PowerShell on Windows) exposed as WebSocket RPC methods and wired through to EnvironmentApi.localProcesses.
  • Extends normalizeCommandActivityPayload in the shared toolActivity package to extract localhost URLs, output previews, and unwrapped shell commands from a broader set of tool completions.
  • Buffers up to 32 KB of command_output deltas per item in the provider ingestion layer and attaches them to tool.completed activities for URL extraction.
  • Persists per-thread dismissed agent command status in uiStateStore so dismissed banners are not re-shown unless a live subprocess is detected.
  • Risk: port probing adds periodic network round-trips from the web app to the local server; polling stops only when all components unsubscribe.

Macroscope summarized e022a8e.

Refs #216

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c53d7f02-e309-4d38-bbfe-21ae162f8173

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Apr 20, 2026
Comment thread apps/web/src/store.ts
Comment thread apps/web/src/store.ts
Comment thread apps/server/src/localProcesses.ts
Comment thread apps/web/src/store.ts
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp Bot commented Apr 20, 2026

Approvability

Verdict: Needs human review

This PR introduces a new feature with significant scope: local server detection, process control capabilities (including process termination), new RPC endpoints, UI components, and polling infrastructure. New features of this scale with process-killing capabilities warrant human review.

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

@Marve10s
Copy link
Copy Markdown
Contributor Author

2026-04-20.20.27.24.mov

@Marve10s
Copy link
Copy Markdown
Contributor Author

2026-04-20.20.27.24.mov

@juliusmarminge There are a lot of use cases when I can't understand if Agent run the server simply, without checking all commands and tool calls. I often ask them to do since it's easier to run it like this than run multiple different worktrees and codebases manually

@juliusmarminge
Copy link
Copy Markdown
Member

interesting solution. I've played with this long time ago (#43) but never found a reliable way to identify the ports 😭

@Marve10s
Copy link
Copy Markdown
Contributor Author

interesting solution. I've played with this long time ago (#43) but never found a reliable way to identify the ports 😭

Oh, I never saw that PR. I'm resolving the conflicts and comments from AI reviewers. Can take a look at PR 43, might be useful or close that, idk.

Do you think you'd have time to check my other PRs ? There are a lot of UX improvements and Gemini CLI that was merged to DPcode. Seems like working fine

@Marve10s Marve10s force-pushed the feat/agent-server-status-overlay branch 2 times, most recently from da93f3e to 63a32c6 Compare April 20, 2026 18:57
Comment thread apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Comment thread apps/web/src/components/Sidebar.tsx
@Marve10s Marve10s force-pushed the feat/agent-server-status-overlay branch from 63a32c6 to 7875a07 Compare April 20, 2026 19:11
Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment thread packages/shared/src/toolActivity.ts
Comment thread apps/web/src/session-logic.ts Outdated
Comment thread apps/web/src/types.ts
Comment thread apps/web/src/store.ts Outdated
Comment thread apps/server/src/localProcesses.ts Outdated
Adds end-to-end visibility into localhost servers that coding agents
start through any tool (terminal pty, Bash background tasks, Monitor
tail, etc). The sidebar now shows a pulsing terminal icon when a
process is live, and clicking it opens a redesigned dialog listing
detected URLs with per-port Open/Stop controls.

Server
- New `localProcesses.probePorts` RPC and `LocalProcessProbePorts*`
  contracts that reuse the existing `lsof` / `Get-NetTCPConnection`
  infrastructure to report listener state without killing anything.
- ProviderRuntimeIngestion now buffers `command_output` deltas per
  thread/turn/item and attaches a `commandActivity` payload (with
  parsed localhost URLs) to `tool.updated` and `tool.completed`
  activities. Shared `toolActivity` helpers extract the URLs.

Web
- `deriveSidebarAgentCommandStatus` scans every `tool.completed`
  activity on the latest turn so URLs surfaced via Monitor/tail/log
  tools are captured (not just `command_execution`).
- New `useListeningPortProbe` hook ref-counts probe requests per
  environment and polls `localProcesses.probePorts` every 5s, so
  rows only render the live icon when a port is actually listening.
- `AgentCommandStatusIcon` (sidebar) emerald + pulse when live; click
  opens the redesigned dialog.
- Dialog redesign: subtle backdrop matching the command palette,
  single URL list with per-row Open + Stop, "Also listening" strip
  for orphan ports. Stops are scoped to a single port so an agent
  running multiple servers can have one killed at a time.
- `DialogPopup` gains `forceBackdrop` to bypass Base UI's nested-
  dialog backdrop suppression when opened from the sidebar stack.
- Work-log URL chip restyled to neutral pill with sky pulse + arrow.
- Dismiss state persisted per thread/status; suppression is bypassed
  while a process is still observed live.
Address three Cursor reviewer comments on 7875a07:

- ChatView: pass `activeLatestTurn?.turnId` into `deriveWorkLogEntries`
  again. The `undefined` slipped in during rebase resolution and broadened
  the chat work log across every turn instead of the active one.
- session-logic: run `extractToolCommandActivity` for non-command tools
  so the timeline picks up localhost URLs and `outputPreview` from
  Monitor/tail tools, matching what `deriveSidebarAgentCommandStatus`
  already does. `commandPreview` stays narrow (via `extractToolCommand`)
  for non-command tools so details like `/tmp/app.ts` aren't inferred as
  commands.
- toolActivity: normalize `127.0.0.1` and `[::1]` (along with `0.0.0.0`)
  to `localhost` for href-based dedup so the same server printed under
  multiple loopback hosts collapses to a single chip. The visible `url`
  preserves the original spelling.
`deriveSidebarAgentCommandStatus` only emits a status when at least one
localhost URL is detected, so the only label it ever returns is "Agent
local URL detected" — the "Agent ran command" union member and the
matching runtime fallbacks were unreachable.

- types: narrow `SidebarAgentCommandStatus.label` to the literal that's
  actually produced.
- Sidebar dialog title: collapse the dead `?? "Agent ran command"`
  fallback; pick the title from `isRunning` only.
- ThreadStatusIndicators tooltip: drop the unreachable
  `!isRunning && !hasLocalUrl` branch.
…tick

`writeShellStreamThread` compared the stored sidebar summary (which has
a non-null computed `agentCommandStatus`) against `nextThread.summary`
straight off the shell stream — but the server always sets
`agentCommandStatus: null`. For any thread with an active URL detection
the equality check could never succeed, so the block ran on every shell
tick: it rebuilt the summary, recomputed the same status, and stored a
new object reference even though nothing changed. That cascaded into
unnecessary Zustand notifications and sidebar re-renders.

Compute the desired summary first (via `withSidebarAgentCommandStatus`)
and compare that to the stored one. When activities haven't changed the
new check returns true and the no-op write is skipped, breaking the loop
without changing the contract that activities own `agentCommandStatus`
and `withSidebarAgentCommandStatus` derives it.
@Marve10s Marve10s force-pushed the feat/agent-server-status-overlay branch from 70e4d57 to 4869630 Compare April 29, 2026 17:38
Comment thread packages/shared/src/toolActivity.ts
Comment thread packages/shared/src/toolActivity.ts
Comment thread apps/web/src/store.ts Outdated
Comment thread apps/web/src/lib/portProbeState.ts
Comment thread apps/web/src/store.ts Outdated
Comment thread apps/web/src/store.ts Outdated
Comment thread apps/web/src/components/Sidebar.tsx Outdated
Comment thread apps/web/src/components/Sidebar.tsx
Comment thread apps/web/src/session-logic.ts
}
return /(?:^|\s)(?:(?:bun|npm|pnpm|yarn|npx|node|deno|python|python3|ruby|go|cargo|make|cmake|docker|git|bash|sh|zsh|uv|tsx|ts-node|turbo|vite|next|astro|remix)\b|\.\/)[\s\w./:=@-]*/iu.test(
trimmed,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Regex matches common English words as executable names

Low Severity

The isSafeCommandFallback regex includes short executable names like go, make, next, and sh that are also common English words. Because the pattern only requires a word boundary (\b) after the match, natural-language tool details such as "The next step is to refactor" or "Just go ahead and deploy" would pass the check and be incorrectly extracted as commands. This is used as a last-resort fallback when no structured command data exists, so the blast radius is limited, but it can produce misleading command labels in the work log timeline.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 872a84e. Configure here.

const primaryUrl = urls[0];
if (!newestLocalUrlCommand || !primaryUrl) {
return null;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Redundant guard after filtering guarantees non-empty result

Low Severity

The null check if (!newestLocalUrlCommand || !primaryUrl) on the results of completedCommands.at(-1) and urls[0] is redundant — the early return at line 546 already guarantees completedCommands is non-empty and every entry has at least one URL with a truthy href (per the filter at line 555). The guard silently masks future regressions where the dedup filter might accidentally discard all URLs, returning null instead of surfacing the underlying inconsistency.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 872a84e. Configure here.

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

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

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

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e022a8e. Configure here.

}
}
return listening;
}, [environmentId, portsKey, statusMap]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Port probe creates new Set on every tick causing rerenders

Medium Severity

The useListeningPortProbe hook's final useMemo produces a new Set reference every time statusMap changes, which happens on every 5-second probe tick for the environment — even when the specific ports haven't changed state. Because this hook runs inside the memoized SidebarThreadRow, every thread row with candidate ports re-renders every 5 seconds. The applyProbe store action replaces the entire environment's PortStatusMap object reference (via { ...previous }) whenever any port in that environment changes, so all subscribers re-render.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e022a8e. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Tracking running server URLs per worktree

2 participants