Clarify agent hibernation controls (#5426)

This commit is contained in:
Brennan Benson 2026-06-15 16:31:18 -07:00 committed by GitHub
parent f3f41e3e2b
commit 3a87a9e572
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 143 additions and 42 deletions

View File

@ -20,6 +20,7 @@ afterEach(() => {
async function renderExperimentalPane(args: {
updateSettings: (settings: Partial<GlobalSettings>) => 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(
<ExperimentalPane
settings={getDefaultSettings('/tmp')}
settings={args.settings ?? getDefaultSettings('/tmp')}
updateSettings={args.updateSettings}
/>
)
@ -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<HTMLInputElement>(
'#experimental-agent-hibernation input[type="number"]'

View File

@ -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({
}
/>
</div>
<NumberField
label={translate(
'auto.components.settings.ExperimentalPane.agentHibernation.idleMinutesLabel',
'Hibernate after'
)}
description={translate(
'auto.components.settings.ExperimentalPane.agentHibernation.idleMinutesDescription',
'How many idle minutes a completed background agent must wait before Orca can hibernate it.'
)}
value={agentHibernationIdleMinutes}
defaultValue={DEFAULT_AGENT_HIBERNATION_IDLE_MS / MS_PER_MINUTE}
min={MIN_AGENT_HIBERNATION_IDLE_MS / MS_PER_MINUTE}
max={MAX_AGENT_HIBERNATION_IDLE_MS / MS_PER_MINUTE}
step={1}
suffix={translate(
'auto.components.settings.ExperimentalPane.agentHibernation.idleMinutesSuffix',
'minutes'
)}
onChange={(minutes) =>
updateSettings({
// Why: settings persist the planner contract, not the display unit.
agentHibernationIdleMs: minutes * MS_PER_MINUTE
})
}
/>
{agentHibernationEnabled ? (
<NumberField
label={translate(
'auto.components.settings.ExperimentalPane.agentHibernation.idleMinutesLabel',
'Hibernate after'
)}
description={translate(
'auto.components.settings.ExperimentalPane.agentHibernation.idleMinutesDescription',
'How many idle minutes a completed background agent must wait before Orca can hibernate it.'
)}
value={agentHibernationIdleMinutes}
min={MIN_AGENT_HIBERNATION_IDLE_MS / MS_PER_MINUTE}
max={MAX_AGENT_HIBERNATION_IDLE_MS / MS_PER_MINUTE}
step={1}
suffix={translate(
'auto.components.settings.ExperimentalPane.agentHibernation.idleMinutesSuffix',
'minutes'
)}
onChange={(minutes) =>
updateSettings({
// Why: settings persist the planner contract, not the display unit.
agentHibernationIdleMs: minutes * MS_PER_MINUTE
})
}
/>
) : null}
</SearchableSetting>
) : null}

View File

@ -83,6 +83,7 @@ vi.mock('react', async () => {
memo: <T>(component: T) => component,
useEffect: () => {},
useLayoutEffect: () => {},
useCallback: <T extends (...args: never[]) => unknown>(callback: T) => callback,
useMemo: <T>(factory: () => T) => factory(),
useRef: <T>(current: T) => ({ current }),
useState: <T>(initial: T | (() => T)) => {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -147,6 +147,28 @@ function signatureFor(worktreeId: string, panes: EligiblePane[]): string {
return `${worktreeId}|${parts.join('|')}`
}
function getAgentEntriesByTabId(
agentStatusByPaneKey: AgentHibernationPlannerSnapshot['agentStatusByPaneKey']
): Map<string, AgentStatusEntry[]> {
const entriesByTabId = new Map<string, AgentStatusEntry[]>()
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,