feat: Hosted Frontend, Tailscale Integration & SSH Lancher#2361
feat: Hosted Frontend, Tailscale Integration & SSH Lancher#2361juliusmarminge wants to merge 47 commits intomainfrom
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Hosted pairing URL fallback to desktop URL is unreachable
- Added isHostedPairingCompatible check so resolveHostedPairingUrl returns null for non-HTTPS endpoints, allowing the ?? operator to correctly fall through to resolveDesktopPairingUrl for HTTP LAN backends.
Or push these changes by commenting:
@cursor push 428913d5d5
Preview (428913d5d5)
diff --git a/apps/web/src/components/settings/ConnectionsSettings.tsx b/apps/web/src/components/settings/ConnectionsSettings.tsx
--- a/apps/web/src/components/settings/ConnectionsSettings.tsx
+++ b/apps/web/src/components/settings/ConnectionsSettings.tsx
@@ -250,7 +250,18 @@
return setPairingTokenOnUrl(url, credential).toString();
}
-function resolveHostedPairingUrl(endpointUrl: string, credential: string): string {
+function isHostedPairingCompatible(endpointUrl: string): boolean {
+ try {
+ return new URL(endpointUrl).protocol === "https:";
+ } catch {
+ return false;
+ }
+}
+
+function resolveHostedPairingUrl(endpointUrl: string, credential: string): string | null {
+ if (!isHostedPairingCompatible(endpointUrl)) {
+ return null;
+ }
return buildHostedPairingUrl({
host: endpointUrl,
token: credential,You can send follow-ups to the cloud agent here.
ApprovabilityVerdict: Needs human review 2 blocking correctness issues found. Diff is too large for automated approval analysis. A human reviewer should evaluate this PR. You can customize Macroscope's approvability policy. Learn more. |
Co-authored-by: codex <codex@users.noreply.github.com>
34102d3 to
814c206
Compare
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Cached no-RPC LocalApi causes failures in newly-accessible routes
- Moved the fire-and-forget
server.updateSettingsRPC call inside the existingif (currentServerConfig)guard so it is only attempted when a server backend is actually connected, preventing unhandled rejections in hosted-static mode.
- Moved the fire-and-forget
Or push these changes by commenting:
@cursor push f485d6b4ba
Preview (f485d6b4ba)
diff --git a/apps/web/src/hooks/useSettings.ts b/apps/web/src/hooks/useSettings.ts
--- a/apps/web/src/hooks/useSettings.ts
+++ b/apps/web/src/hooks/useSettings.ts
@@ -165,9 +165,9 @@
const currentServerConfig = getServerConfig();
if (currentServerConfig) {
applySettingsUpdated(applyServerSettingsPatch(currentServerConfig.settings, serverPatch));
+ // Fire-and-forget RPC — push will reconcile on success
+ void ensureLocalApi().server.updateSettings(serverPatch);
}
- // Fire-and-forget RPC — push will reconcile on success
- void ensureLocalApi().server.updateSettings(serverPatch);
}
if (Object.keys(clientPatch).length > 0) {You can send follow-ups to the cloud agent here.
|
Bugbot Autofix prepared a fix for the issue found in the latest run.
Or push these changes by commenting: Preview (519c3e126d)diff --git a/apps/web/src/environments/primary/index.ts b/apps/web/src/environments/primary/index.ts
--- a/apps/web/src/environments/primary/index.ts
+++ b/apps/web/src/environments/primary/index.ts
@@ -32,4 +32,8 @@
__resetServerAuthBootstrapForTests,
} from "./auth";
-export { resolvePrimaryEnvironmentHttpUrl, isLoopbackHostname } from "./target";
+export {
+ resolvePrimaryEnvironmentHttpUrl,
+ isLoopbackHostname,
+ readPrimaryEnvironmentTarget,
+} from "./target";
diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx
--- a/apps/web/src/routes/__root.tsx
+++ b/apps/web/src/routes/__root.tsx
@@ -55,6 +55,7 @@
import {
ensurePrimaryEnvironmentReady,
getPrimaryKnownEnvironment,
+ readPrimaryEnvironmentTarget,
resolveInitialServerAuthGateState,
updatePrimaryEnvironmentDescriptor,
} from "../environments/primary";
@@ -81,7 +82,8 @@
authGateState,
};
} catch (error) {
- if (location.pathname === "/pair") {
+ const primaryTarget = readPrimaryEnvironmentTarget();
+ if (location.pathname === "/pair" || primaryTarget?.source !== "window-origin") {
throw error;
}You can send follow-ups to the cloud agent here. |
Co-authored-by: codex <codex@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: "Try again" button regresses paired state to error
- Wrapped the 'Try again' button in a
status !== "paired"conditional so it is hidden after successful pairing, preventing re-invocation ofsubmitHostedPairingRequestwhich would throw on the already-connected environment.
- Wrapped the 'Try again' button in a
Or push these changes by commenting:
@cursor push 9d19673665
Preview (9d19673665)
diff --git a/apps/web/src/components/auth/PairingRouteSurface.tsx b/apps/web/src/components/auth/PairingRouteSurface.tsx
--- a/apps/web/src/components/auth/PairingRouteSurface.tsx
+++ b/apps/web/src/components/auth/PairingRouteSurface.tsx
@@ -240,13 +240,15 @@
) : null}
<div className="mt-6 flex flex-wrap gap-2">
- <Button
- disabled={status === "pairing"}
- size="sm"
- onClick={() => void submitHostedPairingRequest()}
- >
- {status === "pairing" ? "Pairing..." : "Try again"}
- </Button>
+ {status !== "paired" ? (
+ <Button
+ disabled={status === "pairing"}
+ size="sm"
+ onClick={() => void submitHostedPairingRequest()}
+ >
+ {status === "pairing" ? "Pairing..." : "Try again"}
+ </Button>
+ ) : null}
{status === "paired" ? (
<Button size="sm" variant="outline" onClick={() => (window.location.href = "/")}>
Open appYou can send follow-ups to the cloud agent here.
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
- Discover SSH hosts and persist SSH targets - Bootstrap tunneled SSH sessions with desktop password prompts - Extend IPC and storage tests for SSH metadata
- Validate SSH targets and known-host parsing more strictly - Retry desktop SSH session refresh on auth failures - Preserve saved registry state when bearer persistence fails
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
- Ignore invalid custom HTTPS endpoint URLs when building desktop exposure - Reset pairing submission state after backend errors and clarify retry guidance - Add coverage for malformed endpoint inputs
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Misleading "Custom HTTPS" label for non-HTTPS endpoints
- Made the label and description conditional on isHttpsEndpointUrl: HTTPS URLs get "Custom HTTPS" / "HTTPS endpoint" text, while non-HTTPS URLs get "Custom endpoint" / generic description.
Or push these changes by commenting:
@cursor push 72d1449f64
Preview (72d1449f64)
diff --git a/apps/desktop/src/serverExposure.test.ts b/apps/desktop/src/serverExposure.test.ts
--- a/apps/desktop/src/serverExposure.test.ts
+++ b/apps/desktop/src/serverExposure.test.ts
@@ -171,7 +171,7 @@
},
{
id: "manual:http://desktop.example.test:3773",
- label: "Custom HTTPS",
+ label: "Custom endpoint",
provider: {
id: "manual",
label: "Manual",
@@ -187,7 +187,7 @@
},
source: "user",
status: "unknown",
- description: "User-configured HTTPS endpoint for this desktop backend.",
+ description: "User-configured endpoint for this desktop backend.",
},
]);
});
diff --git a/apps/desktop/src/serverExposure.ts b/apps/desktop/src/serverExposure.ts
--- a/apps/desktop/src/serverExposure.ts
+++ b/apps/desktop/src/serverExposure.ts
@@ -165,17 +165,18 @@
for (const customEndpointUrl of input.customHttpsEndpointUrls ?? []) {
try {
+ const isHttps = isHttpsEndpointUrl(customEndpointUrl);
endpoints.push(
createManualEndpoint({
id: `manual:${customEndpointUrl}`,
- label: "Custom HTTPS",
+ label: isHttps ? "Custom HTTPS" : "Custom endpoint",
httpBaseUrl: customEndpointUrl,
reachability: "public",
- ...(isHttpsEndpointUrl(customEndpointUrl)
- ? ({ hostedHttpsCompatibility: "compatible" } as const)
- : {}),
+ ...(isHttps ? ({ hostedHttpsCompatibility: "compatible" } as const) : {}),
status: "unknown",
- description: "User-configured HTTPS endpoint for this desktop backend.",
+ description: isHttps
+ ? "User-configured HTTPS endpoint for this desktop backend."
+ : "User-configured endpoint for this desktop backend.",
}),
);
} catch {You can send follow-ups to the cloud agent here.
- Relax settings UI text assertions to match reachable URLs - Add `packages/tailscale/package.json` to release smoke coverage
- Move SSH parsing and discovery logic into `@t3tools/ssh` - Reuse the shared helpers from the desktop app and release smoke checks - Add coverage for host discovery, parsing, and package spec resolution
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Tailscale Serve uses config port before server binds
- The tailscaleServeLayer now reads the actual bound port from HttpServer.address (matching the pattern used by runtimeStateLayer) instead of using config.port directly, so when port 0 is specified the real OS-assigned port is passed to ensureTailscaleServe.
Or push these changes by commenting:
@cursor push c90ddf7775
Preview (c90ddf7775)
diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts
--- a/apps/server/src/server.ts
+++ b/apps/server/src/server.ts
@@ -309,17 +309,24 @@
);
const tailscaleServeLayer = config.tailscaleServeEnabled
? Layer.effectDiscard(
- ensureTailscaleServe({
- localPort: config.port,
- servePort: config.tailscaleServePort,
- localHost: "127.0.0.1",
+ Effect.gen(function* () {
+ const server = yield* HttpServer.HttpServer;
+ const address = server.address;
+ const localPort =
+ typeof address !== "string" && "port" in address ? address.port : config.port;
+ yield* ensureTailscaleServe({
+ localPort,
+ servePort: config.tailscaleServePort,
+ localHost: "127.0.0.1",
+ }).pipe(
+ Effect.tap(() =>
+ Effect.logInfo("Tailscale Serve configured", {
+ localPort,
+ servePort: config.tailscaleServePort,
+ }),
+ ),
+ );
}).pipe(
- Effect.tap(() =>
- Effect.logInfo("Tailscale Serve configured", {
- localPort: config.port,
- servePort: config.tailscaleServePort,
- }),
- ),
Effect.catch((cause) =>
Effect.logWarning("Failed to configure Tailscale Serve", {
cause,You can send follow-ups to the cloud agent here.
- Add auth, command, config, and tunnel exports - Update desktop SSH environment imports - Add tests for auth and command helpers
- Avoid showing the hosted pairing flow outside the static app - Keep the existing error boundary behavior for local builds
- Factor Tailscale command execution into a reusable helper - Add tests for `tailscale serve off` - Ensure hosted pairing UI cleans up Tailscale Serve after startup
There was a problem hiding this comment.
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: Token stripped before reading hosted pairing request
- Cached the result of readHostedPairingRequest() in a module-level variable so that remounts of HostedPairingRouteSurface read the originally-captured token instead of re-reading the already-stripped URL.
Or push these changes by commenting:
@cursor push f4b4a82b5a
Preview (f4b4a82b5a)
diff --git a/apps/web/src/components/auth/PairingRouteSurface.tsx b/apps/web/src/components/auth/PairingRouteSurface.tsx
--- a/apps/web/src/components/auth/PairingRouteSurface.tsx
+++ b/apps/web/src/components/auth/PairingRouteSurface.tsx
@@ -161,8 +161,16 @@
);
}
+let cachedHostedPairingRequest: ReturnType<typeof readHostedPairingRequest> | undefined;
+function readHostedPairingRequestOnce() {
+ if (cachedHostedPairingRequest === undefined) {
+ cachedHostedPairingRequest = readHostedPairingRequest();
+ }
+ return cachedHostedPairingRequest;
+}
+
export function HostedPairingRouteSurface() {
- const hostedPairingRequestRef = useRef(readHostedPairingRequest());
+ const hostedPairingRequestRef = useRef(readHostedPairingRequestOnce());
const [status, setStatus] = useState<"pairing" | "paired" | "error">("pairing");
const [message, setMessage] = useState("Connecting to this backend.");
const submitAttemptedRef = useRef(false);You can send follow-ups to the cloud agent here.
- Move SSH auth, config, and tunnel logic into `packages/ssh` - Wire `apps/desktop` to the shared Effect runtime - Add tests for config, auth, and tunnel behavior
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 4 total unresolved issues (including 3 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Windows askpass CMD references wrong PowerShell filename
- Updated askpass-windows.cmd to invoke askpass-windows.ps1 instead of the nonexistent ssh-askpass.ps1.
Or push these changes by commenting:
@cursor push 2befcb1e74
Preview (2befcb1e74)
diff --git a/apps/desktop/src/sshScripts/askpass-windows.cmd b/apps/desktop/src/sshScripts/askpass-windows.cmd
--- a/apps/desktop/src/sshScripts/askpass-windows.cmd
+++ b/apps/desktop/src/sshScripts/askpass-windows.cmd
@@ -1,2 +1,2 @@
@echo off
-powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ssh-askpass.ps1" %*
+powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0askpass-windows.ps1" %*You can send follow-ups to the cloud agent here.
- Simplify add-environment dialog around remote and SSH pairing - Auto-detect pairing URLs and improve SSH prompt handling - Add coverage for tunnel and connection parsing behavior
- Support remote T3 runner selection for hosted pairing - Surface SSH/environment disconnect states in the UI - Improve websocket snapshot and password prompt error handling
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 4 total unresolved issues (including 3 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Hardcoded developer-specific path as fallback value
- Changed the fallback value from a developer-specific absolute path to an empty string so the guard correctly falls through to the production CLI resolution path when the env var is unset.
Or push these changes by commenting:
@cursor push eb3bd611e5
Preview (eb3bd611e5)
diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
--- a/apps/desktop/src/main.ts
+++ b/apps/desktop/src/main.ts
@@ -124,8 +124,7 @@
// for a built server entry, for example:
// "/Users/julius/Development/Work/codething-mvp/apps/server/dist/bin.mjs"
const DEV_REMOTE_T3_SERVER_ENTRY_PATH =
- process.env.T3CODE_DEV_REMOTE_T3_SERVER_ENTRY_PATH?.trim() ??
- "/Users/julius/Developer/t3code/apps/server/dist/bin.mjs";
+ process.env.T3CODE_DEV_REMOTE_T3_SERVER_ENTRY_PATH?.trim() ?? "";
const desktopAppBranding: DesktopAppBranding = resolveDesktopAppBranding({
isDevelopment,
appVersion: app.getVersion(),You can send follow-ups to the cloud agent here.
| } | ||
| } | ||
|
|
||
| async dispose(): Promise<void> { |
There was a problem hiding this comment.
🟡 Medium src/sshEnvironment.ts:344
After dispose() is called, the IPC handlers registered in registerIpcHandlers() remain active. If the renderer sends a message to ENSURE_SSH_ENVIRONMENT_CHANNEL or DISCONNECT_SSH_ENVIRONMENT_CHANNEL, the handler calls this.manager.ensureEnvironment() or this.manager.disconnectEnvironment() on the disposed manager, which throws because its internal runtime is already disposed. The dispose() method should unregister all IPC handlers via ipcMain.removeHandler() for each channel.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/desktop/src/sshEnvironment.ts around line 344:
After `dispose()` is called, the IPC handlers registered in `registerIpcHandlers()` remain active. If the renderer sends a message to `ENSURE_SSH_ENVIRONMENT_CHANNEL` or `DISCONNECT_SSH_ENVIRONMENT_CHANNEL`, the handler calls `this.manager.ensureEnvironment()` or `this.manager.disconnectEnvironment()` on the disposed manager, which throws because its internal runtime is already disposed. The `dispose()` method should unregister all IPC handlers via `ipcMain.removeHandler()` for each channel.
Evidence trail:
apps/desktop/src/sshEnvironment.ts lines 344-347 (dispose method - no removeHandler calls), lines 230-334 (registerIpcHandlers - registers handlers using ipcMain parameter but ipcMain is not stored as class field), lines 82-141 (DesktopSshEnvironmentManager class - dispose() calls this.runtime.dispose()), lines 118-136 (ensureEnvironment/disconnectEnvironment call this.runtime.runPromise)
- Add structured logging around SSH command, tunnel, and pairing flows - Let desktop SSH bootstrap failures propagate instead of timing out locally - Update reconnect test to expect the underlying SSH timeout error
- tail remote server logs on readiness failures - log tunnel command, PID, and local port state - capture last HTTP probe failure on timeout
Co-authored-by: codex <codex@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Medium
t3code/apps/web/src/components/ChatView.tsx
Line 3069 in 7c52db2
onImplementPlanInNewThread does not check activeEnvironmentUnavailable before dispatching commands. When the environment is disconnected but the API object is still stale (not yet cleaned up), readEnvironmentApi returns a non-undefined API, so the !api guard passes and the function attempts to dispatch thread.create and thread.turn.start to an unavailable environment. This will likely fail with confusing errors or hang. Consider adding the same activeEnvironmentUnavailable guard that was added to onSend.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/components/ChatView.tsx around line 3069:
`onImplementPlanInNewThread` does not check `activeEnvironmentUnavailable` before dispatching commands. When the environment is disconnected but the API object is still stale (not yet cleaned up), `readEnvironmentApi` returns a non-`undefined` API, so the `!api` guard passes and the function attempts to dispatch `thread.create` and `thread.turn.start` to an unavailable environment. This will likely fail with confusing errors or hang. Consider adding the same `activeEnvironmentUnavailable` guard that was added to `onSend`.
Evidence trail:
apps/web/src/components/ChatView.tsx lines 2450-2457 (`onSend` includes `activeEnvironmentUnavailable` guard); apps/web/src/components/ChatView.tsx lines 3071-3081 (`onImplementPlanInNewThread` guard lacks `activeEnvironmentUnavailable`); apps/web/src/components/ChatView.tsx lines 3188-3200 (`useCallback` dependency array for `onImplementPlanInNewThread` also omits `activeEnvironmentUnavailable`); apps/web/src/components/ChatView.tsx lines 874-876 (definition of `activeEnvironmentUnavailable`)
- Add desktop SSH cancellation handling for environment setup - Let chat reconnect a saved environment from the unavailable state - Refine connections UI for hosted pairing links, network access, and favicon fallbacks
| Effect.gen(function* () { | ||
| if (tunnels.get(tunnelEntry.key) !== tunnelEntry) { | ||
| return; | ||
| } | ||
| yield* Effect.logDebug("ssh.environment.tunnel.finalizer.start", { | ||
| ...sshTargetLogFields(tunnelEntry.target), | ||
| key: tunnelEntry.key, | ||
| localPort: tunnelEntry.localPort, | ||
| remotePort: tunnelEntry.remotePort, | ||
| }); | ||
| tunnels.delete(tunnelEntry.key); | ||
| const authSecret = authSecrets.get(tunnelEntry.key) ?? null; | ||
| yield* Effect.all( |
There was a problem hiding this comment.
🟢 Low src/tunnel.ts:1271
The scope finalizer at lines 1272-1281 contains a race condition. After the guard check tunnels.get(tunnelEntry.key) !== tunnelEntry returns false (meaning this entry is still current), the Effect.logDebug calls at lines 1275-1280 are yield points. If another fiber creates a new entry for the same key during this yield, line 1281 tunnels.delete(tunnelEntry.key) will delete the new entry instead of this one, leaving the new entry orphaned and its cleanup skipped.
Re-check the guard immediately before deletion: wrap the delete in if (tunnels.get(tunnelEntry.key) === tunnelEntry) to ensure we only delete our own entry.
yield* Effect.gen(function* () {
if (tunnels.get(tunnelEntry.key) !== tunnelEntry) {
return;
}
yield* Effect.logDebug("ssh.environment.tunnel.finalizer.start", {
...sshTargetLogFields(tunnelEntry.target),
key: tunnelEntry.key,
localPort: tunnelEntry.localPort,
remotePort: tunnelEntry.remotePort,
});
tunnels.delete(tunnelEntry.key);
const authSecret = authSecrets.get(tunnelEntry.key) ?? null;🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/ssh/src/tunnel.ts around lines 1271-1283:
The scope finalizer at lines 1272-1281 contains a race condition. After the guard check `tunnels.get(tunnelEntry.key) !== tunnelEntry` returns false (meaning this entry is still current), the `Effect.logDebug` calls at lines 1275-1280 are yield points. If another fiber creates a new entry for the same key during this yield, line 1281 `tunnels.delete(tunnelEntry.key)` will delete the *new* entry instead of this one, leaving the new entry orphaned and its cleanup skipped.
Re-check the guard immediately before deletion: wrap the delete in `if (tunnels.get(tunnelEntry.key) === tunnelEntry)` to ensure we only delete our own entry.
Evidence trail:
packages/ssh/src/tunnel.ts lines 1265 (tunnels.set), 1270-1281 (scope finalizer with guard check at 1272, yield point at 1275, unconditional delete at 1281) at REVIEWED_COMMIT. Only one tunnels.set call exists (confirmed via git_grep). Effect.gen yield* creates flatMap boundaries that are fiber interleaving points in Effect-TS runtime.
- Match the new pairing URL button label - Scope Add Environment assertions to the dialog and updated SSH flow
- Default the remote T3 server entry path to an empty string - Rely on `T3CODE_DEV_REMOTE_T3_SERVER_ENTRY_PATH` when set
- Remove desktop SSH launch scripts and bundled copy step - Make endpoint labels and server exposure detection host-aware - Clear Tailscale Serve using the active MagicDNS name
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Composer disabled during approval/input states blocks needed interaction
- Changed the disabled condition to only apply environmentUnavailable when activePendingProgress is null, allowing users to type custom answers for pending user inputs even during temporary disconnects.
Or push these changes by commenting:
@cursor push fb722be077
Preview (fb722be077)
diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx
--- a/apps/web/src/components/chat/ChatComposer.tsx
+++ b/apps/web/src/components/chat/ChatComposer.tsx
@@ -1989,7 +1989,9 @@
: "Ask anything, @tag files/folders, or use / to show available commands"
}
disabled={
- isConnecting || isComposerApprovalState || environmentUnavailable !== null
+ isConnecting ||
+ isComposerApprovalState ||
+ (environmentUnavailable !== null && activePendingProgress === null)
}
/>
</div>You can send follow-ups to the cloud agent here.
| : "Ask anything, @tag files/folders, or use / to show available commands" | ||
| } | ||
| disabled={ | ||
| isConnecting || isComposerApprovalState || environmentUnavailable !== null |
There was a problem hiding this comment.
Composer disabled during approval/input states blocks needed interaction
Medium Severity
The disabled condition on the composer text input includes environmentUnavailable !== null, which disables the textarea even when there's an active pending approval or pending user input that the user needs to interact with. Since the approval/input panels use the same form, users would be unable to type custom answers for pending user inputs when the environment is marked unavailable (e.g., during a brief reconnect), even though those answers are already queued locally and could complete once the environment reconnects.
Reviewed by Cursor Bugbot for commit ef679d3. Configure here.
- drop pending saved-environment connects on disconnect - ignore stale WebSocket close events after transport disposal
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Revert check unreachable when environment is disconnected
- Moved the activeEnvironmentUnavailable check before the readEnvironmentApi call so the informative error message is shown instead of a silent early return when the environment is disconnected.
Or push these changes by commenting:
@cursor push fdc0ee45ec
Preview (fdc0ee45ec)
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -2417,9 +2417,7 @@
const onRevertToTurnCount = useCallback(
async (turnCount: number) => {
- const api = readEnvironmentApi(environmentId);
- const localApi = readLocalApi();
- if (!api || !localApi || !activeThread || isRevertingCheckpoint) return;
+ if (!activeThread || isRevertingCheckpoint) return;
if (activeEnvironmentUnavailable && activeEnvironmentUnavailableLabel) {
setThreadError(
@@ -2428,6 +2426,10 @@
);
return;
}
+
+ const api = readEnvironmentApi(environmentId);
+ const localApi = readLocalApi();
+ if (!api || !localApi) return;
if (phase === "running" || isSendBusy || isConnecting) {
setThreadError(activeThread.id, "Interrupt the current turn before reverting checkpoints.");
return;You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 229a485. Configure here.
| `Reconnect ${activeEnvironmentUnavailableLabel} before reverting checkpoints.`, | ||
| ); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Revert check unreachable when environment is disconnected
Medium Severity
The activeEnvironmentUnavailable check at line 2424 is unreachable when the saved environment is actually disconnected. readEnvironmentApi(environmentId) returns undefined when no active connection exists for the environment, so the !api guard at line 2422 short-circuits first with a silent return. The user clicks "revert" and nothing happens — they never see the intended "Reconnect before reverting checkpoints" error message.
Reviewed by Cursor Bugbot for commit 229a485. Configure here.



Note
High Risk
Touches remote connectivity, credential/token exchange surfaces, and Electron IPC (including SSH password prompting) while adding Tailscale Serve lifecycle management on the server. Misconfiguration or edge cases could break pairing/reconnect flows or expose confusing/incorrect endpoint choices.
Overview
Enables a hosted static web app pairing flow (
/pair?host=...#token=...) that saves environments browser-locally (no hosted control plane), and adds UI/UX safeguards when a saved environment is disconnected (reconnect banner; blocks send/checkpoint revert; more descriptive reconnect toasts).On desktop, introduces first-class remote endpoint advertising and providers: core loopback/LAN plus user-configured custom endpoints, and a Tailscale provider that discovers Tailnet IP/MagicDNS and optionally exposes a verified HTTPS endpoint via Tailscale Serve; adds persisted desktop settings and IPC to toggle Tailscale Serve (relaunching the app) and to fetch advertised endpoints.
Adds a desktop-managed SSH remote environment bridge over Electron IPC (host discovery, environment ensure/disconnect, bearer/bootstrap/ws-token HTTP calls via the forwarded loopback endpoint, and renderer password prompting), and persists
desktopSshmetadata alongside saved environments in both desktop and web storage.On the server/CLI, adds
--tailscale-serve/--tailscale-serve-portplus env/bootstrap support and lifecycle hooks to configure/disable Tailscale Serve on startup/shutdown; also improves orchestration WS error logging/mapping for snapshot failures. Documentation is updated to formalize theAdvertisedEndpointconcept, hosted pairing constraints, and remote access/SSH guidance;.vercelis ignored.Reviewed by Cursor Bugbot for commit 229a485. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add hosted frontend mode, Tailscale Serve integration, and SSH environment launcher
hosted-staticauth gate state and onboarding UI when no environments are saved.HostedPairingRouteSurface) that processes pairing links shared from the desktop app, saves the environment, and navigates the user into the app.@t3tools/ssh,@t3tools/tailscale): the desktop app can discover SSH hosts, tunnel a remote T3 server over SSH, issue bearer tokens, and manage the lifecycle from the Connections settings UI.AdvertisedEndpointcontract with provider metadata, reachability classification, and hosted-HTTPS compatibility — used to generate shareable pairing URLs and drive the redesigned Connections settings panel.Macroscope summarized 229a485.