From dc189ee46f5d3f60201ba1ba29bab894e7fe6757 Mon Sep 17 00:00:00 2001 From: Cameron Date: Sun, 25 Oct 2026 21:25:50 +0000 Subject: [PATCH 1/3] fix(desktop): skip safe restart confirmation for resumable work --- .../src/app/DesktopShutdownGuard.test.ts | 94 ++++++++++++++++++- apps/desktop/src/app/DesktopShutdownGuard.ts | 2 +- 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/app/DesktopShutdownGuard.test.ts b/apps/desktop/src/app/DesktopShutdownGuard.test.ts index 5c5bfb9ce5c4..f1e87924f7c3 100644 --- a/apps/desktop/src/app/DesktopShutdownGuard.test.ts +++ b/apps/desktop/src/app/DesktopShutdownGuard.test.ts @@ -146,6 +146,93 @@ function harness( } describe("DesktopShutdownGuard", () => { + it.effect("restarts resumable work without consent but still lets the user cancel Quit", () => { + const h = harness({ + activity: () => + Effect.succeed({ + ...idle, + activeSessions: 1, + continuableSessions: 1, + threads: [ + { + threadId: ThreadId.make("release"), + title: "Prepare release", + reasons: ["running"], + continuable: true, + }, + ], + }), + }); + return Effect.gen(function* () { + const guard = yield* Guard.DesktopShutdownGuard; + assert.isTrue(yield* guard.confirm("restart")); + assert.equal(h.checks(), 2); + assert.isEmpty(h.messages); + + assert.isFalse(yield* guard.confirm("quit")); + assert.equal(h.checks(), 4); + assert.lengthOf(h.messages, 1); + assert.include(h.messages[0]!, "Quit with running work?"); + assert.include(h.messages[0]!, "2 threads will continue when styal next starts."); + assert.deepEqual( + h.details[0]?.map((item) => item.continuesAfterRestart), + [true, true], + ); + + assert.isTrue(yield* guard.confirm("restart")); + assert.equal(h.checks(), 6); + assert.lengthOf(h.messages, 1); + + assert.isFalse(yield* guard.confirm("restart", { continueThreads: false })); + assert.lengthOf(h.messages, 2); + assert.include(h.messages[1]!, "2 threads will be interrupted."); + }).pipe(Effect.provide(h.layer)); + }); + + it.effect.each([ + { name: "waiting requests or secrets", blocker: { waitingSessions: 1 } }, + { name: "non-continuable turns", blocker: { activeSessions: 2 } }, + { name: "terminal commands", blocker: { terminalsRequiringConfirmation: 1 } }, + { + name: "unknown terminal activity", + blocker: { terminalsRequiringConfirmation: 1, terminalsWithUnknownActivity: 1 }, + }, + { name: "provider installs", blocker: { providerInstalls: 1 } }, + { name: "shared devices", blocker: { environmentsUsingDevices: 1 } }, + ])("asks before restarting resumable turns alongside $name", ({ blocker }) => { + const h = harness({ + activity: (port) => + Effect.succeed({ + ...idle, + activeSessions: 1, + continuableSessions: 1, + ...(port === "3774" ? blocker : {}), + }), + }); + return Effect.gen(function* () { + const guard = yield* Guard.DesktopShutdownGuard; + assert.isFalse(yield* guard.confirm("restart")); + assert.equal(h.checks(), 2); + assert.lengthOf(h.messages, 1); + assert.include(h.messages[0]!, "Make sure you're ready before continuing."); + }).pipe(Effect.provide(h.layer)); + }); + + it.effect("does not infer a safe restart from the hosts that answered successfully", () => { + const h = harness({ + activity: (port) => + Effect.succeed( + port === "3773" ? { ...idle, activeSessions: 1, continuableSessions: 1 } : {}, + ), + }); + return Effect.gen(function* () { + const guard = yield* Guard.DesktopShutdownGuard; + assert.isFalse(yield* guard.confirm("restart")); + assert.include(h.messages[0]!, "Could not check every local environment"); + assert.equal(h.checks(), 2); + }).pipe(Effect.provide(h.layer)); + }); + it.effect("names affected threads from each owned backend and preserves cancellation", () => { const h = harness({ activity: (port) => @@ -358,7 +445,7 @@ describe("shutdown confirmation copy", () => { [true, false], ); }); - it("drops the warning when every thread continues, and says when for quit", () => { + it("skips restart confirmation when every thread continues, and says when for quit", () => { const check = { ...idle, activeSessions: 1, @@ -368,10 +455,7 @@ describe("shutdown confirmation copy", () => { const quit = Guard.shutdownConfirmation([check], "quit"); assert.strictEqual(quit?.message, "Quit with running work?"); assert.strictEqual(quit?.detail, "1 thread will continue when styal next starts."); - assert.strictEqual( - Guard.shutdownConfirmation([check], "restart")?.detail, - "1 thread will continue after the restart.", - ); + assert.isNull(Guard.shutdownConfirmation([check], "restart")); }); it("keeps the warning when a continuing thread shares the restart with a waiting one", () => { const confirmation = Guard.shutdownConfirmation( diff --git a/apps/desktop/src/app/DesktopShutdownGuard.ts b/apps/desktop/src/app/DesktopShutdownGuard.ts index f5f44a6f47e5..c7ab3477364e 100644 --- a/apps/desktop/src/app/DesktopShutdownGuard.ts +++ b/apps/desktop/src/app/DesktopShutdownGuard.ts @@ -36,7 +36,7 @@ export function shutdownConfirmation( continueRunningThreads: continueThreads, continuesWhen: action === "quit" ? "when styal next starts" : "after the restart", }); - if (concern === null) return null; + if (concern === null || (action === "restart" && !interrupts)) return null; const detail = incomplete ? [ ...lines, From e385de766d4b46df01aaa28e3b2309e89d6a2b26 Mon Sep 17 00:00:00 2001 From: Cameron Date: Sun, 25 Oct 2026 21:30:30 +0000 Subject: [PATCH 2/3] docs(updates): explain safe local restarts and quit confirmation --- .github/fork-features.yml | 2 +- docs/user/updating.md | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/fork-features.yml b/.github/fork-features.yml index e7ecc733e950..d48338f6e4f5 100644 --- a/.github/fork-features.yml +++ b/.github/fork-features.yml @@ -853,7 +853,7 @@ features: - Nightly release previews retain curated highlights and footer filtering while showing newest entries with counts and links to styal releases. - Desktop activity authentication remains available for the host process lifetime, including the first quit after days of uptime and renewal after bearer expiry. Signing in for an activity check never revokes the desktop window's session. - In-app shutdown dialogs appear only for active or unknown work and list affected thread titles under their project icons/names and host labels, with threads that continue after the restart in their own section. Which threads continue comes from the server's activity report, which applies each project's setting and excludes runs the user asked to stop, native maintenance commands, and threads at the limit of automatic continuations in a row, as delivery after the restart does; desktop quits say they continue when styal next starts, and desktop relaunches that turn off or replace a backend report every thread as interrupted. Each thread has a badge per turn, input, approval, or background-work reason from the same snapshot as the counts; older servers retain counts and explicitly report unavailable details, and a reason the client does not recognize is dropped rather than failing the check. Terminal activity is counted and inspection warnings appear only for activity that could not be checked. - - Manual desktop quits and restarts check every owned backend; active or unknown host activity requires confirmation, and cancellation preserves windows and downloaded updates. The readiness warning appears only when some work will be interrupted or could not be checked. + - Local desktop restarts skip confirmation only when every owned backend check succeeds and no work would be interrupted; resumable turns are allowed, while running or unknown terminal activity, waiting requests, non-continuable turns, provider installs and shared devices require confirmation. Quit still confirms active or resumable work because continuation waits until the next launch. Cancellation preserves windows and downloaded updates. - Shutdown confirmation has one renderer-wide host, including on recovery screens; cancelling an update restart dismisses the request without reopening it. Failed presentation alone arms a subsequent Quit override that preserves cleanup, cannot be consumed by restart actions, and clears when the renderer recovers or displays a dialog. - Available desktop updates download automatically while preserving accurate available, downloading, downloaded, install, and retry state across updater events and pre-install refreshes. - Downloaded updates install on normal app quit; the sidebar offers a plain restart arrow without a completion checkmark, using the theme update surface and foreground once an installer is ready, while Settings presents automatic checks and downloads as status and reserves download actions for failure retries. diff --git a/docs/user/updating.md b/docs/user/updating.md index 48668282133d..5789def707e5 100644 --- a/docs/user/updating.md +++ b/docs/user/updating.md @@ -144,8 +144,11 @@ next time you look at its window, with a link to that version's release notes. Before quitting or restarting, styal checks agents and terminal commands in the environments the desktop app hosts, including WSL and work started from other -devices. If work is active or cannot be checked, styal lists it and lets you -cancel or continue; cancelling keeps the downloaded update. If the confirmation +devices. A restart proceeds directly when every check succeeds and all active +threads can continue. Terminal commands, waiting requests and unchecked activity +still require confirmation. Quit asks about active work even when threads can +continue, because they resume only when styal next starts. You can cancel or +continue; cancelling keeps the downloaded update. If the confirmation cannot be displayed and the app stays unresponsive, choose **Quit** again after five seconds to exit. This can interrupt running work. From 2fc2dc0a858425434098949cf6e1fc2f319286fd Mon Sep 17 00:00:00 2001 From: Cameron Date: Sun, 25 Oct 2026 21:43:59 +0000 Subject: [PATCH 3/3] docs(updates): clarify local restart and quit confirmation --- docs/user/styal-differences.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/user/styal-differences.md b/docs/user/styal-differences.md index 95919990383a..01bb90cb0c26 100644 --- a/docs/user/styal-differences.md +++ b/docs/user/styal-differences.md @@ -337,7 +337,9 @@ works. This overview focuses on desktop and web; the mobile app is a work in pro - **Confirm only when quitting may interrupt work.** Desktop quits and restarts check agents and terminal work across the environments hosted by the app, including work started from other devices and agents in other environments using its shared devices. - Idle hosts proceed without a dialog; active work or an unavailable check requires an in-app confirmation. + Local restarts proceed without a dialog when all active threads will resume and no other work + would be interrupted. Quit still asks about active work. Terminal commands, other interruptions, + and an unavailable check require confirmation. The **Update server** action checks the connected server the same way before it restarts. Compared with [T3 Code revision d4d5d12](https://github.com/pingdotgg/t3code/tree/d4d5d12e8ba086cfbf79ca3adeb4156b46ead665), whose desktop lifecycle does not check host activity before quitting, and