Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
db4e74e
fix: retain ordinary window startup geometry
WilliamWang1721 Oct 3, 2026
aa89066
fix: preserve full work area and first-show placement ownership
WilliamWang1721 Oct 3, 2026
f99fcba
test: verify native window placement and durable empty geometry
WilliamWang1721 Oct 3, 2026
0afbbde
style: apply remote formatting and tighten platform budget
WilliamWang1721 Oct 3, 2026
5e6d503
test: follow startup geometry ownership for application identity
WilliamWang1721 Oct 3, 2026
5cfb77a
Merge main into codex/issue-window-geometry
WilliamWang1721 Oct 6, 2026
b2eeb18
Merge Kuddev/pebrel main into codex/issue-window-geometry
WilliamWang1721 Oct 11, 2026
2cd2dd1
Merge Kuddev/pebrel main into codex/issue-window-geometry
WilliamWang1721 Oct 11, 2026
c7f3c68
Merge Kuddev/pebrel main into codex/issue-window-geometry
WilliamWang1721 Oct 13, 2026
e475bfa
Merge Kuddev/pebrel main into codex/issue-window-geometry
WilliamWang1721 Oct 13, 2026
8722b04
Merge Kuddev/pebrel main into codex/issue-window-geometry
WilliamWang1721 Oct 14, 2026
88d9e0b
Merge Kuddev/pebrel main into codex/issue-window-geometry
WilliamWang1721 Oct 14, 2026
cdd2738
Merge Kuddev/pebrel main into codex/issue-window-geometry
WilliamWang1721 Oct 14, 2026
4572059
Merge Kuddev/pebrel main into codex/issue-window-geometry
WilliamWang1721 Oct 15, 2026
8259245
ci: retrigger after Windows git metadata timeout
WilliamWang1721 Oct 15, 2026
00f455c
Merge Kuddev/pebrel main into codex/issue-window-geometry
WilliamWang1721 Oct 15, 2026
d99d9e7
Merge Kuddev/pebrel main into codex/issue-window-geometry
WilliamWang1721 Oct 16, 2026
b34e239
Merge Kuddev/pebrel main into codex/issue-window-geometry
WilliamWang1721 Oct 17, 2026
60f79c1
Merge Kuddev/pebrel main into codex/issue-window-geometry
WilliamWang1721 Oct 19, 2026
1a9c3ba
Merge Kuddev/pebrel main into codex/issue-window-geometry
WilliamWang1721 Oct 21, 2026
ab6ac67
Merge Kuddev/pebrel main into codex/issue-window-geometry
WilliamWang1721 Oct 21, 2026
da4e997
Merge Kuddev/pebrel main into codex/issue-window-geometry
WilliamWang1721 Oct 22, 2026
4dae3d8
Merge Kuddev/pebrel main into codex/issue-window-geometry
WilliamWang1721 Oct 23, 2026
52371d6
Merge Kuddev/pebrel main into codex/issue-window-geometry
WilliamWang1721 Oct 23, 2026
0901585
Merge Kuddev/pebrel main into codex/issue-window-geometry
WilliamWang1721 Oct 23, 2026
352a914
Merge branch 'main' into codex/issue-window-geometry
Kuddev Oct 24, 2026
562c3a8
Merge main while retaining geometry and tab transfer regressions
Kuddev Oct 24, 2026
60f8212
Merge branch 'main' into codex/issue-window-geometry
Kuddev Oct 24, 2026
1f6890a
Merge branch 'main' into codex/issue-window-geometry
Kuddev Oct 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Ordinary-window startup geometry

## Status

Proposed for review.

## Context

The GPUI startup path already reads explicit Lua grid dimensions, but ignores
`window.position`. Regular workspace snapshots omit `Session.window`, so the
last size and position disappear when the process exits. Quick Terminal already
owns an independent remembered size and monitor policy.

## Evidence

`workspace/windowing.rs` owns the registry and combined persistence boundary.
`session/window_layout.rs` preserves per-window metadata in the same atomic JSON
file as tabs. The pinned GPUI Windows backend (`fc05d637`) obtains normal bounds
with `GetWindowPlacement`; maximized and fullscreen bounds retain their restore
rectangle. Windows placement uses workspace coordinates, whereas display bounds
and configured positions use desktop coordinates.

## Decision

Attach normal native geometry at the ordinary-window persistence boundary,
including empty tab-close snapshots. Cold restore belongs only to the first
ordinary `RestoreOrDefault` window. New windows, Quick Terminal and isolated
administrator processes never consume this geometry. Geometry restoration is
independent of restoring shell tabs; the boot-attempt cutoff still prevents a
broken saved startup loop. Existing empty-session and background-close policies
remain owned by session persistence.

Sizes remain logical GPUI pixels. Windows saved position is the physical desktop
client origin, paired with the stable display UUID. Restore first chooses the
configured physical monitor, otherwise the remembered display, otherwise the
nearest physical monitor, then the primary display. It clamps size and origin
within that display's work area. Working coordinates are translated only at the
Windows placement boundary. Fullscreen saves its normal rectangle but does not
force a fullscreen startup; maximized state may be restored.

Explicit Lua dimensions override saved size; explicit Lua position overrides
saved position. Missing dimensions are not an implicit explicit default.
Configured Windows position describes the outer window in physical desktop
pixels, as the legacy configuration does. Apply it using `SetWindowPos` because native border insets are known only after
creation. Hidden Windows startup retains a one-shot position until its first
active frame, after GPUI consumes its cached native placement; applying it
in the builder would be overwritten by activation.
Other platforms keep their existing position behavior pending an equivalent
public/native placement contract.

## Rejected alternatives

- Persisting zoom-derived rows/columns: couples layout to font state and loses
the user's actual native size.
- Saving maximized/fullscreen viewport size: destroys the normal restore size.
- Always recentering on primary: discards a still-attached secondary display.
- Reopening all ordinary windows for cold startup: changes tab restoration policy.
- A second geometry file: adds a competing save lifecycle and atomicity boundary.

## Consequences

New JSON fields are optional; older sessions remain readable. The shared session
format still contains flattened tabs and per-window layouts, without a version
bump. Quick Terminal settings remain separate. Native DPI conversion follows the
pinned backend and must be reviewed when that dependency changes.

## Validation

The added GPUI snapshot regression exercises a real registered workspace and
normal native bounds. Session tests atomically roundtrip multiwindow and empty
geometry, accept old metadata, and enforce the crash-loop cutoff. Startup tests
cover explicit grid priority and selected-display offscreen clamping. Execution
and platform acceptance are performed in GitHub Actions; local execution is not
part of this change's evidence.

## Supersedes

None.

## Revisit when

GPUI exposes a cross-platform display scale/position API or cold startup begins
restoring separate ordinary windows.
20 changes: 13 additions & 7 deletions nebula_app/src/gpui_shell/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ fn default_font_family() -> &'static str {
crate::font_install::REQUIRED_FONT_FAMILY
}

/// `config.window.dimensions`:启动窗口的网格行列。
/// 显式窗口网格、物理坐标与首个普通窗口的冷启动恢复值。
///
/// 单独驻留而不是并入 [`Settings`],因为生命周期不同:`Settings` 随设置页写入
/// 反复重载,启动网格按定义只在开窗时生效一次,而 Lua 配置求值不该出现在每次
Expand All @@ -480,6 +480,8 @@ fn default_font_family() -> &'static str {
#[derive(Default)]
pub(crate) struct StartupWindow {
pub dimensions: Option<crate::config::window::Dimensions>,
pub position: Option<crate::config::ui_config::Delta<i32>>,
pub(crate) restored: Option<crate::session::WindowState>,
}

impl Global for StartupWindow {}
Expand Down Expand Up @@ -510,7 +512,11 @@ impl StartupWindow {

/// `dimensions()` 自带「行列必须同时非零」的旧壳合同。
fn from_config(config: &crate::config::UiConfig) -> Self {
Self { dimensions: config.window.dimensions() }
Self {
dimensions: config.window.dimensions(),
position: config.window.position,
restored: None,
}
}
}

Expand Down Expand Up @@ -881,7 +887,7 @@ mod tests {
&path,
"local pebrel = require 'pebrel'\n\
local config = pebrel.config_builder()\n\
config.window = { dimensions = { columns = 123, lines = 37 } }\n\
config.window = { dimensions = { columns = 123, lines = 37 }, position = { x = 197, y = 102 } }\n\
return config\n",
)
.unwrap();
Expand All @@ -892,10 +898,9 @@ mod tests {
])
.unwrap();

assert_eq!(
StartupWindow::load(options.config_file).dimensions,
Some(Dimensions { columns: 123, lines: 37 })
);
let startup = StartupWindow::load(options.config_file);
assert_eq!(startup.dimensions, Some(Dimensions { columns: 123, lines: 37 }));
assert_eq!(startup.position, Some(crate::config::ui_config::Delta { x: 197, y: 102 }));
}

/// 用户写在 `pebrel.lua` 里的 `config.window.dimensions` 必须真的决定启动
Expand Down Expand Up @@ -933,6 +938,7 @@ mod tests {
);
let configured = |columns: usize, lines: usize| StartupWindow {
dimensions: Some(Dimensions { columns, lines }),
..Default::default()
};
cx.update(|cx| {
assert_eq!(startup_grid(cx), builtin);
Expand Down
20 changes: 5 additions & 15 deletions nebula_app/src/gpui_shell/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -838,12 +838,10 @@ impl NebulaWorkspace {
let runtime = nebula_settings::RuntimeSettings::load();
let sidebar_width = runtime.sidebar_width;
windowing::observe_window_bounds(runtime_window_id, window, cx);
let initial_grid = windowing::prepare_initial_grid(
window,
cx,
sidebar_width,
window_role == windowing::WindowRole::Regular,
);
let fit_default_grid = window_role == windowing::WindowRole::Regular
&& !windowing::startup_geometry::restored_size(runtime_window_id, &startup, cx);
let initial_grid =
windowing::prepare_initial_grid(window, cx, sidebar_width, fit_default_grid);
let this = cx.entity().downgrade();
let appearance_sub = window.observe_window_appearance(move |_, cx| {
if let Some(workspace) = this.upgrade() {
Expand Down Expand Up @@ -1541,15 +1539,6 @@ impl NebulaWorkspace {
(0..self.tabs.len()).find_map(|tab_ix| self.busy_process_in_tab(tab_ix, None, cx))
}

fn save_clean_window_session(&mut self, cx: &mut App) -> std::io::Result<()> {
windowing::save_current_window_session(
self.runtime_window_id,
self.snapshot_session(cx),
session_persistence::SaveReason::WindowClose,
cx,
)
}

/// 聚焦另一个 pane(点击上报或方向导航落点)。
fn focus_pane(
&mut self,
Expand Down Expand Up @@ -1672,6 +1661,7 @@ impl NebulaWorkspace {
self.runtime_window_id,
self.snapshot_session(cx),
session_persistence::SaveReason::TabsClosed,
window,
cx,
) {
log::warn!("Could not save closed tabs: {error}");
Expand Down
2 changes: 1 addition & 1 deletion nebula_app/src/gpui_shell/workspace/closing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ impl NebulaWorkspace {
cx.notify();
return;
}
if workspace.save_clean_window_session(cx).is_err() {
if workspace.save_clean_window_session(window, cx).is_err() {
workspace.window_close_pending = false;
let language = crate::gpui_shell::config::ui_language(cx);
crate::gpui_shell::toast::banner(
Expand Down
2 changes: 1 addition & 1 deletion nebula_app/src/gpui_shell/workspace/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ impl Render for NebulaWorkspace {
let theme_transition = self.theme_transition.render(window, cx);
crate::gpui_shell::theme::sync_component_focus_ring(window, cx);
if window.is_window_active() {
windowing::mark_active(self.runtime_window_id, cx);
windowing::mark_active(self.runtime_window_id, window, cx);
}
if !cx.has_active_drag() {
self.cross_window_dock = None;
Expand Down
1 change: 1 addition & 0 deletions nebula_app/src/gpui_shell/workspace/residency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,7 @@ impl NebulaWorkspace {
} else {
super::session_persistence::SaveReason::Checkpoint
},
window,
cx,
) {
log::warn!("Could not checkpoint before hiding window: {error}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ impl Default for SessionPersistence {
}

impl SessionPersistence {
#[cfg(test)]
pub(super) fn ordinary_for_test() -> Self {
Self { isolated: false, ..Self::default() }
}

pub(super) fn update_windows(&self) -> std::io::Result<Vec<Session>> {
// 管理员只在显式更新事务中落盘;commit 会在授权安装前持久保存这份内存快照。
(if self.isolated { &self.latest } else { &self.saved })
Expand Down Expand Up @@ -128,7 +133,7 @@ mod tests {
fn ordinary_window() -> SessionPersistence {
// Hosted Windows runners can be elevated. These tests exercise ordinary
// window persistence; privileged isolation has its own negative test.
SessionPersistence { isolated: false, ..SessionPersistence::default() }
SessionPersistence::ordinary_for_test()
}

fn sample_session() -> Session {
Expand Down
14 changes: 14 additions & 0 deletions nebula_app/src/gpui_shell/workspace/session_recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
use super::*;

impl NebulaWorkspace {
pub(super) fn save_clean_window_session(
&mut self,
window: &Window,
cx: &mut App,
) -> std::io::Result<()> {
windowing::save_current_window_session(
self.runtime_window_id,
self.snapshot_session(cx),
session_persistence::SaveReason::WindowClose,
window,
cx,
)
}

/// 冷启动回退(没有恢复出任何标签)时首个终端的 cwd:与 `add_terminal`
/// 同一合同,设置页「启动目录」优先,未设置或失效时才继承进程启动目录。
pub(super) fn cold_start_cwd() -> Option<std::path::PathBuf> {
Expand Down
1 change: 1 addition & 0 deletions nebula_app/src/gpui_shell/workspace/terminal_activity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ impl NebulaWorkspace {
self.runtime_window_id,
self.snapshot_session(cx),
session_persistence::SaveReason::Checkpoint,
window,
cx,
) {
log::warn!("Could not checkpoint native recovery identity: {error}");
Expand Down
Loading
Loading