{description}
++ {translate('auto.components.sleepy-mode.SleepyModeOverlay.quiet', 'No agents working')} +
+ ) + } + + return ( ++ 0 && !reducedMotion + ? 'size-1.5 animate-pulse rounded-full bg-foreground' + : 'size-1.5 rounded-full bg-muted-foreground' + } + /> + {parts.join(' · ')} +
+ ) +} + +/** + * A full-window resting screen for a fleet left running. It covers the workspace (and whatever + * the agents have on screen) with a clock and a live fleet summary, and gets out of the way on + * the first input. Display sleep is not touched here — "Keep computer awake" owns that. + */ +export default function SleepyModeOverlay(): React.JSX.Element | null { + const active = useAppStore((s) => s.sleepyModeActive) + const setActive = useAppStore((s) => s.setSleepyModeActive) + const idleMinutes = useAppStore((s) => s.settings?.sleepyModeIdleMinutes) + + const delayMs = sleepyModeIdleDelayMs(normalizeSleepyModeIdleMinutes(idleMinutes)) + const start = useCallback(() => setActive(true), [setActive]) + useSleepyModeIdleTrigger({ delayMs, suspended: active, onIdle: start }) + + const now = useClock(active) + const timeFormat = useMemo( + () => new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }), + [] + ) + const dateFormat = useMemo( + () => new Intl.DateTimeFormat(undefined, { weekday: 'long', month: 'long', day: 'numeric' }), + [] + ) + + useEffect(() => { + if (!active) { + return + } + const wake = (): void => setActive(false) + // Capture phase: xterm and Monaco stop plenty of events before they reach window. + const options = { capture: true } as const + window.addEventListener('keydown', wake, options) + window.addEventListener('pointerdown', wake, options) + window.addEventListener('wheel', wake, options) + return () => { + window.removeEventListener('keydown', wake, options) + window.removeEventListener('pointerdown', wake, options) + window.removeEventListener('wheel', wake, options) + } + }, [active, setActive]) + + if (!active) { + return null + } + + return ( ++ {timeFormat.format(now)} +
+{dateFormat.format(now)}
++ {translate( + 'auto.components.sleepy-mode.SleepyModeOverlay.dismissHint', + 'Press any key to wake' + )} +
+{timeFormat.format(now)} diff --git a/tests/e2e/sleepy-mode.spec.ts b/tests/e2e/sleepy-mode.spec.ts new file mode 100644 index 00000000000..0521f195b82 --- /dev/null +++ b/tests/e2e/sleepy-mode.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from './helpers/orca-app' +import { waitForSessionReady } from './helpers/store' + +test('Sleepy Mode covers the window from the status bar and wakes on a keypress', async ({ + orcaPage +}) => { + await waitForSessionReady(orcaPage) + + const awakeStatus = orcaPage.getByRole('button', { name: /^Keep computer awake/ }) + await expect(awakeStatus).toBeVisible() + await awakeStatus.click() + + await orcaPage.getByRole('menuitem', { name: 'Start Sleepy Mode' }).click() + + const scene = orcaPage.getByRole('dialog', { name: 'Sleepy Mode' }) + await expect(scene).toBeVisible() + await expect(scene.getByText('No agents working')).toBeVisible() + await expect(scene.getByText('Press any key to wake')).toBeVisible() + + const proofPath = process.env.ORCA_SLEEPY_MODE_PROOF_PATH + if (proofPath) { + await orcaPage.screenshot({ path: proofPath }) + } + + await orcaPage.keyboard.press('Space') + await expect(scene).toBeHidden() +}) From b6338f842518d7511058e01fe717e6a7b4fa102f Mon Sep 17 00:00:00 2001 From: danny <> Date: Sun, 20 Sep 2026 15:59:36 +0700 Subject: [PATCH 3/8] feat(sleepy-mode): give the scene an ambient background A flat fill read as a blank window rather than a resting screen. The scene now sits on a radial wash mixed from the existing tokens, so it gains depth without introducing a colour. The direction flips per theme: light mode can't go brighter than the canvas, so it vignettes the edges; dark mode lights a pale pool behind the clock. --- src/renderer/src/assets/main.css | 19 +++++++++++++++++++ .../sleepy-mode/SleepyModeOverlay.tsx | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 4edd4da2fde..77c3d5ad358 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -864,6 +864,25 @@ html.native-shell .app-layout { height: calc(var(--browser-page-viewport-height) / var(--ui-zoom-factor, 1)); } +/* Why: the Sleepy Mode scene needs depth without color — a pale pool behind the + clock fading outward. Direction flips per theme: light mode can't go brighter + than #fff, so it vignettes the edges instead of lighting the middle. */ +.sleepy-mode-scene { + background: radial-gradient( + 125% 85% at 50% 42%, + var(--background) 0%, + color-mix(in srgb, var(--foreground) 6%, var(--background)) 100% + ); +} + +.dark .sleepy-mode-scene { + background: radial-gradient( + 125% 85% at 50% 42%, + color-mix(in srgb, var(--foreground) 8%, var(--background)) 0%, + var(--background) 70% + ); +} + /* Why: small identity anchor on desktop custom titlebars where native window chrome is hidden. Sized to sit comfortably in the 36px titlebar with a little horizontal breathing room. The SVG fill is white; light mode inverts diff --git a/src/renderer/src/components/sleepy-mode/SleepyModeOverlay.tsx b/src/renderer/src/components/sleepy-mode/SleepyModeOverlay.tsx index a2a1c4a506b..5de23058318 100644 --- a/src/renderer/src/components/sleepy-mode/SleepyModeOverlay.tsx +++ b/src/renderer/src/components/sleepy-mode/SleepyModeOverlay.tsx @@ -134,7 +134,7 @@ export default function SleepyModeOverlay(): React.JSX.Element | null { role="dialog" aria-modal="true" aria-label={translate('auto.components.sleepy-mode.SleepyModeOverlay.label', 'Sleepy Mode')} - className="fixed inset-0 z-[95] flex flex-col items-center justify-center gap-3 bg-background" + className="sleepy-mode-scene fixed inset-0 z-[95] flex flex-col items-center justify-center gap-3" >
{timeFormat.format(now)}
From 3acd26bdb09825bc3d52ad0d58cfde42e9ddfc33 Mon Sep 17 00:00:00 2001
From: danny <>
Date: Sun, 20 Sep 2026 16:05:58 +0700
Subject: [PATCH 4/8] feat(sleepy-mode): put the pet on the resting screen
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Orca already ships Claudino and animates it off live agent state, but only as
a draggable corner overlay. The resting screen now shows the same pet centred
above the clock, so the scene reads the fleet at a glance: it runs while agents
work, waits when one is blocked, and idles when the night is quiet.
The rendering half of PetOverlay (sprite sheet stepping, auto-detected frames,
the bob keyframes, the document-visibility gate) moves to PetSprite so both
surfaces share one implementation instead of a copy. PetOverlay keeps all of
its positioning, drag, and persistence behaviour.
The scene's pet is not gated on the experimentalPet flag — that flag owns the
draggable overlay, not the artwork — but an explicit "Hide pet" still hides it.
---
.../src/components/pet/PetOverlay.tsx | 279 +----------------
src/renderer/src/components/pet/PetSprite.tsx | 280 ++++++++++++++++++
.../sleepy-mode/SleepyModeOverlay.test.tsx | 10 +-
.../sleepy-mode/SleepyModeOverlay.tsx | 38 +++
4 files changed, 341 insertions(+), 266 deletions(-)
create mode 100644 src/renderer/src/components/pet/PetSprite.tsx
diff --git a/src/renderer/src/components/pet/PetOverlay.tsx b/src/renderer/src/components/pet/PetOverlay.tsx
index 590a8b196af..f87bf00aaca 100644
--- a/src/renderer/src/components/pet/PetOverlay.tsx
+++ b/src/renderer/src/components/pet/PetOverlay.tsx
@@ -1,233 +1,13 @@
-import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
+import { useCallback, useEffect, useState } from 'react'
import { usePrefersReducedMotion } from '@/hooks/usePrefersReducedMotion'
-import { usePetUrl } from './usePetUrl'
-import type { DetectedSpriteCacheEntry } from './pet-blob-cache'
-import type { CustomPet } from '../../../../shared/pet-types'
import { useAppStore } from '../../store'
-import { getAgentStatusEpochNow } from '@/lib/agent-status-epoch-clock'
-import { AGENT_STATUS_STALE_AFTER_MS } from '../../../../shared/agent-status-types'
-import {
- selectPetAnimationName,
- type PetAnimationName,
- type PetDragAnimation
-} from './pet-agent-state'
import { usePetPointerInteraction } from './usePetPointerInteraction'
-import { buildSpriteAnimationCss } from './sprite-animation-css'
-
-type Sprite = NonNullable
- )}
+
{timeFormat.format(now)}
From a1e1f7add567ef971fcab7ca0aa735740f052d03 Mon Sep 17 00:00:00 2001 From: danny <> Date: Sun, 20 Sep 2026 16:31:03 +0700 Subject: [PATCH 5/8] chore(sleepy-mode): sync the runtime English catalog `pnpm lint` regenerates en-runtime-required.json from the keys the renderer reaches at boot; the new Sleepy Mode search keywords were missing from it. Also lets the E2E capture a before shot for the PR. --- src/renderer/src/i18n/en-runtime-required.json | 10 ++++++++++ tests/e2e/sleepy-mode.spec.ts | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index f28c32e0b8a..e1139d31d80 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -1766,6 +1766,16 @@ "refreshLinks": "Refresh", "showButton": "Show Skills button" }, + "sleepy-mode": { + "search": { + "clock": "clock", + "idle": "idle", + "overnight": "overnight", + "privacy": "privacy", + "screensaver": "screensaver", + "sleepy": "sleepy" + } + }, "ssh": { "search": { "62826efbe9": "Add a new remote SSH target.", diff --git a/tests/e2e/sleepy-mode.spec.ts b/tests/e2e/sleepy-mode.spec.ts index 0521f195b82..39a21bf232c 100644 --- a/tests/e2e/sleepy-mode.spec.ts +++ b/tests/e2e/sleepy-mode.spec.ts @@ -6,6 +6,11 @@ test('Sleepy Mode covers the window from the status bar and wakes on a keypress' }) => { await waitForSessionReady(orcaPage) + const beforePath = process.env.ORCA_SLEEPY_MODE_BEFORE_PROOF_PATH + if (beforePath) { + await orcaPage.screenshot({ path: beforePath }) + } + const awakeStatus = orcaPage.getByRole('button', { name: /^Keep computer awake/ }) await expect(awakeStatus).toBeVisible() await awakeStatus.click() From 78364c392cd72ff8fd212110d52913616f84d845 Mon Sep 17 00:00:00 2001 From: danny <> Date: Sun, 20 Sep 2026 16:57:57 +0700 Subject: [PATCH 6/8] fix(sleepy-mode): trigger on the OS input clock, swallow the wake key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two interaction defects from review. The wake listener dismissed the scene but let the same event through, so the key that woke Orca also typed into the terminal or editor behind it. It is now consumed before the scene closes. The idle countdown listened for renderer events, which miss the two places work actually happens: xterm and Monaco stop propagation on what they handle, and browser panes are `