From 3a87a9e572cf88ee35c75315ec147ec6ecdcd2fa Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:31:18 -0700 Subject: [PATCH] Clarify agent hibernation controls (#5426) --- .../settings/ExperimentalPane.test.tsx | 12 +++-- .../components/settings/ExperimentalPane.tsx | 52 +++++++++---------- .../TabBar.windows-shell-launch.test.ts | 1 + src/renderer/src/i18n/locales/es.json | 23 +++++++- src/renderer/src/i18n/locales/ja.json | 23 +++++++- src/renderer/src/i18n/locales/ko.json | 23 +++++++- src/renderer/src/i18n/locales/zh.json | 23 +++++++- .../src/lib/agent-hibernation-planner.ts | 28 ++++++++-- 8 files changed, 143 insertions(+), 42 deletions(-) diff --git a/src/renderer/src/components/settings/ExperimentalPane.test.tsx b/src/renderer/src/components/settings/ExperimentalPane.test.tsx index 284ac6f55..898a8db12 100644 --- a/src/renderer/src/components/settings/ExperimentalPane.test.tsx +++ b/src/renderer/src/components/settings/ExperimentalPane.test.tsx @@ -20,6 +20,7 @@ afterEach(() => { async function renderExperimentalPane(args: { updateSettings: (settings: Partial) => void + settings?: GlobalSettings }): Promise<{ root: Root; container: HTMLDivElement }> { const container = document.createElement('div') document.body.appendChild(container) @@ -27,7 +28,7 @@ async function renderExperimentalPane(args: { await act(async () => { root.render( ) @@ -56,8 +57,7 @@ describe('ExperimentalPane', () => { expect(settings.experimentalAgentHibernation).toBe(false) expect(settings.agentHibernationIdleMs).toBe(30 * 60 * 1000) expect(markup).toContain('Agent hibernation') - expect(markup).toContain('Hibernate after') - expect(markup).toContain('minutes') + expect(markup).not.toContain('Hibernate after') expect(markup).toContain('aria-checked="false"') expect(getExperimentalPaneSearchEntries().map((entry) => entry.title)).toContain( 'Agent hibernation' @@ -66,7 +66,11 @@ describe('ExperimentalPane', () => { it('renders the agent hibernation idle duration as configurable minutes', async () => { const updateSettings = vi.fn() - const { root, container } = await renderExperimentalPane({ updateSettings }) + const settings = { + ...getDefaultSettings('/tmp'), + experimentalAgentHibernation: true + } + const { root, container } = await renderExperimentalPane({ updateSettings, settings }) const idleInput = container.querySelector( '#experimental-agent-hibernation input[type="number"]' diff --git a/src/renderer/src/components/settings/ExperimentalPane.tsx b/src/renderer/src/components/settings/ExperimentalPane.tsx index 2202a91a9..76e3c5a8e 100644 --- a/src/renderer/src/components/settings/ExperimentalPane.tsx +++ b/src/renderer/src/components/settings/ExperimentalPane.tsx @@ -8,7 +8,6 @@ import { HiddenExperimentalGroup } from './HiddenExperimentalGroup' import { NumberField, SettingsSwitch } from './SettingsFormControls' import { translate } from '@/i18n/i18n' import { - DEFAULT_AGENT_HIBERNATION_IDLE_MS, MAX_AGENT_HIBERNATION_IDLE_MS, MIN_AGENT_HIBERNATION_IDLE_MS, getEffectiveAgentHibernationIdleMs @@ -236,31 +235,32 @@ export function ExperimentalPane({ } /> - - updateSettings({ - // Why: settings persist the planner contract, not the display unit. - agentHibernationIdleMs: minutes * MS_PER_MINUTE - }) - } - /> + {agentHibernationEnabled ? ( + + updateSettings({ + // Why: settings persist the planner contract, not the display unit. + agentHibernationIdleMs: minutes * MS_PER_MINUTE + }) + } + /> + ) : null} ) : null} diff --git a/src/renderer/src/components/tab-bar/TabBar.windows-shell-launch.test.ts b/src/renderer/src/components/tab-bar/TabBar.windows-shell-launch.test.ts index 9f55641fa..5cf3f9923 100644 --- a/src/renderer/src/components/tab-bar/TabBar.windows-shell-launch.test.ts +++ b/src/renderer/src/components/tab-bar/TabBar.windows-shell-launch.test.ts @@ -83,6 +83,7 @@ vi.mock('react', async () => { memo: (component: T) => component, useEffect: () => {}, useLayoutEffect: () => {}, + useCallback: unknown>(callback: T) => callback, useMemo: (factory: () => T) => factory(), useRef: (current: T) => ({ current }), useState: (initial: T | (() => T)) => { diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 3f6833242..7e796cbb4 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -4640,7 +4640,16 @@ "f63ea281e3": "Feed integrado en la barra lateral izquierda para completar agentes y estados de bloqueo.", "ca2219fe5e": "Muestra una pequeña mascota animada fijada en la esquina inferior derecha. Elige un personaje (Claudino, OpenCode, Gremlin) o carga tu propio PNG, APNG, GIF, WebP, JPG o SVG desde el menú de mascotas de la barra de estado. Ocultarlo en cualquier momento desde el mismo menú sin desactivar esta configuración.", "dd6f0a1d45": "Mascota", - "0e89a574ae": "Mascota animada flotante en la esquina inferior derecha." + "0e89a574ae": "Mascota animada flotante en la esquina inferior derecha.", + "agentHibernation": { + "copy": "Detiene los terminales de agentes en segundo plano que estén inactivos después del intervalo configurado y reanuda las sesiones compatibles cuando las vuelves a abrir. Experimental mientras ajustamos el modelo de seguridad.", + "description": "Detiene los terminales de agentes en segundo plano que estén inactivos después del intervalo configurado y reanuda las sesiones compatibles cuando las vuelves a abrir.", + "idleMinutesDescription": "Cuántos minutos inactivo debe esperar un agente en segundo plano completado antes de que Orca pueda hibernarlo.", + "idleMinutesLabel": "Hibernar después de", + "idleMinutesSuffix": "minutos", + "title": "Hibernación de agentes", + "toggleLabel": "Alternar hibernación de agentes" + } }, "FloatingWorkspacePane": { "aeaf76fda9": "Barra de estado", @@ -6692,7 +6701,17 @@ "b54cea709b": "compañero", "051203d37c": "mascota", "6b5a56ac35": "Mascota animada flotante en la esquina inferior derecha.", - "87d99e634b": "Mascota" + "87d99e634b": "Mascota", + "agentHibernation": { + "agent": "agente", + "agents": "agentes", + "description": "Detiene los terminales de agentes en segundo plano que estén inactivos después del intervalo configurado y reanuda las sesiones compatibles cuando se vuelven a abrir.", + "hibernate": "hibernar", + "minutes": "minutos", + "sleep": "suspender", + "terminal": "terminal", + "title": "Hibernación de agentes" + } } }, "floating": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index c07cfccde..f7b049453 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -4625,7 +4625,16 @@ "f63ea281e3": "agent の完了とブロック状態を示すスレッド化された左側のサイドバー フィード。", "ca2219fe5e": "右下隅に固定された小さなアニメーションのペットを表示します。キャラクター (Claudino、OpenCode、Gremlin) を選択するか、ステータス バーのペット メニューから独自の PNG、APNG、GIF、WebP、JPG、または SVG をアップロードします。この設定を無効にしなくても、同じメニューからいつでも非表示にできます。", "dd6f0a1d45": "ペット", - "0e89a574ae": "右下隅に浮かぶアニメーションのペット。" + "0e89a574ae": "右下隅に浮かぶアニメーションのペット。", + "agentHibernation": { + "copy": "設定したアイドル時間が経過したバックグラウンド agent ターミナルを停止し、対応しているセッションは再度開いたときに再開します。安全モデルを調整中の実験的機能です。", + "description": "設定したアイドル時間が経過したバックグラウンド agent ターミナルを停止し、対応しているセッションは再度開いたときに再開します。", + "idleMinutesDescription": "完了したバックグラウンド agent を Orca が休止状態にできるまで待つアイドル時間(分)。", + "idleMinutesLabel": "休止まで", + "idleMinutesSuffix": "分", + "title": "Agent の休止", + "toggleLabel": "Agent の休止を切り替え" + } }, "FloatingWorkspacePane": { "aeaf76fda9": "ステータスバー", @@ -6714,7 +6723,17 @@ "b54cea709b": "相棒", "051203d37c": "ペット", "6b5a56ac35": "右下隅に浮かぶアニメーションのペット。", - "87d99e634b": "ペット" + "87d99e634b": "ペット", + "agentHibernation": { + "agent": "agent", + "agents": "agents", + "description": "設定したアイドル時間が経過したバックグラウンド agent ターミナルを停止し、再度開いたときに対応セッションを再開します。", + "hibernate": "休止", + "minutes": "分", + "sleep": "スリープ", + "terminal": "ターミナル", + "title": "Agent の休止" + } } }, "floating": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index fecc03a43..193a52eb4 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -4625,7 +4625,16 @@ "f63ea281e3": "agent 완료 및 차단 상태에 대한 스레드 왼쪽 사이드바 피드입니다.", "ca2219fe5e": "오른쪽 하단에 고정된 작은 애니메이션 애완동물을 표시합니다. 캐릭터(Claudino, OpenCode, Gremlin)를 선택하거나 상태 표시줄 애완동물 메뉴에서 자신만의 PNG, APNG, GIF, WebP, JPG 또는 SVG를 업로드하세요. 이 설정을 비활성화하지 않고 동일한 메뉴에서 언제든지 숨길 수 있습니다.", "dd6f0a1d45": "애완 동물", - "0e89a574ae": "오른쪽 하단에 떠 있는 애니메이션 애완동물." + "0e89a574ae": "오른쪽 하단에 떠 있는 애니메이션 애완동물.", + "agentHibernation": { + "copy": "설정된 유휴 시간이 지난 백그라운드 agent 터미널을 중지하고, 다시 열 때 지원되는 세션을 재개합니다. 안전 모델을 조정하는 동안 제공되는 실험적 기능입니다.", + "description": "설정된 유휴 시간이 지난 백그라운드 agent 터미널을 중지하고, 다시 열 때 지원되는 세션을 재개합니다.", + "idleMinutesDescription": "완료된 백그라운드 agent를 Orca가 최대 절전 모드로 전환하기 전에 기다릴 유휴 시간(분)입니다.", + "idleMinutesLabel": "최대 절전까지", + "idleMinutesSuffix": "분", + "title": "Agent 최대 절전", + "toggleLabel": "Agent 최대 절전 전환" + } }, "FloatingWorkspacePane": { "aeaf76fda9": "상태 표시줄", @@ -6677,7 +6686,17 @@ "b54cea709b": "친구", "051203d37c": "애완 동물", "6b5a56ac35": "오른쪽 하단에 떠 있는 애니메이션 애완동물.", - "87d99e634b": "애완 동물" + "87d99e634b": "애완 동물", + "agentHibernation": { + "agent": "agent", + "agents": "agents", + "description": "설정된 유휴 시간이 지난 백그라운드 agent 터미널을 중지하고, 다시 열 때 지원되는 세션을 재개합니다.", + "hibernate": "최대 절전", + "minutes": "분", + "sleep": "절전", + "terminal": "터미널", + "title": "Agent 최대 절전" + } } }, "floating": { diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index a61816e23..f0da231ab 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -4625,7 +4625,16 @@ "f63ea281e3": "用于 Agent 完成和阻塞状态的螺纹左侧边栏提要。", "ca2219fe5e": "显示固定在右下角的小动画宠物。从状态栏宠物菜单中选择一个角色(Claudino、OpenCode、Gremlin)或上传您自己的 PNG、APNG、GIF、WebP、JPG 或 SVG。随时从同一菜单隐藏它,而不禁用此设置。", "dd6f0a1d45": "宠物", - "0e89a574ae": "右下角漂浮的动画宠物。" + "0e89a574ae": "右下角漂浮的动画宠物。", + "agentHibernation": { + "copy": "在配置的空闲时间后停止后台 agent 终端,并在你再次打开时恢复受支持的会话。我们仍在调校安全模型,此功能为实验性功能。", + "description": "在配置的空闲时间后停止后台 agent 终端,并在你再次打开时恢复受支持的会话。", + "idleMinutesDescription": "已完成的后台 agent 在 Orca 可以将其休眠前需要等待的空闲分钟数。", + "idleMinutesLabel": "休眠等待时间", + "idleMinutesSuffix": "分钟", + "title": "Agent 休眠", + "toggleLabel": "切换 agent 休眠" + } }, "FloatingWorkspacePane": { "aeaf76fda9": "状态栏", @@ -6677,7 +6686,17 @@ "b54cea709b": "伙伴", "051203d37c": "宠物", "6b5a56ac35": "右下角漂浮的动画宠物。", - "87d99e634b": "宠物" + "87d99e634b": "宠物", + "agentHibernation": { + "agent": "agent", + "agents": "agents", + "description": "在配置的空闲时间后停止后台 agent 终端,并在再次打开时恢复受支持的会话。", + "hibernate": "休眠", + "minutes": "分钟", + "sleep": "睡眠", + "terminal": "终端", + "title": "Agent 休眠" + } } }, "floating": { diff --git a/src/renderer/src/lib/agent-hibernation-planner.ts b/src/renderer/src/lib/agent-hibernation-planner.ts index 6e5a4805c..6e86c4c71 100644 --- a/src/renderer/src/lib/agent-hibernation-planner.ts +++ b/src/renderer/src/lib/agent-hibernation-planner.ts @@ -147,6 +147,28 @@ function signatureFor(worktreeId: string, panes: EligiblePane[]): string { return `${worktreeId}|${parts.join('|')}` } +function getAgentEntriesByTabId( + agentStatusByPaneKey: AgentHibernationPlannerSnapshot['agentStatusByPaneKey'] +): Map { + const entriesByTabId = new Map() + for (const entry of Object.values(agentStatusByPaneKey)) { + if (!entry) { + continue + } + const tabId = getEntryTabId(entry) + if (!tabId) { + continue + } + const entries = entriesByTabId.get(tabId) + if (entries) { + entries.push(entry) + } else { + entriesByTabId.set(tabId, [entry]) + } + } + return entriesByTabId +} + export function planAgentHibernationCandidates( snapshot: AgentHibernationPlannerSnapshot ): AgentHibernationCandidate[] { @@ -156,6 +178,7 @@ export function planAgentHibernationCandidates( const idleMs = getEffectiveAgentHibernationIdleMs(snapshot.settings.agentHibernationIdleMs) const mobileLockedPtyIds = new Set(snapshot.mobileLockedPtyIds) const foregroundWorktreeIds = new Set(snapshot.foregroundWorktreeIds) + const agentEntriesByTabId = getAgentEntriesByTabId(snapshot.agentStatusByPaneKey) const candidates: AgentHibernationCandidate[] = [] for (const [worktreeId, tabs] of Object.entries(snapshot.tabsByWorktree)) { if ( @@ -181,10 +204,7 @@ export function planAgentHibernationCandidates( continue } const layout = snapshot.terminalLayoutsByTabId[tab.id] - for (const entry of Object.values(snapshot.agentStatusByPaneKey)) { - if (!entry || getEntryTabId(entry) !== tab.id) { - continue - } + for (const entry of agentEntriesByTabId.get(tab.id) ?? []) { const eligible = getEligiblePane({ entry, tab,