From 0591da2ba65f1e8bc61e5b5f2ae1dab579e73451 Mon Sep 17 00:00:00 2001
From: Siddharth Balyan <52913345+alt-glitch@users.noreply.github.com>
Date: Fri, 11 Sep 2026 18:16:48 +0530
Subject: [PATCH] Guided first launch: review fixes from #107985 and the
free-tier chip badge (NS-848, NS-855) (#108211)
* fix(desktop): centralize guide handoff receipt reads
Resolve the guide receipt key and value together in setup-profile. Use the helper at all four read sites so connection scoping follows one implementation.
* fix(desktop): recover from unreadable handoff receipts
Memoize receipt reads and show Retry only for the error phase. Quarantine corrupt data before retrying, and resolve the guide identity when the failed request did not retain it so a fresh build can start.
Cover preservation of corrupt data and removal from the active receipt key with an invariant test.
* fix(desktop): validate persisted onboarding phases from one list
Derive OnboardingPhase and persisted-value validation from the same phase list so future phases survive relaunch. Verify every persisted phase reloads and an unknown value falls back to idle.
* fix(desktop): share window centering arithmetic
Extract centeredBounds and use it for onboarding boot and window growth. Keep the existing work-area clamps and coordinate rounding unchanged.
* fix(desktop): compute progress steps inline
Remove the ineffective ProgressCard memo because streaming flushes replace the messages array. Keep the same transcript scan and rendered steps.
* fix(desktop): center the free-tier status chip detail
Wrap the model label and sign-in badge in an inline flex span with a shared gap. This centers the badge beside the model text without changing other status-bar details.
* fix(desktop): derive the guide receipt key in one place
The Retry path spelled the key derivation out again because the read helper throws on a corrupt receipt before it can return the key. A separate guideHandoffReceiptKey serves both the reader and the quarantine, so the derivation has one home again.
* fix(desktop): keep the free-tier badge at its intended leading
Badge declares leading-none, but the class merger drops it behind the size variant's font-size class, so the badge inherits a 1.5 leading and renders 16px tall next to an 11px label. That height, not the inline alignment, is what read as a detached badge. Restating leading-none on the chip's badge brings it to 11.6px, inside the label's cap height. The Badge component itself is left alone; every other badge in the app has the same dropped leading and that is a separate decision.
---
.../electron/chat-onboarding-window.ts | 12 +--
apps/desktop/electron/window-growth.ts | 6 ++
.../src/app/contrib/handoff-receipt.test.ts | 18 ++++
.../src/app/contrib/handoff-receipt.ts | 7 +-
.../src/app/contrib/onboarding-handoff.ts | 9 +-
.../app/shell/hooks/use-statusbar-items.tsx | 9 +-
.../onboarding-chat/cards/build.tsx | 98 ++++++++++++-------
.../onboarding-chat/setup-profile.ts | 15 ++-
.../desktop/src/store/onboarding-gate.test.ts | 37 +++++++
apps/desktop/src/store/onboarding-gate.ts | 14 +--
10 files changed, 165 insertions(+), 60 deletions(-)
create mode 100644 apps/desktop/src/app/contrib/handoff-receipt.test.ts
create mode 100644 apps/desktop/src/store/onboarding-gate.test.ts
diff --git a/apps/desktop/electron/chat-onboarding-window.ts b/apps/desktop/electron/chat-onboarding-window.ts
index f9d33774cb..25757d2a07 100644
--- a/apps/desktop/electron/chat-onboarding-window.ts
+++ b/apps/desktop/electron/chat-onboarding-window.ts
@@ -1,6 +1,6 @@
import { type BrowserWindow, ipcMain, screen } from 'electron'
-import { type GrowRequest, growWindowBounds } from './window-growth'
+import { centeredBounds, type GrowRequest, growWindowBounds } from './window-growth'
interface ChatOnboardingWindowOptions {
enabled: boolean
@@ -40,14 +40,6 @@ export function registerChatOnboardingWindow({ enabled, mainWindow }: ChatOnboar
const width = Math.min(600, area.width)
const height = Math.min(640, area.height)
- win.setBounds(
- {
- height,
- width,
- x: Math.round(area.x + (area.width - width) / 2),
- y: Math.round(area.y + (area.height - height) / 2)
- },
- true
- )
+ win.setBounds(centeredBounds(area, width, height), true)
})
}
diff --git a/apps/desktop/electron/window-growth.ts b/apps/desktop/electron/window-growth.ts
index 816adab1c5..40850274d3 100644
--- a/apps/desktop/electron/window-growth.ts
+++ b/apps/desktop/electron/window-growth.ts
@@ -6,6 +6,8 @@
* asserted rather than eyeballed on a first run.
*/
+import type { Rectangle } from 'electron'
+
export interface GrowRequest {
bottom?: number
left?: number
@@ -65,6 +67,10 @@ export function growWindowBounds(
Math.round(workArea.height * MAX_WORK_AREA)
)
+ return centeredBounds(workArea, width, height)
+}
+
+export function centeredBounds(workArea: Rectangle, width: number, height: number): Rectangle {
return {
height,
width,
diff --git a/apps/desktop/src/app/contrib/handoff-receipt.test.ts b/apps/desktop/src/app/contrib/handoff-receipt.test.ts
new file mode 100644
index 0000000000..ac01c18900
--- /dev/null
+++ b/apps/desktop/src/app/contrib/handoff-receipt.test.ts
@@ -0,0 +1,18 @@
+import { expect, it } from 'vitest'
+
+import { readKey, writeKey } from '@/lib/storage'
+
+import { handoffReceiptKey, quarantineHandoffReceipt, readHandoffReceipt } from './handoff-receipt'
+
+it('preserves an unreadable receipt and removes it from the retry lookup', () => {
+ const key = handoffReceiptKey('receipt-recovery', 'guide')
+ const corrupt = '{unreadable receipt'
+ writeKey(key, corrupt)
+
+ expect(() => readHandoffReceipt(key)).toThrow()
+
+ quarantineHandoffReceipt(key)
+
+ expect(readKey(`${key}.unreadable`)).toBe(corrupt)
+ expect(readHandoffReceipt(key)).toBeNull()
+})
diff --git a/apps/desktop/src/app/contrib/handoff-receipt.ts b/apps/desktop/src/app/contrib/handoff-receipt.ts
index 3ef068f020..876e7ae041 100644
--- a/apps/desktop/src/app/contrib/handoff-receipt.ts
+++ b/apps/desktop/src/app/contrib/handoff-receipt.ts
@@ -1,4 +1,4 @@
-import { readKey, writeJson } from '@/lib/storage'
+import { readKey, writeJson, writeKey } from '@/lib/storage'
import type { HandoffReceipt } from './handoff-leg'
@@ -59,6 +59,11 @@ export function readHandoffReceipt(key: string): HandoffReceipt | null {
return value
}
+export function quarantineHandoffReceipt(key: string): void {
+ writeKey(`${key}.unreadable`, readKey(key))
+ writeKey(key, null)
+}
+
export function saveHandoffReceipt(key: string, receipt: HandoffReceipt): void {
unsavedReceipts.set(key, receipt)
writeJson(key, receipt)
diff --git a/apps/desktop/src/app/contrib/onboarding-handoff.ts b/apps/desktop/src/app/contrib/onboarding-handoff.ts
index 7a1a2b1454..d78f9e1fda 100644
--- a/apps/desktop/src/app/contrib/onboarding-handoff.ts
+++ b/apps/desktop/src/app/contrib/onboarding-handoff.ts
@@ -14,6 +14,7 @@ import {
buildHandoffCompleteNote,
firstTaskTitle,
guideSourceConnectionId,
+ readGuideHandoffReceipt,
retrySetupHandoff,
SETUP_PROFILE
} from '@/components/onboarding-chat/setup-profile'
@@ -40,7 +41,7 @@ import {
import { patchSessionTile } from '@/store/session-states'
import { BUILD_PROFILE, type HandoffDeps, type HandoffReceipt, paintHandoffBrief, startHandoff } from './handoff-leg'
-import { handoffReceiptKey, readHandoffReceipt, saveHandoffReceipt } from './handoff-receipt'
+import { saveHandoffReceipt } from './handoff-receipt'
import type { AmbientGatewayRequest } from './session-rpc-dispatcher'
export interface OnboardingHandoffOptions extends Pick<
@@ -82,7 +83,7 @@ export function useOnboardingHandoff({
const connectionId = guideSourceConnectionId(selectedStoredId)
try {
- const saved = readHandoffReceipt(handoffReceiptKey(connectionId, selectedStoredId))
+ const { receipt: saved } = readGuideHandoffReceipt(selectedStoredId)
if (!saved) {
return
@@ -146,8 +147,8 @@ export function useOnboardingHandoff({
$setupSession.set(setupSession)
// Resume only has the guide's stored id, so its source must also key the saved receipt.
- const receiptKey = handoffReceiptKey(guideSourceConnectionId(setupSession.storedId), setupSession.storedId)
- receipt = readHandoffReceipt(receiptKey)
+ const { key: receiptKey, receipt: saved } = readGuideHandoffReceipt(setupSession.storedId)
+ receipt = saved
const owner: HandoffReceipt['owner'] = receipt?.owner ?? { connectionId, profile: BUILD_PROFILE }
// Save facts before session.create freezes the new agent's memory.
// A retry never re-creates the session or copies the guide's memory.
diff --git a/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx b/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx
index c45a377621..7b3f7f3b7d 100644
--- a/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx
+++ b/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx
@@ -463,14 +463,17 @@ export function useStatusbarItems({
// The model id is the quiet part; the sign-in is the action, so it is
// solid and set off by a gap instead of touching the label.
detail: (
- <>
+
{freeTier?.model ?? FREE_TIER_MODEL}
-