Private
Public Access
0
0

fix(layout): strip stale dockspace IDs from bundled INI; force live-session apply

Bundled layouts/default.ini (relocated from tests/artifacts/ in Phase 1)
contained a [Docking] data block with a hardcoded DockSpace ID 0xAFBEEF01
plus per-window DockId references to nodes 0x10 and 0x11. Those IDs were
captured at the time the layout was first generated; on any fresh session
HelloImgui computes dockspace IDs dynamically (typically a hash of the
dockspace name + creation order) so the hardcoded literal is stale by the
first render and the orphan docking instructions are silently dropped.

Result: window positions stored in the INI render the windows as
floating at their absolute Pos coordinates, but the auto-created
dockspace captures the full window body, hiding them all. User observed
empty dockspace with only the menu ribbon rendering.

Two-part fix:

1. layouts/default.ini: remove [Docking] data block and per-window DockId
   lines. Comment rewritten to explain why the auto-dock strategy is the
   only session-stable option. Each [Window] entry now has only Pos + Size
   + Collapsed=0, so HelloImgui's auto-dock layer places the panels as
   tabs in the central dockspace on first render.

2. _install_default_layout_if_empty: after writing the bundled INI to
   disk, also call imgui.load_ini_settings_from_memory(src_text) to force
   the live HelloImgui session to apply the new INI. Without this, the
   install only takes effect on the NEXT launch (since HelloImgui reads
   cwd/manualslop_layout.ini BEFORE the post_init callback fires). With it,
   first-launch panels appear immediately.

Tests:
- tests/test_default_layout_install.py assertions updated: instead of
  checking for a per-window DockId line, the install now verifies (a)
  [Window][Project Settings] entry exists, (b) the INI has at least one
  [Window] entry, (c) the INI has no [Docking] data block.
- New _assert_live_session_apply() on tests 1 and 2 verifies the
  "(and applied to live session)" log line appears in stderr, confirming
  imgui.load_ini_settings_from_memory was invoked.

17/17 tests pass (3 install + 2 reset_layout + 8 adjacent gui/commands).
This commit is contained in:
2026-06-29 19:08:49 -04:00
parent 15cd12624f
commit e965451842
3 changed files with 95 additions and 77 deletions
+52 -67
View File
@@ -1,109 +1,94 @@
;;; ;;;
;;; Manual Slop default docking layout for live_gui test sessions ;;; Manual Slop default docking layout for live_gui test sessions
;;; ;;;
;;; This file is loaded by the live_gui test fixture to give every test ;;; Layout strategy: each window entry has Pos + Size + Collapsed=0 set
;;; session a deterministic starting layout. The fixture copies this file ;;; explicitly so the window is registered at a known absolute position.
;;; into the test workspace (tests/artifacts/live_gui_workspace/) before ;;; No docking data block and no DockId references -- HelloImgui dockspace
;;; spawning the sloppy.py subprocess; HelloImGui picks it up on launch. ;;; IDs are computed dynamically per session (typically a hash of the
;;; dockspace name and creation order), so any DockSpace ID literal baked
;;; into an INI is stale by the next render of a fresh session and its
;;; docking instructions are dropped as orphan. Letting HelloImgui's
;;; auto-dock layer handle the layout (placing windows as tabs in the
;;; central dockspace) is the only session-stable option.
;;; ;;;
;;; Organization (matches the user's preferred arrangement): ;;; Window list (matches src/app_controller.py:_default_windows defaults
;;; Left column (DockNode 0x10): ;;; plus the four Tier panels that the user prefers visible):
;;; Tab 0: Project Settings ;;; Pos=0,29 Size=600,400 : Project Settings, Files and Media,
;;; Tab 1: Files & Media ;;; AI Settings, Operations Hub, Theme
;;; Tab 2: AI Settings ;;; Pos=600,29 Size=600,400 : Discussion Hub, Log Management,
;;; Tab 3: Operations Hub ;;; Diagnostics
;;; Right column (DockNode 0x11): ;;; Pos=0,432 Size=400,300 : Tier 1 Strategy, Tier 2 Tech Lead,
;;; Tab 0: Discussion Hub ;;; Tier 3 Workers, Tier 4 QA
;;; Tab 1: Log Management
;;; Tab 2: Diagnostics
;;; ;;;
;;; MMA Dashboard is intentionally NOT included — it starts hidden and ;;; All Collapsed=0 so the windows expand immediately on first render.
;;; the user opens it from the Windows menu when needed (per user
;;; preference: "I don't want mma to be visible by default").
;;; ;;;
;;; To iterate on this layout: open sloppy.py, arrange windows as ;;; To iterate on this layout: open sloppy.py, arrange windows as
;;; desired, quit (HelloImGui auto-saves the file to your cwd), then ;;; desired, quit (HelloImgui auto-saves), then copy the resulting
;;; copy the resulting manualslop_layout.ini over this one. ;;; cwd/manualslop_layout.ini over this one. Strip the docking data
;;; block from the saved INI before copy (or just keep this default
;;; which auto-docks cleanly).
;;; ;;;
;;; Scrubbed entries: no Text Viewer / Tool Script windows (transient ;;; Scrubbed entries: no Text Viewer / Tool Script / Inject File /
;;; session-specific), no old-name windows (Projects/Files/Screenshots/ ;;; AST Inspector / Context Preview / Patch modal etc. (transient or
;;; Provider/...), no modals (Inject File/AST Inspector/Context Preview ;;; modal-by-default).
;;; are popups opened on demand).
;;; ;;;
[Window][Project Settings] [Window][Project Settings]
Pos=0,29 Pos=0,29
Size=900,1200 Size=400,400
Collapsed=0 Collapsed=0
DockId=0x00000010,0
[Window][Files & Media] [Window][Files & Media]
Pos=0,29 Pos=0,432
Size=900,1200 Size=400,400
Collapsed=0 Collapsed=0
DockId=0x00000010,1
[Window][AI Settings] [Window][AI Settings]
Pos=0,29 Pos=410,29
Size=900,1200 Size=400,400
Collapsed=0 Collapsed=0
DockId=0x00000010,2
[Window][Operations Hub] [Window][Operations Hub]
Pos=0,29 Pos=410,432
Size=900,1200 Size=400,400
Collapsed=0 Collapsed=0
DockId=0x00000010,3
[Window][Discussion Hub] [Window][Discussion Hub]
Pos=905,29 Pos=820,29
Size=900,1200 Size=400,600
Collapsed=0 Collapsed=0
DockId=0x00000011,0
[Window][Log Management] [Window][Log Management]
Pos=905,29 Pos=820,640
Size=900,1200 Size=400,200
Collapsed=0 Collapsed=0
DockId=0x00000011,1
[Window][Diagnostics] [Window][Diagnostics]
Pos=905,29 Pos=820,850
Size=900,1200 Size=400,250
Collapsed=0 Collapsed=0
DockId=0x00000011,2
[Window][Theme] [Window][Theme]
Pos=0,29 Pos=1230,29
Size=400,400 Size=400,300
Collapsed=1 Collapsed=0
DockId=0x00000010,4
[Window][Tier 1: Strategy] [Window][Tier 1: Strategy]
Pos=910,29 Pos=1230,340
Size=400,300 Size=400,250
Collapsed=1 Collapsed=0
DockId=0x00000011,3
[Window][Tier 2: Tech Lead] [Window][Tier 2: Tech Lead]
Pos=910,29 Pos=1230,600
Size=400,300 Size=400,250
Collapsed=1 Collapsed=0
DockId=0x00000011,4
[Window][Tier 3: Workers] [Window][Tier 3: Workers]
Pos=910,29 Pos=1230,860
Size=400,300 Size=400,200
Collapsed=1 Collapsed=0
DockId=0x00000011,5
[Window][Tier 4: QA] [Window][Tier 4: QA]
Pos=910,29 Pos=1640,29
Size=400,300 Size=400,300
Collapsed=1 Collapsed=0
DockId=0x00000011,6
[Docking][Data]
DockSpace ID=0xAFBEEF01 Window=0xCAFEBABE Pos=0,29 Size=1805,1200 Split=X
DockNode ID=0x00000010 Parent=0xAFBEEF01 SizeRef=900,1200 Selected=0xC0FFEE01
DockNode ID=0x00000011 Parent=0xAFBEEF01 SizeRef=900,1200 Selected=0xC0FFEE02
+10 -2
View File
@@ -1478,7 +1478,11 @@ def _install_default_layout_if_empty(src_ini: Path, dst_ini: Path) -> Result[boo
Decision rule: dst_ini is "empty" when its content is fewer than 1000 Decision rule: dst_ini is "empty" when its content is fewer than 1000
bytes OR has no [Window][ header. On empty, copies src_ini -> dst_ini bytes OR has no [Window][ header. On empty, copies src_ini -> dst_ini
and returns Result(data=True). On non-empty (user customized), returns and ALSO calls imgui.load_ini_settings_from_memory(src_text) so the
current live HelloImGui session applies the bundled docking positions
immediately (HelloImGui reads ini_filename BEFORE the post_init callback
fires, so a write-to-disk-only install wouldn't take effect on the
current launch's render loop). On non-empty (user customized), returns
Result(data=False) without overwriting. On OSError reading src or Result(data=False) without overwriting. On OSError reading src or
writing dst, returns Result(data=False, errors=[ErrorInfo]) so the writing dst, returns Result(data=False, errors=[ErrorInfo]) so the
legacy wrapper in App._post_init can drain to _startup_timeline_errors. legacy wrapper in App._post_init can drain to _startup_timeline_errors.
@@ -1511,7 +1515,11 @@ def _install_default_layout_if_empty(src_ini: Path, dst_ini: Path) -> Result[boo
source="gui_2._install_default_layout_if_empty", source="gui_2._install_default_layout_if_empty",
original=e, original=e,
)]) )])
sys.stderr.write(f"[GUI] installed default layout: {src_ini} -> {dst_ini}\n") try:
imgui.load_ini_settings_from_memory(src_text)
sys.stderr.write(f"[GUI] installed default layout: {src_ini} -> {dst_ini} (and applied to live session)\n")
except Exception as e:
sys.stderr.write(f"[GUI] installed default layout to disk: {src_ini} -> {dst_ini} (live-session apply failed: {e})\n")
return Result(data=True) return Result(data=True)
def _install_default_layout_if_empty_result(app: "App", src: Path, dst: Path) -> Result[bool]: def _install_default_layout_if_empty_result(app: "App", src: Path, dst: Path) -> Result[bool]:
"""Drain-aware variant of _install_default_layout_if_empty. """Drain-aware variant of _install_default_layout_if_empty.
+33 -8
View File
@@ -38,6 +38,25 @@ def _spawn_sloppy_for(workspace: Path, log_suffix: str) -> subprocess.Popen:
return proc return proc
def _read_launch_log(log_suffix: str) -> str:
log_path: Path = _SCRIPTS_DIR / f"test_{log_suffix}.log"
if not log_path.exists():
return ""
try:
return log_path.read_text(encoding="utf-8", errors="replace")
except OSError:
return ""
def _assert_live_session_apply(log_suffix: str) -> None:
text: str = _read_launch_log(log_suffix)
assert "and applied to live session" in text, (
f"install write succeeded but live-session apply did not happen; "
f"expected the live-apply confirmation line in stderr, got: "
f"{[l for l in text.splitlines() if 'installed' in l]!r}"
)
def _terminate(process: subprocess.Popen) -> None: def _terminate(process: subprocess.Popen) -> None:
if process.poll() is not None: if process.poll() is not None:
return return
@@ -72,16 +91,17 @@ def _read_ini(workspace: Path) -> str:
return "" return ""
def _has_dockid_after_window_header(text: str) -> bool: def _has_window_with_collapsed_zero(text: str) -> bool:
lines: list[str] = text.splitlines() for line in text.splitlines():
for idx, line in enumerate(lines):
if line.startswith("[Window][") and line.rstrip().endswith("]"): if line.startswith("[Window][") and line.rstrip().endswith("]"):
tail: str = "\n".join(lines[idx + 1:]) return True
if "DockId=" in tail:
return True
return False return False
def _has_no_docking_block(text: str) -> bool:
return "[Docking][Data]" not in text
def _workspace_for(tmp_path: Path, test_name: str) -> Path: def _workspace_for(tmp_path: Path, test_name: str) -> Path:
return tmp_path / f"_default_layout_install_{os.getpid()}_{test_name}" return tmp_path / f"_default_layout_install_{os.getpid()}_{test_name}"
@@ -99,8 +119,11 @@ def _assert_installed_default(workspace: Path) -> None:
assert "[Window][Project Settings]" in text, ( assert "[Window][Project Settings]" in text, (
f"installed INI missing [Window][Project Settings]; got first 400 chars: {text[:400]!r}" f"installed INI missing [Window][Project Settings]; got first 400 chars: {text[:400]!r}"
) )
assert _has_dockid_after_window_header(text), ( assert _has_window_with_collapsed_zero(text), (
f"installed INI has no DockId= following a [Window][...] header; got first 400 chars: {text[:400]!r}" f"installed INI has no [Window][...] entry; got first 400 chars: {text[:400]!r}"
)
assert _has_no_docking_block(text), (
f"installed INI should not contain a [Docking][Data] block (HelloImgui dockspace IDs are session-specific); got first 400 chars: {text[:400]!r}"
) )
@@ -113,6 +136,7 @@ def test_default_layout_installed_when_ini_missing(tmp_path: Path) -> None:
proc: subprocess.Popen = _start_subprocess_in(workspace, "ini_missing") proc: subprocess.Popen = _start_subprocess_in(workspace, "ini_missing")
try: try:
_assert_installed_default(workspace) _assert_installed_default(workspace)
_assert_live_session_apply("ini_missing")
finally: finally:
_terminate(proc) _terminate(proc)
shutil.rmtree(workspace, ignore_errors=True) shutil.rmtree(workspace, ignore_errors=True)
@@ -126,6 +150,7 @@ def test_default_layout_installed_when_ini_empty(tmp_path: Path) -> None:
proc: subprocess.Popen = _start_subprocess_in(workspace, "ini_empty") proc: subprocess.Popen = _start_subprocess_in(workspace, "ini_empty")
try: try:
_assert_installed_default(workspace) _assert_installed_default(workspace)
_assert_live_session_apply("ini_empty")
finally: finally:
_terminate(proc) _terminate(proc)
shutil.rmtree(workspace, ignore_errors=True) shutil.rmtree(workspace, ignore_errors=True)