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}
-
+ {/* The class merger drops Badge's own leading-none behind the size's
+ font-size class, so the badge grows to the inherited 1.5 leading and
+ overhangs an 11px label. Restating it here keeps it 11.6px tall. */}
+
{freeTierCopy.signIn}
- >
+
),
// Shown while a free-tier identity exists and the tier is on: it names the
// identity that carries the connectors (and inference when nothing else
diff --git a/apps/desktop/src/components/onboarding-chat/cards/build.tsx b/apps/desktop/src/components/onboarding-chat/cards/build.tsx
index d01df113af..1eb20232ad 100644
--- a/apps/desktop/src/components/onboarding-chat/cards/build.tsx
+++ b/apps/desktop/src/components/onboarding-chat/cards/build.tsx
@@ -11,7 +11,7 @@ import { useEffect, useMemo, useState } from 'react'
import { requestComposerSubmit } from '@/app/chat/composer/focus'
import { useSessionView } from '@/app/chat/session-view'
-import { handoffReceiptKey, readHandoffReceipt } from '@/app/contrib/handoff-receipt'
+import { quarantineHandoffReceipt } from '@/app/contrib/handoff-receipt'
import { resolveSessionOwner } from '@/app/session/hooks/use-session-actions/utils'
import type { CardProps } from '@/components/onboarding-chat/cards/frame'
import { Chip } from '@/components/onboarding-chat/chip'
@@ -19,8 +19,9 @@ import {
$handoffError,
$setupHandoff,
firstTaskTitle,
- guideSourceConnectionId,
+ guideHandoffReceiptKey,
parseHandoffPlan,
+ readGuideHandoffReceipt,
requestSetupHandoff,
retrySetupHandoff,
SETUP_PROFILE
@@ -111,16 +112,19 @@ export function HandoffCard({ attrs, locked }: CardProps) {
const brief = (attrs.brief ?? '').trim().slice(0, 240)
const plan = parseHandoffPlan(attrs.plan)
const state = useStore($setupHandoff)
- let error = useStore($handoffError)
- let completed = false
+ const receipt = useMemo(() => {
+ try {
+ return {
+ completed: !!storedId && readGuideHandoffReceipt(storedId).receipt?.status === 'accepted',
+ error: null
+ }
+ } catch (error) {
+ return { completed: false, error: String(error) }
+ }
+ }, [storedId, state?.phase])
- try {
- completed =
- !!storedId &&
- readHandoffReceipt(handoffReceiptKey(guideSourceConnectionId(storedId), storedId))?.status === 'accepted'
- } catch (receiptError) {
- error = String(receiptError)
- }
+ const error = useStore($handoffError) ?? receipt.error
+ const completed = receipt.completed
useEffect(() => {
if (!task || !brief || locked || !storedId || !runtimeId || $setupHandoff.get() || completed) {
@@ -161,6 +165,36 @@ export function HandoffCard({ attrs, locked }: CardProps) {
const failed = state?.phase === 'error' || error !== null
const title = state?.sessionTitle ?? firstTaskTitle(task)
+ const retry = async () => {
+ if (state?.phase !== 'error') {
+ return
+ }
+
+ try {
+ if (receipt.error && storedId) {
+ quarantineHandoffReceipt(guideHandoffReceiptKey(storedId))
+ }
+
+ if (!state.guide && storedId && runtimeId) {
+ const owner = await resolveSessionOwner(storedId)
+ assertSessionOwnerResolved(owner, { method: 'onboarding.handoff', sessionId: storedId })
+ $setupHandoff.set({
+ ...state,
+ guide: {
+ storedId,
+ runtimeId,
+ connectionId: isSessionOwnerRoute(owner) ? owner.connectionId : null,
+ profile: isSessionOwnerRoute(owner) ? owner.profile : owner || SETUP_PROFILE
+ }
+ })
+ }
+
+ retrySetupHandoff()
+ } catch (error) {
+ $handoffError.set(String(error))
+ }
+ }
+
return (
@@ -171,8 +205,8 @@ export function HandoffCard({ attrs, locked }: CardProps) {
? `${title} was started — find it in your sessions`
: `Opening ${title}\u2026`}
- {failed && (
-