Skip to content

fix(preview): open Portless dev server URLs - #6514

Open
jakeleventhal wants to merge 10 commits into
pingdotgg:mainfrom
jakeleventhal:t3code/detect-portless-dev-servers
Open

fix(preview): open Portless dev server URLs#6514
jakeleventhal wants to merge 10 commits into
pingdotgg:mainfrom
jakeleventhal:t3code/detect-portless-dev-servers

Conversation

@jakeleventhal

@jakeleventhal jakeleventhal commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Demo

Screenshot 2026-08-13 at 3 25 10 PM Screenshot 2026-08-13 at 3 25 13 PM

Problem

T3 Code only exposed the underlying localhost listener for Portless-managed dev servers, so its sidebar and browser opened a transient localhost port instead of the stable named HTTPS URL.

What changed

  • read and validate Portless route and proxy state while preserving the detected host and port
  • prefer the named Portless route when a terminal exposes multiple listeners, without showing an extra server count
  • open Portless URLs directly for local environments while preserving target-port fallback for remote environments
  • apply the behavior across the current sidebar, legacy sidebar, and preview surfaces

Validation

  • vp test run apps/server/src/preview/PortScanner.test.ts apps/web/src/browser/browserTargetResolver.test.ts apps/web/src/components/preview/useDiscoveredLocalServers.test.ts (35 tests passed)
  • vp run --filter t3 typecheck
  • vp run --filter @t3tools/web typecheck
  • verified in T3 Code desktop against https://artelo.localhost

Built with GPT-5.6 in the Codex harness.

Note

Add Portless and ngrok tunnel URL support to preview server discovery

  • PortScanner reads Portless routes.json state files and queries the ngrok agent API to enrich discovered local servers with named or public-tunnel URLs, carrying a new urlKind field on DiscoveredLocalServer
  • resolveDiscoveredServerUrl maps local-proxy URLs to the remote environment's listener port (preserving path/query/hash) while public-tunnel URLs open directly
  • Sidebar globe buttons and preview cards now display and open the preferred server in order: public tunnel, local-proxy, then bare listener
  • COMMON_DEV_PORTS extended to include port 4040; ngrok agent API ports are excluded from web probing
  • Risk: serversEqual now compares urlKind, so a urlKind change on an existing listener triggers an update notification; out-of-tree consumers of DiscoveredLocalServer must handle the new optional urlKind field

Macroscope summarized fdf6eb5.


Note

Medium Risk
Changes how discovered dev URLs are resolved and opened across local vs remote environments, and adds filesystem reads and HTTP calls to the ngrok agent during port scans.

Overview
Port discovery now enriches HTTP listeners with Portless routes (from routes.json / proxy state) and ngrok tunnels (via the agent api/tunnels on port 4040), tagging results with optional urlKind (local-proxy | public-tunnel) on DiscoveredLocalServer. Ngrok agent ports are skipped for normal web probing so a non-ngrok server on 4040 still works.

Opening previews passes listener port and urlKind through resolveDiscoveredServerUrl: Portless named URLs open directly on a local loopback environment, but on remote environments map to the environment host on the underlying port; ngrok public URLs open directly everywhere. Sidebar globe buttons, preview empty-state cards, and recents use selectPreferredDiscoveredServer (ngrok → Portless → first listener) and formatDiscoveredServerHost for labels instead of raw localhost:port.

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

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

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 Plus

Run ID: f2e51eb6-349d-40d0-98ed-37042288b612

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

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.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 13, 2026
Comment thread apps/server/src/preview/PortScanner.ts Outdated

@macroscopeapp macroscopeapp Bot left a comment

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.

Reviewed the Effect service changes in apps/server/src/preview/PortScanner.ts (plus its test) and the web-side helpers. One finding: the new Portless state lookup reads host state outside the Effect environment.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/preview/PortScanner.ts Outdated
@jakeleventhal
jakeleventhal marked this pull request as ready for review August 13, 2026 19:55
Comment thread apps/web/src/components/preview/useDiscoveredLocalServers.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This change adds a cross-cutting preview capability: production port scanning now reads Portless state and ngrok APIs, rewrites discovered URLs, and adds new sidebar and browser behavior. Its external integrations and unresolved cases involving remote history, stale proxies, and terminal attribution warrant focused human review.

Not approved because:

  • 2 blocking correctness issues found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

Comment thread apps/web/src/components/preview/PreviewView.tsx Outdated
Comment thread apps/web/src/components/preview/previewEmptyStateLogic.ts
Comment thread apps/web/src/components/preview/PreviewEmptyState.tsx Outdated
Comment thread apps/web/src/components/preview/previewEmptyStateLogic.ts
@t3dotgg t3dotgg added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. and removed vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Aug 24, 2026
@jakeleventhal
jakeleventhal force-pushed the t3code/detect-portless-dev-servers branch from 424b513 to 58040a5 Compare August 26, 2026 02:52
Comment thread apps/server/src/preview/PortScanner.ts
Comment thread apps/web/src/components/preview/useDiscoveredLocalServers.ts Outdated

@macroscopeapp macroscopeapp Bot left a comment

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.

One UI consistency finding on the discovered-server label. Everything else in the changed web UI (new sidebar globe action, PreviewEmptyState/PreviewView target-port threading, openDiscoveredPort) follows the surrounding patterns: raw buttons are wrapped in TooltipTrigger render exactly like the neighboring pin/settle actions, keep cursor-pointer, focus-visible rings and aria-label, and every environment-scoped action resolves its target from the bound thread's environmentId rather than the active environment.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/preview/PreviewLocalServerCard.tsx Outdated

@macroscopeapp macroscopeapp Bot left a comment

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.

One consistency issue found in the preview local-server card label. The new sidebar globe button matches the surrounding row conventions (pinIndicator, terminalStatusIcon) for geometry, focus ring, and aria labeling, so no primitive-migration findings there.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/preview/PreviewLocalServerCard.tsx Outdated
Comment thread apps/server/src/preview/PortScanner.ts
Comment thread apps/web/src/components/preview/PreviewLocalServerCard.tsx Outdated
Comment thread apps/web/src/browser/browserTargetResolver.ts
@jakeleventhal
jakeleventhal force-pushed the t3code/detect-portless-dev-servers branch from 51572d5 to 27d303b Compare August 26, 2026 03:13
@jakeleventhal
jakeleventhal force-pushed the t3code/detect-portless-dev-servers branch from 27d303b to 1fb6bb3 Compare August 26, 2026 03:20
normalizedUrl,
).resolvedUrl;
}
}

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.

Remote Portless history skips remap

Medium Severity

Remote remap for Portless URLs only runs when both targetPort and a local-proxy classification are present. Preview visits now record the named Portless URL, and the empty-state recent list only supplies targetPort/urlKind when a live discovered match exists. Without that match, resolveDiscoveredServerUrl opens the .localhost URL directly on the client instead of the remote listener, so recent Portless entries fail on remote environments whenever the server is not currently listed.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1fb6bb3. Configure here.

@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 26, 2026
): ReadonlyArray<DiscoveredLocalServer> => {
const visibleByServer = new Map<string, DiscoveredLocalServer>();
const namedRouteByServer = new Map<string, DiscoveredLocalServer>();
for (const server of snapshot.discovered) {

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.

🟡 Medium preview/PortScanner.ts:303

A successful configured URL remains on localhost instead of using its Portless named proxy URL when the listener root is not HTML. namedRouteByServer is built only from snapshot.discovered, so the configured entry is missing route metadata and takes the !namedRoute branch; preserve or derive the named route for successful configured entries as well.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/preview/PortScanner.ts around line 303:

A successful configured URL remains on `localhost` instead of using its Portless named proxy URL when the listener root is not HTML. `namedRouteByServer` is built only from `snapshot.discovered`, so the configured entry is missing route metadata and takes the `!namedRoute` branch; preserve or derive the named route for successful configured entries as well.

.exists(path.join(stateDir, "proxy.tls"))
.pipe(Effect.orElseSucceed(() => false));

return parsePortlessRouteSnapshot({

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.

🟠 High preview/PortScanner.ts:479

When the Portless proxy has died but its managed route PID is still alive, readPortlessRoutes accepts the stale route and applyNamedRoutes overwrites the working localhost URL with it, so the discovered server becomes unreachable. Verify that the Portless proxy is still listening before applying routes (or discard routes when it is not).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/preview/PortScanner.ts around line 479:

When the Portless proxy has died but its managed route PID is still alive, `readPortlessRoutes` accepts the stale route and `applyNamedRoutes` overwrites the working localhost URL with it, so the discovered server becomes unreachable. Verify that the Portless proxy is still listening before applying routes (or discard routes when it is not).

@cursor cursor Bot left a comment

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.

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

There are 2 total unresolved issues (including 1 from previous review).

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 fdf6eb5. Configure here.

routes.set(
targetPort,
response.terminal === null ? route : { ...route, terminal: response.terminal },
);

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.

Ngrok steals app terminal

Medium Severity

readNgrokRoutes attaches the ngrok agent’s terminal to each tunnel target, and applyNamedRoutes spreads that over the app listener. When the app and ngrok live in different threads, useThreadDiscoveredPorts reattributes the enriched server to the ngrok thread, so the sidebar globe leaves the thread that owns the app.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fdf6eb5. Configure here.

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

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants