Skip to content

gpui: Prevent idle sleep during AI response streaming - #53130

Open
cppcoffee wants to merge 3 commits into
zed-industries:mainfrom
cppcoffee:prevent_idle_sleep_ai
Open

gpui: Prevent idle sleep during AI response streaming#53130
cppcoffee wants to merge 3 commits into
zed-industries:mainfrom
cppcoffee:prevent_idle_sleep_ai

Conversation

@cppcoffee

@cppcoffee cppcoffee commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

Release Notes:

  • Added button tu prevent idle sleep during AI response streaming

Context / Motivation:
When an AI agent takes a long time to generate a response, the operating system might enter an idle sleep (or display sleep) state if there is no user interaction (mouse or keyboard). This can interrupt the workflow. This PR aims to prevent the system from sleeping during an active AI turn by invoking native OS APIs, and provides a UI toggle for users to control this behavior.

Key Changes:

  1. Cross-Platform Idle Sleep Prevention in GPUI (gpui, gpui_*)

    • Added a new prevent_idle_sleep method to the Platform trait. It returns a RAII-based PreventIdleSleepToken, which automatically restores the original sleep policy when dropped.
    • macOS: Utilizes NSProcessInfo's beginActivityWithOptions with NSActivityIdleDisplaySleepDisabled.
    • Windows: Uses SetThreadExecutionState to prevent display and system sleep, incorporating a reference counter to handle concurrent requests safely.
    • Linux: Uses ashpd to request org.freedesktop.portal.Inhibit (inhibiting Idle and Suspend states) via D-Bus.
  2. AI Agent State Integration (crates/acp_thread)

    • Introduced idle sleep prevention logic in AcpThread: Acquires the token at the beginning of run_turn, and automatically releases it when the turn completes normally, errors out, or is explicitly canceled by the user.
    • Added comprehensive unit tests to ensure the token is correctly acquired and released under various cancellation and concurrency scenarios.
  3. Settings and UI (crates/agent_settings, crates/agent_ui)

    • Added AgentOutputIdleSleepControl to global state.
    • Added a new toggle button (screen icon) to the toolbar at the bottom of the ThreadView (AI conversation panel). Users can hover to view the tooltip and click to toggle the feature on or off.
截屏2026-04-04 16 09 08

@cla-bot cla-bot Bot added the cla-signed The user has signed the Contributor License Agreement label Apr 4, 2026
@cppcoffee
cppcoffee marked this pull request as ready for review April 4, 2026 08:19
@zed-codeowner-coordinator
zed-codeowner-coordinator Bot requested review from a team, danilo-leal and maxbrunsfeld and removed request for a team April 4, 2026 08:19
@hahahuy

hahahuy commented Apr 10, 2026

Copy link
Copy Markdown

Hi, I'd like to take a stab at this.

After reading the code, the root cause is clear: PromptResponse in agent-client-protocol already has a usage: Option<Usage> field (gated on the unstable_session_usage feature, which Zed already enables via the unstable feature flag). But the Ok(r) branch in run_turn (acp_thread.rs) never reads it — so token_usage stays None and render_token_usage() in thread_view.rs returns early and renders nothing.

My planned fix:

  • In crates/acp_thread/src/acp_thread.rs, extract r.usage from the PromptResponse at the end of each turn and call update_token_usage() with the data.
  • acp::Usage has total_tokens, input_tokens, output_tokens but no max_tokens (the ACP protocol doesn't expose context window size). I'll set max_tokens = 0 for now — render_token_usage already handles this gracefully (progress ratio defaults to 0.0, counts are still displayed in the tooltip).

The rendering side already works — it just never fires for external ACP agents today. This is a ~15-line change.

Before I open a draft PR: is there anything already in-flight here I should be aware of? Also, should I build on PR #50360 or start fresh from main?

@maxbrunsfeld maxbrunsfeld left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a good change

We do already have some similar logic, for macOS only, here:

pub struct PreventAppNapGuard {
activity: id,
}
// The activity token returned by NSProcessInfo is thread-safe
unsafe impl Send for PreventAppNapGuard {}
// From NSProcessInfo.h
const NS_ACTIVITY_IDLE_SYSTEM_SLEEP_DISABLED: u64 = 1 << 20;
const NS_ACTIVITY_USER_INITIATED: u64 = 0x00FFFFFF | NS_ACTIVITY_IDLE_SYSTEM_SLEEP_DISABLED;
const NS_ACTIVITY_USER_INITIATED_ALLOWING_IDLE_SYSTEM_SLEEP: u64 =
NS_ACTIVITY_USER_INITIATED & !NS_ACTIVITY_IDLE_SYSTEM_SLEEP_DISABLED;
impl PreventAppNapGuard {
pub fn new() -> Self {
unsafe {
let process_info = NSProcessInfo::processInfo(nil);
#[allow(clippy::disallowed_methods)]
let reason = NSString::alloc(nil).init_str("Audio playback in progress");
let activity: id = msg_send![process_info, beginActivityWithOptions:NS_ACTIVITY_USER_INITIATED_ALLOWING_IDLE_SYSTEM_SLEEP reason:reason];
let _: () = msg_send![reason, release];
let _: () = msg_send![activity, retain];
Self { activity }
}
}
}
impl Drop for PreventAppNapGuard {
fn drop(&mut self) {
unsafe {
let process_info = NSProcessInfo::processInfo(nil);
let _: () = msg_send![process_info, endActivity:self.activity];
let _: () = msg_send![self.activity, release];
}
}
}
.

In that case, we're telling the OS not to throttle the app because we are playing audio.

As part of this PR, could you replace that PreventAppNapGuard with your new cross-platform abstraction?

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>
@cppcoffee
cppcoffee force-pushed the prevent_idle_sleep_ai branch from 532dfb4 to be01ad5 Compare June 7, 2026 08:28
@cppcoffee

cppcoffee commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

This seems like a good change

We do already have some similar logic, for macOS only, here:

pub struct PreventAppNapGuard {
activity: id,
}
// The activity token returned by NSProcessInfo is thread-safe
unsafe impl Send for PreventAppNapGuard {}
// From NSProcessInfo.h
const NS_ACTIVITY_IDLE_SYSTEM_SLEEP_DISABLED: u64 = 1 << 20;
const NS_ACTIVITY_USER_INITIATED: u64 = 0x00FFFFFF | NS_ACTIVITY_IDLE_SYSTEM_SLEEP_DISABLED;
const NS_ACTIVITY_USER_INITIATED_ALLOWING_IDLE_SYSTEM_SLEEP: u64 =
NS_ACTIVITY_USER_INITIATED & !NS_ACTIVITY_IDLE_SYSTEM_SLEEP_DISABLED;
impl PreventAppNapGuard {
pub fn new() -> Self {
unsafe {
let process_info = NSProcessInfo::processInfo(nil);
#[allow(clippy::disallowed_methods)]
let reason = NSString::alloc(nil).init_str("Audio playback in progress");
let activity: id = msg_send![process_info, beginActivityWithOptions:NS_ACTIVITY_USER_INITIATED_ALLOWING_IDLE_SYSTEM_SLEEP reason:reason];
let _: () = msg_send![reason, release];
let _: () = msg_send![activity, retain];
Self { activity }
}
}
}
impl Drop for PreventAppNapGuard {
fn drop(&mut self) {
unsafe {
let process_info = NSProcessInfo::processInfo(nil);
let _: () = msg_send![process_info, endActivity:self.activity];
let _: () = msg_send![self.activity, release];
}
}
}

.
In that case, we're telling the OS not to throttle the app because we are playing audio.

As part of this PR, could you replace that PreventAppNapGuard with your new cross-platform abstraction?

rebase main branch. use PreventAppNapGuard done

@smitbarmase smitbarmase added the area:gpui GPUI rendering framework support label Jun 29, 2026

@SomeoneToIgnore SomeoneToIgnore left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the idea — I've polished it based on the feedback, seems good enough to merge after this Wednesday's release and test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:gpui GPUI rendering framework support cla-signed The user has signed the Contributor License Agreement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants