Skip to content

Commit 99ddb61

Browse files
skulidropekclaude
andcommitted
feat(app): auto-restart CF tunnel when process dies + refresh button
- API: don't return hostname of dead cloudflared process (check processClosed) - Frontend: poll every 30s when panel is open, auto-restart if tunnel died - Frontend: add ↻ refresh button in ready state for manual restart Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 46f6880 commit 99ddb61

2 files changed

Lines changed: 36 additions & 8 deletions

File tree

packages/api/src/services/ssh-project-tunnels.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export const startSshProjectTunnel = (
197197
): Effect.Effect<string | null, ApiInternalError> =>
198198
Effect.gen(function*(_) {
199199
const existing = projectTunnelMap.get(projectKey)
200-
if (existing !== undefined && !existing.stopping && existing.hostname !== null) {
200+
if (existing !== undefined && !existing.stopping && !existing.processClosed && existing.hostname !== null) {
201201
return existing.hostname
202202
}
203203
if (existing !== undefined) {

packages/app/src/web/app-ready-terminal-pane.tsx

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,13 @@ const VsCodeAccessPanel = (
203203
cfState,
204204
info,
205205
onClose,
206+
onRefresh,
206207
onRetry
207208
}: {
208209
readonly cfState: CfTunnelState
209210
readonly info: VsCodeAccessInfo
210211
readonly onClose: () => void
212+
readonly onRefresh: () => void
211213
readonly onRetry: () => void
212214
}
213215
): JSX.Element => {
@@ -230,13 +232,12 @@ const VsCodeAccessPanel = (
230232
}}>
231233
<div style={{ alignItems: "center", display: "flex", justifyContent: "space-between", marginBottom: "10px" }}>
232234
<div style={{ color: "#8be9fd", fontWeight: "bold" }}>VS Code / SSH access</div>
233-
<button
234-
onClick={onClose}
235-
style={{ ...vsCodePanelCopyBtnStyle, color: "#f87171" }}
236-
type="button"
237-
>
238-
✕ close
239-
</button>
235+
<div style={{ display: "flex", gap: "4px" }}>
236+
{cfState.tag === "ready" && (
237+
<button onClick={onRefresh} style={{ ...vsCodePanelCopyBtnStyle, color: "#7fdfff" }} type="button">↻ refresh</button>
238+
)}
239+
<button onClick={onClose} style={{ ...vsCodePanelCopyBtnStyle, color: "#f87171" }} type="button">✕ close</button>
240+
</div>
240241
</div>
241242

242243
{cfState.tag === "loading" && (
@@ -397,6 +398,28 @@ export const TerminalPane = (props: TerminalPaneProps): JSX.Element => {
397398
}
398399
}, [vsCodePanelOpen, runtime.browserProjectKey, cfState.tag])
399400

401+
// Poll every 30s when panel is open: restart tunnel if process died
402+
useEffect(() => {
403+
if (!vsCodePanelOpen || cfState.tag !== "ready" || runtime.browserProjectKey === undefined) return
404+
const projectKey = runtime.browserProjectKey
405+
const id = setInterval(() => {
406+
void Effect.runPromise(
407+
startProjectSshTunnel(projectKey).pipe(
408+
Effect.match({
409+
onFailure: () => { setCfState({ tag: "failed" }) },
410+
onSuccess: ({ hostname, sshPassword }) => {
411+
if (hostname === null) { setCfState({ tag: "failed" }); return }
412+
setCfState((prev) =>
413+
prev.tag === "ready" && prev.hostname === hostname ? prev : { tag: "ready", hostname, sshPassword }
414+
)
415+
}
416+
})
417+
)
418+
)
419+
}, 30_000)
420+
return () => { clearInterval(id) }
421+
}, [vsCodePanelOpen, cfState.tag, runtime.browserProjectKey])
422+
400423
const vsCodeInfo = buildVsCodeAccessInfo(props.project)
401424
const onOpenVsCode = vsCodeInfo !== null ? () => { setVsCodePanelOpen(true) } : undefined
402425
const vsCodeBodyContent = vsCodePanelOpen && vsCodeInfo !== null
@@ -405,6 +428,11 @@ export const TerminalPane = (props: TerminalPaneProps): JSX.Element => {
405428
cfState={cfState}
406429
info={vsCodeInfo}
407430
onClose={() => { setVsCodePanelOpen(false) }}
431+
onRefresh={() => {
432+
if (runtime.browserProjectKey !== undefined) {
433+
startTunnel(runtime.browserProjectKey, setCfState)
434+
}
435+
}}
408436
onRetry={() => {
409437
if (runtime.browserProjectKey !== undefined) {
410438
startTunnel(runtime.browserProjectKey, setCfState)

0 commit comments

Comments
 (0)