Skip to content

Expose shared session inactivity settings in Settings - #14954

Draft
warp-agent-staging[bot] wants to merge 12 commits into
masterfrom
factory/shared-session-inactivity-settings
Draft

Expose shared session inactivity settings in Settings#14954
warp-agent-staging[bot] wants to merge 12 commits into
masterfrom
factory/shared-session-inactivity-settings

Conversation

@warp-agent-staging

@warp-agent-staging warp-agent-staging Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Description

Exposes the three existing shared-session (/remote-control) inactivity durations as user-configurable settings in Settings > Features > Session, below the existing "Confirm before closing shared session" toggle, as one combined widget:

  • Revoke edit access after being inactive forinactivity_period_before_revoking_roles (default 10 min)
  • Warn before ending the session after being inactive forinactivity_period_before_warning (default 25 min)
  • End the shared session after being inactive forinactivity_period_before_ending_session (default 30 min)

Each field accepts 0 to disable that phase, and shows a short inline explanation next to the field when it's disabled (e.g. "Shared sessions will not end after an idle period."). All-zero means the session never idles out at all — an intentionally supported, uncapped configuration.

No other inactivity timeout behavior changed. Defaults are unchanged, the ladder's phase order is unchanged, and what counts as "activity" is unchanged.

Linked Issue

Linear: APP-5313

Changes

This revision responds to review feedback by trimming the original implementation down to the minimum needed to make these three durations user-configurable, correctly:

  • app/src/terminal/shared_session/settings.rs: the three settings move from private: true to private: false with a toml_path and user-facing description. Added InactivityLadderSnapshot, a frozen copy of the three durations that determines which ladder phase should arm next (skipping any disabled/zero phase) — validated that these three durations are cumulative time-since-last-activity, so revoke <= warn <= end is a real invariant, not an assumption. SharedSessionSettings::enforce_inactivity_ordering(ctx) keeps that invariant on every non-UI path (hand-edited settings file, cloud sync, direct writes) by threading a running "floor" through the enabled phases in order, clamping each one up to at least the highest enabled phase before it -- this also covers a disabled middle phase (e.g. revoke=10m, warn=0, end=5m), which a simple pairwise comparison would let slip through uncorrected. It's called once from init(), kept separate from SharedSessionSettings::register(ctx) so registration itself stays the same plain call every other settings group uses. Dropped the legacy private-store migration entirely, since these settings had no UI before this PR, so there's no real customized value to migrate.
  • app/src/settings_view/features_page.rs: the three rows are now rendered by a single SharedSessionTimeoutWidget instead of three near-identical widgets. A duration of 0 shows literally as 0 (not an "Off" sentinel), with short inline text next to the field explaining what zero means for that row. The per-field clamp (clamp_shared_session_*_minutes) keeps revoke <= warn <= end for values edited through this UI.
  • app/src/terminal/view/shared_session/view_impl.rs / sharer/mod.rs: Sharer now holds an InactivityLadderSnapshot captured once when a sharer's idle period begins (reset_sharer_inactivity_timer), and every later phase transition within that same period — including the follow-up phase armed after roles are revoked, and the warning modal's own end-of-session countdown — is judged against that same snapshot rather than live settings. This means a settings change mid-flight leaves the in-progress idle period's whole chain consistently "old"; only a subsequently-reset idle period (e.g. from new activity) picks up the change, matching the review's request that in-flight timers be unaffected without letting a single chain mix old and new durations.

On the "Off" sentinel / slider

The review asked to check whether a slider component (mirroring macOS's idle-sleep slider) already exists before building the "0 = off" state as an input field. A slider component does exist in this codebase (used for opacity/blur), but it's a continuous 0–100 control with no support for the large, non-linear duration range these fields need. Building that mapping would mean building a new slider variant, which was out of scope, so the text-input approach was kept per the fallback guidance.

Testing

  • cargo build --bin warp, cargo fmt -- --check, cargo clippy -p warp --bin warp --all-targets -- -D warnings — all pass.

  • cargo nextest run -p warp filtered to shared_session/settings/features_page — 637 passed, including the zero-disables-a-phase matrix, minute-parsing bounds, clamp correctness for zero and non-zero values, file-originated and cloud-originated ordering-correction regression tests (including a disabled-middle-phase case arriving via an external write), and a regression test for the exact mid-flight settings-change scenario from review (an already-armed timer's snapshot is unaffected by a later settings change, while a freshly-reset idle period picks up the new values).

  • Manually verified end-to-end with computer use on a freshly rebuilt, freshly launched instance: the three rows render as one cohesive block with correct labels/values/descriptions; editing a value (e.g. 107) persists; setting a field to 0 shows the literal 0 plus its inline explanation, and correctly disables/greys out the dependent "Warn" row with its own explanation, with no visual overlap between rows.

  • Known pre-existing issue (tracked separately, not introduced by this PR): APP-5442 — committing a value via Tab in these fields leaves the shared EditorView single-line text input rendering blank until focus moves elsewhere. This reproduces on a plain numeric commit in these same fields and is a characteristic of the shared editor primitive also used by other existing settings fields, not specific to this feature. The persisted value is always correct on the next full render; clicking into a different field (rather than Tab) also commits and displays it correctly.

  • I have manually tested my changes locally with computer use (equivalent to ./script/run)

Screenshots / Videos

Computer-use video recording

Testing Warp Settings > Features > Session inactivity rows: opening Settings, navigating to Features tab, scrolling to Session section, editing Revoke edit access to 7 and End the shared session to 0, verifying no text overlap.
Opening Settings via Command Palette, navigating to Features > Session, showing the default three rows, editing Revoke edit access to 7, then editing End the shared session to 0 — confirming no text overlap in the resulting state.

Computer-use screenshots (2)

Settings > Features > Session section showing the three inactivity rows in default state: Revoke edit access after being inactive for = 10 minutes, Warn before ending the session after being inactive for = 25 minutes, End the shared session after being inactive for = 30 minutes, each with description text below.
Default state of the merged Session widget: all three rows shown together with labels, values, and descriptions.

After setting End the shared session after being inactive for to 0: the End row shows value 0 with inline text "Shared sessions will not end after an idle period." The Warn before ending row above is dimmed with inline text "Nothing to warn about since sessions won't end." and an updated description. No text overlap between rows.
Zero-disabled state: End = 0 with its inline explanation, and the dependent Warn row greyed out with its own explanation — cleanly separated, no overlap.

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

CHANGELOG-IMPROVEMENT: You can now customize how long a shared session can sit idle before edit access is revoked, a warning is shown, and the session ends (including disabling any of the three) in Settings > Features > Session.

Conversation: https://staging.warp.dev/conversation/62aa162e-53df-4eac-8b9e-da340c2d3b08
Run: https://oz.staging.warp.dev/runs/019ff20d-8301-756e-b971-2365db195c8a

This PR was generated with Oz.

Surfaces the three existing shared-session inactivity durations
(revoke edit access, warn, end session) as user-configurable settings
below the existing confirm-close-shared-session toggle. Values are
edited in minutes and are validated so revoke <= warn <= end always
holds (editing one field clamps it against the other two's current
values). No inactivity timeout behavior, defaults, or the sharer
inactivity ladder logic in view_impl.rs change.

The three settings move from private to public (private: false) with
a toml_path and description, matching the convention used by other
user-facing settings (e.g. ShouldConfirmCloseSession), so they persist
via the normal settings pipeline and take effect on the running
session.

APP-5313

Co-Authored-By: Warp Agent <agent@warp.dev>
@warp-agent-staging
warp-agent-staging Bot requested a review from bnavetta August 11, 2026 19:27
@cla-bot cla-bot Bot added the cla-signed label Aug 11, 2026
oz-agent and others added 5 commits August 11, 2026 20:59
…split

Adversarial review of #14954 surfaced five findings; all addressed here:

1. Legacy private-store values for the three inactivity settings are now
   migrated into their new public location via a dedicated one-time
   migration (its own completion marker, independent of the general
   SettingsFileMigrationComplete marker which is already set for existing
   SettingsFile users). Only copies when the public location doesn't
   already have a value.

2. Added SharedSessionSettings::register_and_enforce_inactivity_ordering,
   which corrects out-of-order values at every point they become
   authoritative (initial load/hand-edited file, cloud sync, disk
   hot-reload), and changed the two derived-interval helpers in
   settings.rs to use saturating_sub as defense-in-depth against the two
   latent underflow panics found in the ladder. The ordering comparison is
   isolated in ladder_phase_order_ok so a future zero-disables-a-phase
   change only needs to touch that one predicate.

3. parse_shared_session_inactivity_minutes now rejects any value above
   u64::MAX / 60, preventing the *60-to-seconds conversion from
   overflowing.

4. Split the single SharedSessionInactivityWidget (one shared search_terms
   blob covering three rows) into three independent SettingsWidgets
   (SharedSessionRevokeEditAccessWidget / SharedSessionWarningWidget /
   SharedSessionEndSessionWidget), each with row-scoped search terms, each
   backed by its own editor field on FeaturesPageView (mirroring
   MouseScrollMultiplierWidget) instead of a ChildView-wrapped sub-view,
   since ChildView's dispatch boundary would have broken action routing
   for per-row widgets. This also resolves the enum_variant_names lint
   nonblocking comment and removes the redundant enum-variant doc comments
   finding, since the standalone Action enum they were attached to no
   longer exists.

5. N/A - see (4).

Added tests: legacy-migration survival/no-clobber/idempotency, ordering
correction from storage and cloud sync, a pure saturating_sub regression
test, minute-parsing bounds (including the overflow case), clamp
correctness/idempotency, and a StubWidget-based filter test proving the
three rows are independently searchable.

Verified visually with a freshly built and launched instance: all three
rows render correctly, 'revoke'/'disconnect' searches now match only
their own row, edits persist across Settings modal close/reopen, and
out-of-order input is clamped rather than accepted.

Co-Authored-By: Warp Agent <agent@warp.dev>
Per the requester's confirmed matrix (relayed via the orchestrator): setting any of
the three inactivity durations to 0 disables that phase, and a phase whose end is
disabled also disables the warning (a countdown to an end that never comes would be
misleading). All-zero means no idle timeout at all, matching what ambient-agent
shared sessions already do today.

- shared_session/settings.rs: added InactivityPhase and
  SharedSessionSettings::next_inactivity_phase()/next_phase_after_revoke(), which
  compute which ladder phase should arm next (skipping disabled phases) and for how
  long. ladder_phase_order_ok now exempts zero from the ordering comparison in either
  position, so the boundary-correction pass (TOML load, cloud sync, disk hot-reload)
  never 'corrects' a disabled phase into something else.
- view_impl.rs: reset_sharer_inactivity_timer and revoke_roles_on_inactivity_period_expired
  now gate on which phase is next enabled instead of unconditionally arming the next
  step of the ladder, via a shared arm_inactivity_timer helper. This means
  open_inactivity_warning_modal can now only ever be reached when both the warning and
  end phases are confirmed enabled, so the pre-existing zero-duration
  fires-instantly-on-open latent bug in the modal's countdown can no longer be hit via
  a disabled end.
- features_page.rs: parsing now accepts 0 (previously rejected), the minutes-for-display
  conversion no longer floors a zero duration up to 1, and a new
  shared_session_inactivity_display_text renders 0 as "Off" rather than the digit 0.
  The three clamp_* functions now treat zero as unconditionally acceptable in every
  field and skip a disabled (zero) neighbor when clamping a non-zero value, instead of
  treating zero as the smallest legal bound. The revoke/end rows grey their label when
  their own value is 0; the warning row also greys out and swaps its description when
  the end phase being off has forced it off regardless of its own value.

No other idle timeout behavior changed: the ladder's phase order, revoke/warn/end
durations' defaults, and what counts as activity are all unchanged for the
already-enabled case.

Tests: settings_tests.rs covers the full phase matrix (all-zero, each single phase
disabled, end disabling warning, ordering enforcement leaving zeros alone) plus a
pure zero-exemption test on the ordering predicate; features_page_tests.rs covers
zero acceptance/display and the clamp exemptions.

Co-Authored-By: Warp <agent@warp.dev>
…form

The three commit_shared_session_* functions only rewrote the editor's buffer text
when clamping changed the numeric value. When a user typed a literal "0" and it
wasn't clamped (0 is always accepted as-is), the buffer kept showing the raw digit
"0" instead of being normalized to "Off" until something else (e.g. reopening
Settings) forced a re-render. Always re-rendering the committed value removes that
gap.

Co-Authored-By: Warp <agent@warp.dev>
…egacy migration

Three real bugs found by an adversarial review pass on the zero-disables-a-phase
increment:

1. parse_shared_session_inactivity_minutes only accepted a numeric u64, while the
   display text for zero renders as "Off". Every field re-parses its own committed
   text on Edited/blur, so the moment a field displayed "Off" its own contents no
   longer parsed as valid, which is what was actually driving the red border and the
   post-Tab blank-frame flash observed during visual verification -- not an error-state
   bug in the styling itself. Fixed by having the parser accept the "Off" sentinel
   (case-insensitively) as zero, so display and parse agree on the same vocabulary.
   Also suppressed the trailing "minutes" unit label when a field is Off, since "Off
   minutes" read as a mistake.

2. The zero semantics only held at the moment a timer was armed, not for the rest of a
   live session: TerminalView never reacted to SharedSessionSettings changing while a
   share was already in progress. Concretely, disabling the end duration while the
   warning modal's zero-duration countdown was in flight could still synchronously fire
   TimedOut (the exact latent bug the initial gating was supposed to close, reachable via
   this race instead of via initial configuration), disabling the end duration while its
   own timer was armed didn't stop the session from still ending, and enabling a phase
   from an all-off ladder did nothing until the next throttled activity event. Fixed by
   having Sharer subscribe to the three inactivity settings and, on any change, abort the
   current timer, close the warning modal (via a new
   Sharer::close_inactivity_warning_modal that also stops the modal's own countdown --
   previously only the render-gating flag was cleared, so an in-flight countdown kept
   ticking even once the modal was no longer shown) if the warning phase just became
   disabled, and re-arm from the sharer's current ladder position, tracked via a new
   Sharer::ladder_position (whether roles have already been revoked in this idle period
   or not, so re-arming doesn't re-revoke).

3. The legacy-value migration copied a stored zero straight into the new public setting.
   Before this increment, these settings were private with no UI to request "no timeout
   at all" -- a legacy zero meant an immediate timer, not a deliberate disable request.
   Carrying it over would silently and permanently disable that phase for a user who
   never asked for that, contradicting the explicit "no idle timeout is fine when the
   user asks for it" agreement. Fixed by skipping a legacy private value of exactly zero
   during migration (leaving the public key absent so the non-zero default applies); an
   explicit zero already present in the public location is unaffected, since that one is
   a real, current user choice.

Tests: 3 new transition tests covering the live re-arm cases (disabling end while the
warning timer is armed, disabling end while the end timer is armed, enabling a phase
mid-session from an all-off ladder) and a migration regression test for a legacy
private zero.

Co-Authored-By: Warp <agent@warp.dev>

@bnavetta bnavetta 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.

@warp-factory please address my comments and provide a computer use video of the updated settings UI

Comment thread app/src/settings/init.rs Outdated
Comment thread app/src/settings_view/features_page.rs Outdated
Comment thread app/src/settings_view/features_page.rs
Comment thread app/src/settings_view/features_page.rs
Comment thread app/src/settings_view/features_page.rs
Comment thread app/src/terminal/shared_session/settings.rs Outdated
Comment thread app/src/terminal/shared_session/settings.rs Outdated
Comment thread app/src/terminal/shared_session/settings.rs Outdated
Comment thread app/src/terminal/view/shared_session/sharer/mod.rs Outdated
Comment thread app/src/terminal/view/shared_session/view_impl.rs
@warp-agent-staging

Copy link
Copy Markdown
Contributor Author

On it, @bnavetta. Working through all 14 comments — including validating the additive-vs-cumulative question against how the timers actually consume these durations — and will follow up with a computer-use video of the updated Session settings.

Responding as wilson: Open session · View in factory

Responds to bnavetta's review on the shared-session inactivity settings PR
(APP-5313). The prior revision added a large amount of machinery beyond what's
needed to make the three durations user-configurable; this trims it down.

- Drop the ordering-enforcement pass (register_and_enforce_inactivity_ordering,
  enforce_inactivity_ordering, ladder_phase_order_ok) and the legacy
  private-store migration entirely. Registration is back to a plain
  SharedSessionSettings::register(ctx) call, matching the naming convention of
  every other settings group.
- Drop the live re-arm-on-settings-change machinery (the sharer's
  ladder_position tracking, its settings subscription, and
  TerminalView::handle_shared_session_inactivity_settings_changed). An
  in-flight timer or open warning modal now simply keeps running on the
  duration it was armed with; only a timer armed after a settings change picks
  up the new value.
- Keep the per-field UI clamp (clamp_shared_session_*_minutes) and the
  zero-disables-a-phase ladder logic (InactivityPhase, next_inactivity_phase,
  next_phase_after_revoke, is_warning_phase_enabled): validated against
  master's usage (settings.rs default-value comments say "after a total of N
  min", and view_impl.rs derives inter-phase gaps via subtraction), these
  three durations are cumulative time-since-last-activity, so revoke <= warn
  <= end is a real invariant, not an assumption.
- Replace the "Off" text sentinel with a literal "0" plus inline explanatory
  text next to the field when a phase is disabled (checked: the existing
  slider component is a continuous 0-100 control with no support for the
  discrete, non-linearly-labeled steps this would need, so building a
  bespoke slider was out of scope).
- Merge the three near-identical SharedSession*Widget structs into one
  SharedSessionTimeoutWidget rendering all three rows.
- Fix the two setting descriptions and move the features_page_tests module
  declaration to the bottom of the file.
- Remove tests that only covered removed logic: the migration and
  ordering-enforcement tests in settings_tests.rs, the widget-search-terms
  split test in mod_tests.rs, the display-text test in features_page_tests.rs,
  and the live re-arm tests (and their now-unused start_sharer_session helper)
  in view_impl_tests.rs.
Computer-use verification of the merged SharedSessionTimeoutWidget found
that the "Warn before ending..." row's long inline off-explanation text
("Sessions won't end due to inactivity, so there's nothing to warn
about.") overflowed the row's available width and visually overlapped the
label above it. The row's label/control layout doesn't wrap the control
side, so a long unbounded inline string pushes past the container.

Shorten all three inline explanations to a consistent, short length so
they render cleanly without needing changes to the shared
render_body_item/build_toggle_element layout code (which many other
settings rows depend on). Re-verified with computer use: no overlap in
either the default or zero-disabled state.
…ion-inactivity-settings

# Conflicts:
#	app/src/settings_view/features_page.rs
Addresses two findings from an independent review of the prior trim-down:

1. Dropping the revoke<=warn<=end ordering enforcement entirely went too
   far -- public TOML hot reload, cloud sync, and direct setting writes can
   all still persist an out-of-order triple, and saturating_sub only makes
   that safe, not correct (it silently collapses the gap to zero instead of
   producing the configured duration). Restored the enforcement, but per
   bnavetta's original naming-convention comment, kept it out of
   SharedSessionSettings::register/init.rs's register_all_settings: it's
   wired up as a separate SharedSessionSettings::enforce_inactivity_ordering(ctx)
   call in init(), alongside this file's other post-registration
   subscriptions. Added back file-originated and cloud-originated regression
   tests.

2. Removing the live re-arm-on-settings-change subscription left the ladder
   *partially* affected by a mid-flight settings change: an already-armed
   timer kept running on its original duration (as intended), but the next
   phase transition recomputed its gap from live (possibly changed)
   settings, mixing old and new durations within one idle period. Added
   InactivityLadderSnapshot, a frozen copy of the three durations captured
   once when an idle period begins (reset_sharer_inactivity_timer) and
   consulted by every later transition in that same period
   (revoke_roles_on_inactivity_period_expired, and the warning modal's own
   end-of-session countdown), so a single chain is now wholly old or wholly
   re-armed depending on which idle period it belongs to. Added a
   regression test covering the reviewer's exact example.

Verified: cargo fmt -- --check, clippy -D warnings, cargo build, and 637
passing tests covering shared_session/settings/features_page.
…ng comments

- Replace pairwise ladder_phase_order_ok with a floor-based sequence check
  (advance_ladder_floor) so a disabled middle phase (e.g. warn=0) can no
  longer let its two enabled neighbors skip comparison against each other.
  Previously revoke=10m, warn=0, end=5m was accepted because both pairwise
  comparisons ignored the zero warn, leaving end unclamped against revoke.
- Add a regression test for a zero-middle triple arriving via an external
  (public/cloud) write, plus a focused unit test for the new floor-advance
  helper.
- Strip revision-narrating and caller-enumerating comments introduced on
  this branch (init.rs, settings.rs, view_impl.rs, sharer/mod.rs) per the
  warp-comments skill; keep only local behavioral rationale.
- Remove advance_ladder_floor_leaves_disabled_phases_alone_but_keeps_the_floor_for_later_ones:
  it pinned a private implementation detail, and
  zero_middle_phase_does_not_let_its_two_enabled_neighbors_skip_comparison
  already covers the same behavior through registration and enforcement.
- Collapse advance_ladder_floor's doc comment to one behavioral sentence.
@warp-agent-staging

Copy link
Copy Markdown
Contributor Author

All 14 comments are addressed and their threads resolved, and a computer-use video of the updated Session settings is in the PR description. Head is 881beb00e. Three things worth your eyes, @bnavetta:

Additive vs. cumulative (r3849525801, r3849599546) — validated as cumulative, not additive. On master the defaults are documented "after a total of N min", and the timer chain subtracts earlier deadlines: revoke is armed with its raw value, the next timer with warn - revoke, and the modal counts down end - warn — summing to exactly 10/25/30 elapsed from last activity. So revoke <= warn <= end is a real invariant and the clamp stays.

Ordering enforcement (r3809150026) — I read that as a naming objection rather than a request to drop the enforcement. init.rs is back to the plain register(ctx) convention with enforcement as a separate post-registration call. It stayed because TOML hot-reload and cloud sync can still persist an out-of-order triple, which saturating_sub makes safe but not correct.

Slider vs. text (r3849552445) — the existing slider is a continuous 0–100 control (opacity/blur) that doesn't fit these ranges, so per your instruction it stayed a text input: a literal 0 with inline explanatory text, no "Off" string.

Also dropped per your comments: the legacy private-store migration, the live re-arm subscription, and the added mod_tests.rs tests. Still a draft — over to you.

Responding as wilson: Open session · View in factory

@warp-agent-staging
warp-agent-staging Bot requested a review from bnavetta August 25, 2026 09:25
@warp-agent-staging

Copy link
Copy Markdown
Contributor Author

One honest caveat on the hand-off: the Check CI results gate is red, but not for anything in this diff. Compute workflow parameters was skipped, so the whole chain below it (fmt/clippy/tests) never ran — and it has been skipped on this branch since before your review, including at 9ac63967. It only started surfacing as a failure rather than a skip once the branch merged current master and picked up the newer gate. Several other open drafts (#15513, #15518) are red the same way right now, while #15516 has the params job running normally, so it looks like a repo-wide condition rather than something about this branch.

That does mean CI has never exercised this PR, so the only executed evidence is local: cargo build --bin warp, cargo fmt -- --check, cargo clippy -p warp --bin warp --all-targets -- -D warnings clean, and 637 tests passing across the shared_session/settings/features_page filters. Worth a real CI pass before merge.

Responding as wilson: Open session · View in factory

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants