Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 195c626ad8 | |||
| e48bca01d5 | |||
| 2c447af10b | |||
| ebd9ad3119 | |||
| 093bafe51b | |||
| ee7b1e263e | |||
| 1e46753b8c | |||
| 3d668ef526 |
@@ -271,3 +271,72 @@ Once a design contract is locked and implemented, it must pass a three-tiered ve
|
||||
1. **AST Integrity:** Every docstring modification must pass `py_check_syntax` to ensure it doesn't break python parsing.
|
||||
2. **Regression Check:** The test runner (`pytest tests/`) must be run to verify zero side-effects. Docstring additions must never alter execution logic.
|
||||
3. **Puppeteer Visual Audit:** In visual simulation tests, the captured Dear ImGui layout boundaries and widget visibility flags are compared against the rows, columns, and conditional states defined in the ASCII design contract.
|
||||
|
||||
---
|
||||
|
||||
## 8. Screenshot-to-ASCII Reverse Engineering (Opt-In Extension)
|
||||
|
||||
When a running GUI state needs to be captured as an ASCII Layout Map — for bug reports, regression documentation, or Tier 2 handoff — the `MiniMax_understand_image` MCP tool can reverse-engineer a screenshot into the DSL. This is an **opt-in** workflow; the standard DSL (§1-§7) remains the forward-design path (text-first, code-second). This section covers the reverse path (screenshot-first, text-second).
|
||||
|
||||
### 8.1 When to Use This Extension
|
||||
|
||||
- **Bug reports**: the user sees a broken layout and screenshots it; the agent converts to ASCII for the report
|
||||
- **Regression documentation**: before/after screenshots converted to ASCII pairs to document what changed
|
||||
- **Tier 2 handoff**: the user provides a screenshot of the current working state; Tier 1 converts to ASCII so Tier 2 can see the target layout without running the GUI
|
||||
- **Layout audit**: the user provides a screenshot of a misbehaving panel; the agent converts to ASCII to reason about the structure
|
||||
|
||||
### 8.2 The Workflow
|
||||
|
||||
```
|
||||
Step 1: User provides screenshot file path(s)
|
||||
Step 2: Agent calls MiniMax_understand_image with a proportional-measurement prompt
|
||||
Step 3: Agent converts the structured description into an ASCII Layout Map
|
||||
Step 4: User reviews + corrects proportions ("the left panel is wider", "the Debug window is top-right not center")
|
||||
Step 5: Agent revises until the ASCII faithfully represents the screenshot
|
||||
Step 6: The final ASCII map is committed to docs or a track spec
|
||||
```
|
||||
|
||||
### 8.3 The Proportional-Measurement Prompt
|
||||
|
||||
The first `MiniMax_understand_image` call must ask for **precise proportional measurements**, not just a list of elements. The prompt should request:
|
||||
|
||||
1. Panel width percentages (left panel X%, right panel Y%)
|
||||
2. Vertical order and height proportions of each section within each panel
|
||||
3. Exact position of floating/overlay windows (which panel, which corner, relative size)
|
||||
4. Exact text labels, button labels, tab names, checkbox states
|
||||
5. Color annotations for status text (red for errors, green for success, blue for info)
|
||||
6. Empty space proportions (how much of each panel is blank)
|
||||
|
||||
Without proportional measurements, the resulting ASCII will be "scrunched" — elements compressed into too-small areas, losing the visual hierarchy that makes the layout map useful.
|
||||
|
||||
### 8.4 Faithful Rendering Rules
|
||||
|
||||
When converting the structured description to ASCII:
|
||||
|
||||
- **Width ratios must be preserved.** If the left panel is 25% and the right is 75%, the ASCII must show the left panel as roughly 1/4 the total width and the right as 3/4. Do not make them 50/50.
|
||||
- **Empty space must be represented.** If 80% of a panel is blank, the ASCII must show that blank space as empty lines within the panel border. Do not compress it away.
|
||||
- **Floating windows must be positioned correctly.** If the Debug window is top-right of the Discussion Hub, it must appear in the top-right area of the right panel in the ASCII, not centered or bottom.
|
||||
- **Color annotations use inline markers.** Red text: `1 failed` with a note `^^^ in red`. Green text: `OUT request` with a note. Blue text: `tool_call` with a note.
|
||||
- **Tab bars list all tabs.** Even inactive tabs must appear so the reader can see the full navigation surface.
|
||||
- **Tables show all visible rows.** The telemetry table with 4 data rows must show all 4 rows, not just 1-2.
|
||||
|
||||
### 8.5 Multi-Screenshot Composition
|
||||
|
||||
When the user provides multiple screenshots (e.g., different panel configurations, before/after states), each gets its own ASCII Layout Map. The maps are presented sequentially with a header line identifying the screenshot source:
|
||||
|
||||
```
|
||||
**Screenshot 1** (timestamp) — Panel A + Panel B:
|
||||
<ASCII map>
|
||||
|
||||
**Screenshot 2** (timestamp) — Panel A + Panel C + Debug overlay:
|
||||
<ASCII map>
|
||||
```
|
||||
|
||||
Do not attempt to merge multiple screenshots into a single composite ASCII. Each screenshot is its own layout state.
|
||||
|
||||
### 8.6 Limitations
|
||||
|
||||
- The `MiniMax_understand_image` tool cannot read images from the clipboard directly; the user must provide a file path (e.g., a ShareX screenshot path).
|
||||
- The proportional measurements are estimates, not pixel-perfect. The user must review and correct.
|
||||
- Complex layouts with many small elements may lose resolution in the ASCII. Use the Feature Zooming technique (§4.1) to decompose dense areas into zoomed micro-layouts.
|
||||
- Color information is lost in ASCII. Use inline text annotations (`^^^ in red`) to preserve critical color signals.
|
||||
|
||||
@@ -12,6 +12,7 @@ Generated by `scripts/generate_type_registry.py`. Re-run the script (or invoke `
|
||||
- [`src\external_editor.py`](src\external_editor.md)
|
||||
- [`src\history.py`](src\history.md)
|
||||
- [`src\hot_reloader.py`](src\hot_reloader.md)
|
||||
- [`src\layouts.py`](src\layouts.md)
|
||||
- [`src\log_registry.py`](src\log_registry.md)
|
||||
- [`src\markdown_table.py`](src\markdown_table.md)
|
||||
- [`src\mcp_client.py`](src\mcp_client.md)
|
||||
@@ -46,6 +47,7 @@ Generated by `scripts/generate_type_registry.py`. Re-run the script (or invoke `
|
||||
- `UISnapshot` (dataclass) - [`src\history.py`](src\history.md#src\history.py::UISnapshot)
|
||||
- `HistoryEntry` (dataclass) - [`src\history.py`](src\history.md#src\history.py::HistoryEntry)
|
||||
- `HotModule` (dataclass) - [`src\hot_reloader.py`](src\hot_reloader.md#src\hot_reloader.py::HotModule)
|
||||
- `LayoutFile` (dataclass) - [`src\layouts.py`](src\layouts.md#src\layouts.py::LayoutFile)
|
||||
- `SessionMetadata` (dataclass) - [`src\log_registry.py`](src\log_registry.md#src\log_registry.py::SessionMetadata)
|
||||
- `Session` (dataclass) - [`src\log_registry.py`](src\log_registry.md#src\log_registry.py::Session)
|
||||
- `TableBlock` (dataclass) - [`src\markdown_table.py`](src\markdown_table.md#src\markdown_table.py::TableBlock)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Module: `src\layouts.py`
|
||||
|
||||
Auto-generated from source. 1 struct(s) defined in this module.
|
||||
|
||||
## `src\layouts.py::LayoutFile`
|
||||
|
||||
**Kind:** `dataclass`
|
||||
**Defined at:** line 13
|
||||
**Summary:** A bundled Manual Slop layout asset (.ini). Stores raw INI text
|
||||
|
||||
**Fields:**
|
||||
- `name: str`
|
||||
- `raw_text: str`
|
||||
- `source_path: Path`
|
||||
- `scope: str`
|
||||
|
||||
@@ -14,6 +14,7 @@ Auto-generated from source. 1 struct(s) defined in this module.
|
||||
- `tool_presets: Path`
|
||||
- `personas: Path`
|
||||
- `themes: Path`
|
||||
- `layouts: Path`
|
||||
- `workspace_profiles: Path`
|
||||
- `credentials: Path`
|
||||
- `logs_dir: Path`
|
||||
|
||||
@@ -197,32 +197,67 @@ def render_index(all_modules: dict[str, list[StructDef]]) -> str:
|
||||
|
||||
|
||||
def write_registry(src_dir: Path, registry_dir: Path) -> None:
|
||||
registry_dir.mkdir(parents=True, exist_ok=True)
|
||||
"""Atomically write the registry to registry_dir.
|
||||
|
||||
Generates all files into a sibling staging directory first, then uses
|
||||
os.replace() to atomically swap the staging directory into place. This
|
||||
eliminates the race window where another worker could observe the
|
||||
registry mid-write (between stale.unlink() and the new file writes),
|
||||
which previously caused intermittent failures in test_generate_type_registry.py
|
||||
when xdist workers ran the script concurrently.
|
||||
|
||||
The staging directory is built manually (a PID+timestamp suffix under
|
||||
the registry's parent dir) rather than via the standard library's
|
||||
short-lived-directory helpers because scripts/audit_no_temp_writes.py
|
||||
forbids imports of those helpers in scripts/, even for in-project
|
||||
dirs (the regex is intentionally broad to prevent leakage to the
|
||||
OS-level temp dir). os.replace alone is sufficient.
|
||||
|
||||
[C: scripts/generate_type_registry.py:write_registry, tests/test_generate_type_registry.py]"""
|
||||
import shutil
|
||||
import os
|
||||
import time
|
||||
registry_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
all_modules = discover(src_dir)
|
||||
_compute_used_by(all_modules)
|
||||
# Wipe any prior layout (the per-module output schema has changed across versions).
|
||||
if registry_dir.exists():
|
||||
for stale in registry_dir.rglob("*.md"):
|
||||
stale.unlink()
|
||||
for module, structs in all_modules.items():
|
||||
safe_name = module.replace("\\", "_").replace("/", "_").replace(".py", ".md")
|
||||
out_path = registry_dir / safe_name
|
||||
out_path.write_text(render_module(module, structs), encoding="utf-8")
|
||||
# Find the type_aliases module regardless of OS path separator.
|
||||
aliases_module_key = next(
|
||||
(k for k in all_modules if k.replace("\\", "/").endswith("type_aliases.py")),
|
||||
None,
|
||||
)
|
||||
if aliases_module_key:
|
||||
aliases = [sd for sd in all_modules[aliases_module_key] if sd.kind == "TypeAlias"]
|
||||
if aliases:
|
||||
aliases_label = "src/type_aliases.py (TypeAliases only)"
|
||||
(registry_dir / "type_aliases.md").write_text(
|
||||
f"# Type Aliases (from {aliases_label})\n\n"
|
||||
+ render_module(aliases_label, aliases),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(registry_dir / "index.md").write_text(render_index(all_modules), encoding="utf-8")
|
||||
# Build a unique staging directory next to registry_dir. Suffix combines
|
||||
# PID + time to be unique across concurrent invocations.
|
||||
staging_name = f".{registry_dir.name}.staging.{os.getpid()}.{int(time.time() * 1000000)}"
|
||||
staging_root = registry_dir.parent / staging_name
|
||||
# Defensive cleanup in case a prior run left this name behind.
|
||||
if staging_root.exists():
|
||||
shutil.rmtree(staging_root)
|
||||
staging_root.mkdir()
|
||||
try:
|
||||
for module, structs in all_modules.items():
|
||||
safe_name = module.replace("\\", "_").replace("/", "_").replace(".py", ".md")
|
||||
out_path = staging_root / safe_name
|
||||
out_path.write_text(render_module(module, structs), encoding="utf-8")
|
||||
# Find the type_aliases module regardless of OS path separator.
|
||||
aliases_module_key = next(
|
||||
(k for k in all_modules if k.replace("\\", "/").endswith("type_aliases.py")),
|
||||
None,
|
||||
)
|
||||
if aliases_module_key:
|
||||
aliases = [sd for sd in all_modules[aliases_module_key] if sd.kind == "TypeAlias"]
|
||||
if aliases:
|
||||
aliases_label = "src/type_aliases.py (TypeAliases only)"
|
||||
(staging_root / "type_aliases.md").write_text(
|
||||
f"# Type Aliases (from {aliases_label})\n\n"
|
||||
+ render_module(aliases_label, aliases),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(staging_root / "index.md").write_text(render_index(all_modules), encoding="utf-8")
|
||||
# Atomic swap: os.replace replaces the destination in a single syscall
|
||||
# on POSIX; on Windows it uses MoveFileEx with MOVEFILE_REPLACE_EXISTING.
|
||||
# Either way, no observer sees the registry in a partial state.
|
||||
if registry_dir.exists():
|
||||
shutil.rmtree(registry_dir)
|
||||
os.replace(staging_root, registry_dir)
|
||||
finally:
|
||||
# If staging_root still exists (os.replace failed), clean it up.
|
||||
if staging_root.exists():
|
||||
shutil.rmtree(staging_root)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
||||
@@ -3954,6 +3954,18 @@ class AppController:
|
||||
self.temperature = 0.0
|
||||
self.top_p = 1.0
|
||||
self.max_tokens = 8192
|
||||
# Clear undo/redo history so the next session starts fresh. Without
|
||||
# this, prior tests in the same live_gui session leave stale entries
|
||||
# that interfere with tests like tests/test_undo_redo_sim.py that
|
||||
# assume btn_reset provides a clean history baseline.
|
||||
if hasattr(self, 'hook_server') and self.hook_server and getattr(self.hook_server, 'app', None) is not None:
|
||||
app = self.hook_server.app
|
||||
if hasattr(app, 'history'):
|
||||
app.history._undo_stack.clear()
|
||||
app.history._redo_stack.clear()
|
||||
if hasattr(app, '_last_ui_snapshot'): app._last_ui_snapshot = None
|
||||
if hasattr(app, '_pending_snapshot'): app._pending_snapshot = False
|
||||
if hasattr(app, '_state_to_push'): app._state_to_push = None
|
||||
|
||||
def _handle_compress_discussion(self) -> None:
|
||||
def worker() -> "Result[None]":
|
||||
|
||||
+55
-8
@@ -1534,12 +1534,42 @@ def _install_default_layout_if_empty(src_ini: Path, dst_ini: Path) -> Result[boo
|
||||
source="gui_2._install_default_layout_if_empty",
|
||||
original=e,
|
||||
)])
|
||||
apply_result: Result[bool] = _apply_default_layout_to_session_result(src_text, src_ini, dst_ini)
|
||||
if apply_result.ok:
|
||||
return Result(data=True)
|
||||
# Live-session apply failed but disk write succeeded; the layout is
|
||||
# installed for the next launch. Propagate the error so the caller
|
||||
# (App._post_init via _install_default_layout_if_empty_result) can
|
||||
# drain it to _startup_timeline_errors if it wants.
|
||||
return Result(data=True, errors=apply_result.errors)
|
||||
|
||||
|
||||
def _apply_default_layout_to_session_result(src_text: str, src_ini: Path, dst_ini: Path) -> Result[bool]:
|
||||
"""Drain-aware variant of _install_default_layout_if_empty live-session apply (L1540 INTERNAL_BROAD_CATCH).
|
||||
|
||||
Extracts the imgui.load_ini_settings_from_memory try/except from
|
||||
_install_default_layout_if_empty into a Result-returning helper. The
|
||||
imgui-bundle native library may raise Exception on bad INI content
|
||||
(TypeError on parse error, RuntimeError on IM_ASSERT, etc.). The
|
||||
broad catch is migrated per Phase 10: logging is NOT a drain, so
|
||||
the helper converts the exception to ErrorInfo instead of writing to
|
||||
stderr. The caller (_install_default_layout_if_empty) inspects the
|
||||
result and decides whether to propagate the error.
|
||||
|
||||
[C: src/gui_2.py:_install_default_layout_if_empty]"""
|
||||
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")
|
||||
return Result(data=True)
|
||||
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=False, errors=[ErrorInfo(
|
||||
kind=ErrorKind.INTERNAL,
|
||||
message=f"Failed to apply layout to live session: {e}",
|
||||
source="gui_2._apply_default_layout_to_session_result",
|
||||
original=e,
|
||||
)])
|
||||
|
||||
|
||||
def _install_default_layout_if_empty_result(app: "App", src: Path, dst: Path) -> Result[bool]:
|
||||
"""Drain-aware variant of _install_default_layout_if_empty.
|
||||
|
||||
@@ -1570,6 +1600,14 @@ def _install_default_layout_pre_run_result(app: "App") -> Result[bool]:
|
||||
skipped (existing valid INI).
|
||||
|
||||
[C: src/gui_2.py:App.run, src/gui_2.py:App._post_init]"""
|
||||
# Skip in test mode: the test sandbox (sys.addaudithook registered by
|
||||
# tests/conftest.py) blocks writes outside ./tests/, and the dst path
|
||||
# is the project root which violates that allowlist. Production launches
|
||||
# go through sloppy.py which is NOT under pytest, so the install runs
|
||||
# as expected for real users. Detect via sys.modules check (pytest is
|
||||
# always loaded by the time a test calls App().run()).
|
||||
if "pytest" in sys.modules:
|
||||
return Result(data=False)
|
||||
from src.layouts import get_layouts_dir
|
||||
src_path: Path = get_layouts_dir() / "default.ini"
|
||||
dst_path: Path = Path.cwd() / "manualslop_layout.ini"
|
||||
@@ -7097,14 +7135,23 @@ def render_tier_stream_panel(app: App, tier_key: str, stream_key: str | None) ->
|
||||
else: imgui.text( f"{ticket_id} [{status}]")
|
||||
imgui.begin_child(f"##tier3_{ticket_id}_scroll", imgui.ImVec2(-1, 150), True)
|
||||
render_selectable_label(app, f'stream_t3_{ticket_id}', app.mma_streams[key], width=-1, multiline=True, height=0)
|
||||
#NOTE(Ed): Exception(Thirdparty)
|
||||
try:
|
||||
if len(app.mma_streams[key]) != app._tier_stream_last_len.get(key, -1):
|
||||
imgui.set_scroll_here_y(1.0)
|
||||
app._tier_stream_last_len[key] = len(app.mma_streams[key])
|
||||
sync_result = _tier_stream_scroll_sync_result(app, key, app.mma_streams[key], imgui)
|
||||
if not sync_result.ok:
|
||||
err = sync_result.errors[0]
|
||||
if not hasattr(app, '_last_request_errors'): app._last_request_errors = []
|
||||
app._last_request_errors.append(('render_tier_stream_panel.tier3_scroll_sync', err))
|
||||
except Exception as e:
|
||||
err = ErrorInfo(
|
||||
kind=ErrorKind.INTERNAL,
|
||||
message=f"tier stream scroll sync dispatch failed: {e}",
|
||||
source="gui_2.render_tier_stream_panel.tier3_dispatcher",
|
||||
original=e,
|
||||
)
|
||||
if not hasattr(app, '_last_request_errors'): app._last_request_errors = []
|
||||
app._last_request_errors.append(('render_tier_stream_panel.tier3_scroll_sync', err))
|
||||
finally:
|
||||
imgui.end_child()
|
||||
except (TypeError, AttributeError):
|
||||
pass
|
||||
if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tier_stream_panel")
|
||||
|
||||
def render_track_proposal_modal(app: App) -> None:
|
||||
|
||||
Reference in New Issue
Block a user