From e965451842d186d543da1e1b539243962ecdc5cc Mon Sep 17 00:00:00 2001 From: Ed_ Date: Mon, 29 Jun 2026 19:08:49 -0400 Subject: [PATCH] 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). --- layouts/default.ini | 119 ++++++++++++--------------- src/gui_2.py | 12 ++- tests/test_default_layout_install.py | 41 +++++++-- 3 files changed, 95 insertions(+), 77 deletions(-) diff --git a/layouts/default.ini b/layouts/default.ini index 49a20fcc..c581ad27 100644 --- a/layouts/default.ini +++ b/layouts/default.ini @@ -1,109 +1,94 @@ ;;; ;;; Manual Slop default docking layout for live_gui test sessions ;;; -;;; This file is loaded by the live_gui test fixture to give every test -;;; session a deterministic starting layout. The fixture copies this file -;;; into the test workspace (tests/artifacts/live_gui_workspace/) before -;;; spawning the sloppy.py subprocess; HelloImGui picks it up on launch. +;;; Layout strategy: each window entry has Pos + Size + Collapsed=0 set +;;; explicitly so the window is registered at a known absolute position. +;;; No docking data block and no DockId references -- HelloImgui dockspace +;;; 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): -;;; Left column (DockNode 0x10): -;;; Tab 0: Project Settings -;;; Tab 1: Files & Media -;;; Tab 2: AI Settings -;;; Tab 3: Operations Hub -;;; Right column (DockNode 0x11): -;;; Tab 0: Discussion Hub -;;; Tab 1: Log Management -;;; Tab 2: Diagnostics +;;; Window list (matches src/app_controller.py:_default_windows defaults +;;; plus the four Tier panels that the user prefers visible): +;;; Pos=0,29 Size=600,400 : Project Settings, Files and Media, +;;; AI Settings, Operations Hub, Theme +;;; Pos=600,29 Size=600,400 : Discussion Hub, Log Management, +;;; Diagnostics +;;; Pos=0,432 Size=400,300 : Tier 1 Strategy, Tier 2 Tech Lead, +;;; Tier 3 Workers, Tier 4 QA ;;; -;;; MMA Dashboard is intentionally NOT included — it starts hidden and -;;; the user opens it from the Windows menu when needed (per user -;;; preference: "I don't want mma to be visible by default"). +;;; All Collapsed=0 so the windows expand immediately on first render. ;;; ;;; To iterate on this layout: open sloppy.py, arrange windows as -;;; desired, quit (HelloImGui auto-saves the file to your cwd), then -;;; copy the resulting manualslop_layout.ini over this one. +;;; desired, quit (HelloImgui auto-saves), then copy the resulting +;;; 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 -;;; session-specific), no old-name windows (Projects/Files/Screenshots/ -;;; Provider/...), no modals (Inject File/AST Inspector/Context Preview -;;; are popups opened on demand). +;;; Scrubbed entries: no Text Viewer / Tool Script / Inject File / +;;; AST Inspector / Context Preview / Patch modal etc. (transient or +;;; modal-by-default). ;;; [Window][Project Settings] Pos=0,29 -Size=900,1200 +Size=400,400 Collapsed=0 -DockId=0x00000010,0 [Window][Files & Media] -Pos=0,29 -Size=900,1200 +Pos=0,432 +Size=400,400 Collapsed=0 -DockId=0x00000010,1 [Window][AI Settings] -Pos=0,29 -Size=900,1200 +Pos=410,29 +Size=400,400 Collapsed=0 -DockId=0x00000010,2 [Window][Operations Hub] -Pos=0,29 -Size=900,1200 +Pos=410,432 +Size=400,400 Collapsed=0 -DockId=0x00000010,3 [Window][Discussion Hub] -Pos=905,29 -Size=900,1200 +Pos=820,29 +Size=400,600 Collapsed=0 -DockId=0x00000011,0 [Window][Log Management] -Pos=905,29 -Size=900,1200 +Pos=820,640 +Size=400,200 Collapsed=0 -DockId=0x00000011,1 [Window][Diagnostics] -Pos=905,29 -Size=900,1200 +Pos=820,850 +Size=400,250 Collapsed=0 -DockId=0x00000011,2 [Window][Theme] -Pos=0,29 -Size=400,400 -Collapsed=1 -DockId=0x00000010,4 +Pos=1230,29 +Size=400,300 +Collapsed=0 [Window][Tier 1: Strategy] -Pos=910,29 -Size=400,300 -Collapsed=1 -DockId=0x00000011,3 +Pos=1230,340 +Size=400,250 +Collapsed=0 [Window][Tier 2: Tech Lead] -Pos=910,29 -Size=400,300 -Collapsed=1 -DockId=0x00000011,4 +Pos=1230,600 +Size=400,250 +Collapsed=0 [Window][Tier 3: Workers] -Pos=910,29 -Size=400,300 -Collapsed=1 -DockId=0x00000011,5 +Pos=1230,860 +Size=400,200 +Collapsed=0 [Window][Tier 4: QA] -Pos=910,29 +Pos=1640,29 Size=400,300 -Collapsed=1 -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 +Collapsed=0 diff --git a/src/gui_2.py b/src/gui_2.py index 71afa53b..d80a1ef4 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -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 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 writing dst, returns Result(data=False, errors=[ErrorInfo]) so the 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", 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) def _install_default_layout_if_empty_result(app: "App", src: Path, dst: Path) -> Result[bool]: """Drain-aware variant of _install_default_layout_if_empty. diff --git a/tests/test_default_layout_install.py b/tests/test_default_layout_install.py index 88b6a17e..54357551 100644 --- a/tests/test_default_layout_install.py +++ b/tests/test_default_layout_install.py @@ -38,6 +38,25 @@ def _spawn_sloppy_for(workspace: Path, log_suffix: str) -> subprocess.Popen: 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: if process.poll() is not None: return @@ -72,16 +91,17 @@ def _read_ini(workspace: Path) -> str: return "" -def _has_dockid_after_window_header(text: str) -> bool: - lines: list[str] = text.splitlines() - for idx, line in enumerate(lines): +def _has_window_with_collapsed_zero(text: str) -> bool: + for line in text.splitlines(): if line.startswith("[Window][") and line.rstrip().endswith("]"): - tail: str = "\n".join(lines[idx + 1:]) - if "DockId=" in tail: - return True + return True 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: 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, ( f"installed INI missing [Window][Project Settings]; got first 400 chars: {text[:400]!r}" ) - assert _has_dockid_after_window_header(text), ( - f"installed INI has no DockId= following a [Window][...] header; got first 400 chars: {text[:400]!r}" + assert _has_window_with_collapsed_zero(text), ( + 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") try: _assert_installed_default(workspace) + _assert_live_session_apply("ini_missing") finally: _terminate(proc) 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") try: _assert_installed_default(workspace) + _assert_live_session_apply("ini_empty") finally: _terminate(proc) shutil.rmtree(workspace, ignore_errors=True)