6 Commits

32 changed files with 705 additions and 113 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -1
View File
@@ -7,7 +7,8 @@
## UX & UI Principles ## UX & UI Principles
- **USA Graphics Company Values:** Embrace high information density and tactile interactions. - **USA Graphics Company Values:** Embrace high information density and tactile interactions.
- **Arcade Aesthetics:** Utilize arcade game-style visual feedback for state updates (e.g., blinking notifications for tool execution and AI responses) to make the experience fun, visceral, and engaging. - **Professional Arcade Aesthetics:** Balances high-energy "Arcade" feedback (blinking notifications, tactile updates) with a "Professional" visual discipline. Employs modern typography (Inter/Maple Mono), subtle rounded geometry, and soft shadows to ensure the tool feels like a sophisticated, expert utility rather than a toy.
- **Rich Text Readability:** Prioritizes legibility of AI communications and technical logs by utilizing GitHub-Flavored Markdown and integrated syntax highlighting. This ensures that complex code fragments and structured data are immediately accessible and professionally presented.
- **Explicit Control & Expert Focus:** The interface should not hold the user's hand. It must prioritize explicit manual confirmation for destructive actions while providing dense, unadulterated access to logs and context. - **Explicit Control & Expert Focus:** The interface should not hold the user's hand. It must prioritize explicit manual confirmation for destructive actions while providing dense, unadulterated access to logs and context.
- **Multi-Viewport Capabilities:** Leverage dockable, floatable panels to allow users to build custom workspaces suitable for multi-monitor setups. - **Multi-Viewport Capabilities:** Leverage dockable, floatable panels to allow users to build custom workspaces suitable for multi-monitor setups.
+3
View File
@@ -59,6 +59,9 @@ For deep implementation details when planning or implementing tracks, consult `d
- **Clean Project Root:** Enforces a "Cruft-Free Root" policy by organizing core implementation into a `src/` directory and redirecting all temporary test data, configurations, and AI-generated artifacts to `tests/artifacts/`. - **Clean Project Root:** Enforces a "Cruft-Free Root" policy by organizing core implementation into a `src/` directory and redirecting all temporary test data, configurations, and AI-generated artifacts to `tests/artifacts/`.
- **Performance Diagnostics:** Comprehensive, conditional per-component profiling across the entire application. Features a dedicated **Diagnostics Panel** providing real-time telemetry for FPS, Frame Time, CPU usage, and **Detailed Component Timings** for all GUI panels and background threads, including automated threshold-based latency alerts. - **Performance Diagnostics:** Comprehensive, conditional per-component profiling across the entire application. Features a dedicated **Diagnostics Panel** providing real-time telemetry for FPS, Frame Time, CPU usage, and **Detailed Component Timings** for all GUI panels and background threads, including automated threshold-based latency alerts.
- **Automated UX Verification:** A robust IPC mechanism via API hooks and a modular simulation suite allows for human-like simulation walkthroughs and automated regression testing of the full GUI lifecycle across multiple specialized scenarios. - **Automated UX Verification:** A robust IPC mechanism via API hooks and a modular simulation suite allows for human-like simulation walkthroughs and automated regression testing of the full GUI lifecycle across multiple specialized scenarios.
- **Professional UI Theme & Typography:** Implements a high-fidelity visual system featuring **Inter** and **Maple Mono** fonts for optimal readability. Employs a cohesive "Subtle Rounding" aesthetic across all standard widgets, supported by custom **soft shadow shaders** for modals and popups to provide depth and professional polish.
- **Rich Text & Syntax Highlighting:** Provides advanced rendering for messages, logs, and tool outputs using a hybrid Markdown system. Supports GitHub-Flavored Markdown (GFM) via `imgui_markdown` and integrates `ImGuiColorTextEdit` for high-performance syntax highlighting of code blocks (Python, JSON, C++, etc.). Includes automated language detection and clickable URL support.
- **Multi-Viewport & Layout Management:** Full support for ImGui Multi-Viewport, allowing users to detach panels into standalone OS windows for complex multi-monitor workflows. Includes a comprehensive **Layout Presets system**, enabling developers to save, name, and instantly restore custom window arrangements, including their Multi-Viewport state.
- **Headless Backend Service:** Optional headless mode allowing the core AI and tool execution logic to run as a decoupled REST API service (FastAPI), optimized for Docker and server-side environments (e.g., Unraid). - **Headless Backend Service:** Optional headless mode allowing the core AI and tool execution logic to run as a decoupled REST API service (FastAPI), optimized for Docker and server-side environments (e.g., Unraid).
- **Remote Confirmation Protocol:** A non-blocking, ID-based challenge/response mechanism for approving AI actions via the REST API, enabling remote "Human-in-the-Loop" safety. - **Remote Confirmation Protocol:** A non-blocking, ID-based challenge/response mechanism for approving AI actions via the REST API, enabling remote "Human-in-the-Loop" safety.
- **Gemini CLI Integration:** Allows using the `gemini` CLI as a headless backend provider. This enables leveraging Gemini subscriptions with advanced features like persistent sessions, while maintaining full "Human-in-the-Loop" safety through a dedicated bridge for synchronous tool call approvals within the Manual Slop GUI. Now features full functional parity with the direct API, including accurate token estimation, safety settings, and robust system instruction handling. - **Gemini CLI Integration:** Allows using the `gemini` CLI as a headless backend provider. This enables leveraging Gemini subscriptions with advanced features like persistent sessions, while maintaining full "Human-in-the-Loop" safety through a dedicated bridge for synchronous tool call approvals within the Manual Slop GUI. Now features full functional parity with the direct API, including accurate token estimation, safety settings, and robust system instruction handling.
+3 -1
View File
@@ -7,7 +7,7 @@
## GUI Frameworks ## GUI Frameworks
- **Dear PyGui:** For immediate/retained mode GUI rendering and node mapping. - **Dear PyGui:** For immediate/retained mode GUI rendering and node mapping.
- **ImGui Bundle (`imgui-bundle`):** To provide advanced multi-viewport and dockable panel capabilities on top of Dear ImGui. Includes **imgui-node-editor** for complex graph-based visualizations. - **ImGui Bundle (`imgui-bundle`):** To provide advanced multi-viewport and dockable panel capabilities on top of Dear ImGui. Includes **imgui-node-editor** for complex graph-based visualizations, **imgui_markdown** for rich text rendering, and **ImGuiColorTextEdit** for syntax-highlighted code blocks.
## Web & Service Frameworks ## Web & Service Frameworks
@@ -53,5 +53,7 @@
- **Synchronous Event Queue:** Employs a `SyncEventQueue` based on `queue.Queue` to manage communication between the UI and backend agents, maintaining responsiveness through a threaded execution model. - **Synchronous Event Queue:** Employs a `SyncEventQueue` based on `queue.Queue` to manage communication between the UI and backend agents, maintaining responsiveness through a threaded execution model.
- **Synchronous IPC Approval Flow:** A specialized bridge mechanism that allows headless AI providers (like Gemini CLI) to synchronously request and receive human approval for tool calls via the GUI's REST API hooks. - **Synchronous IPC Approval Flow:** A specialized bridge mechanism that allows headless AI providers (like Gemini CLI) to synchronously request and receive human approval for tool calls via the GUI's REST API hooks.
- **High-Fidelity Selectable Labels:** Implements a pattern for making read-only UI text selectable by wrapping `imgui.input_text` with `imgui.InputTextFlags_.read_only`. Includes a specialized `_render_selectable_label` helper that resets frame backgrounds, borders, and padding to mimic standard labels while enabling OS-level clipboard support (Ctrl+C). - **High-Fidelity Selectable Labels:** Implements a pattern for making read-only UI text selectable by wrapping `imgui.input_text` with `imgui.InputTextFlags_.read_only`. Includes a specialized `_render_selectable_label` helper that resets frame backgrounds, borders, and padding to mimic standard labels while enabling OS-level clipboard support (Ctrl+C).
- **Hybrid Markdown Rendering:** Employs a custom `MarkdownRenderer` that orchestrates `imgui_markdown` for standard text and headers while intercepting code blocks to render them via cached `ImGuiColorTextEdit` instances. This ensures high-performance rich text rendering with robust syntax highlighting and stateful text selection.
- **Faux-Shader Visual Effects:** Utilizes an optimized `ImDrawList`-based batching technique to simulate advanced visual effects such as soft shadows and acrylic glass overlays without the overhead of heavy GPU-resident shaders or external OpenGL dependencies.
- **Interface-Driven Development (IDD):** Enforces a "Stub-and-Resolve" pattern where cross-module dependencies are resolved by generating signatures/contracts before implementation. - **Interface-Driven Development (IDD):** Enforces a "Stub-and-Resolve" pattern where cross-module dependencies are resolved by generating signatures/contracts before implementation.
+2 -2
View File
@@ -36,7 +36,7 @@ This file tracks all major tracks for the project. Each track has its own detail
*Link: [./tracks/log_session_overhaul_20260308/](./tracks/log_session_overhaul_20260308/)* *Link: [./tracks/log_session_overhaul_20260308/](./tracks/log_session_overhaul_20260308/)*
*Goal: Centralize log management, improve session restoration reliability with full-UI replay mode, and optimize log size via external script/output referencing. Implement transient diagnostic logging for system warnings.* *Goal: Centralize log management, improve session restoration reliability with full-UI replay mode, and optimize log size via external script/output referencing. Implement transient diagnostic logging for system warnings.*
2. [ ] **Track: UI Theme Overhaul & Style System** 2. [x] **Track: UI Theme Overhaul & Style System**
*Link: [./tracks/ui_theme_overhaul_20260308/](./tracks/ui_theme_overhaul_20260308/)* *Link: [./tracks/ui_theme_overhaul_20260308/](./tracks/ui_theme_overhaul_20260308/)*
*Goal: Modernize UI with Inter/Maple Mono fonts, a professional subtle rounded theme, custom shaders (corners, blur, AA), multi-viewport support, and layout presets.* *Goal: Modernize UI with Inter/Maple Mono fonts, a professional subtle rounded theme, custom shaders (corners, blur, AA), multi-viewport support, and layout presets.*
@@ -44,7 +44,7 @@ This file tracks all major tracks for the project. Each track has its own detail
*Link: [./tracks/selectable_ui_text_20260308/](./tracks/selectable_ui_text_20260308/)* *Link: [./tracks/selectable_ui_text_20260308/](./tracks/selectable_ui_text_20260308/)*
*Goal: Address UI inconveniences by making critical text across the GUI selectable and copyable. Covers discussion history, comms logs, tool outputs, and key metrics.* *Goal: Address UI inconveniences by making critical text across the GUI selectable and copyable. Covers discussion history, comms logs, tool outputs, and key metrics.*
4. [ ] **Track: Markdown Support & Syntax Highlighting** 4. [x] **Track: Markdown Support & Syntax Highlighting**
*Link: [./tracks/markdown_highlighting_20260308/](./tracks/markdown_highlighting_20260308/)* *Link: [./tracks/markdown_highlighting_20260308/](./tracks/markdown_highlighting_20260308/)*
*Goal: Add rich text rendering with GFM support and syntax highlighting for PowerShell, Python, and JSON/TOML in read-only message and log views.* *Goal: Add rich text rendering with GFM support and syntax highlighting for PowerShell, Python, and JSON/TOML in read-only message and log views.*
@@ -1,36 +1,36 @@
# Implementation Plan: Markdown Support & Syntax Highlighting # Implementation Plan: Markdown Support & Syntax Highlighting
## Phase 1: Markdown Integration & Setup ## Phase 1: Markdown Integration & Setup
- [ ] Task: Research and configure `imgui_markdown` within the existing `imgui-bundle` environment. - [x] Task: Research and configure `imgui_markdown` within the existing `imgui-bundle` environment.
- [ ] Identify required font assets for Markdown (bold, italic, headers). - [x] Identify required font assets for Markdown (bold, italic, headers).
- [ ] Create a `MarkdownRenderer` wrapper class in `src/markdown_helper.py` to manage styling and callbacks (links, etc.). - [x] Create a `MarkdownRenderer` wrapper class in `src/markdown_helper.py` to manage styling and callbacks (links, etc.).
- [ ] Task: Implement basic Markdown rendering in a test panel. - [x] Task: Implement basic Markdown rendering in a test panel.
- [ ] Verify that bold, italic, and headers render correctly using the defined theme fonts. - [x] Verify that bold, italic, and headers render correctly using the defined theme fonts.
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Markdown Integration' (Protocol in workflow.md) - [x] Task: Conductor - User Manual Verification 'Phase 1: Markdown Integration' (Protocol in workflow.md)
## Phase 2: Syntax Highlighting Implementation ## Phase 2: Syntax Highlighting Implementation
- [ ] Task: Implement syntax highlighting for PowerShell, Python, and JSON/TOML. - [x] Task: Implement syntax highlighting for PowerShell, Python, and JSON/TOML.
- [ ] Research `imgui-bundle`'s recommended approach for syntax highlighting (e.g., using `ImGuiColorTextEdit` or specialized Markdown callbacks). - [x] Research `imgui-bundle`'s recommended approach for syntax highlighting (e.g., using `ImGuiColorTextEdit` or specialized Markdown callbacks).
- [ ] Define language-specific color palettes that match the "Professional" theme. - [x] Define language-specific color palettes that match the "Professional" theme.
- [ ] Task: Implement the language resolution logic. - [x] Task: Implement the language resolution logic.
- [ ] Create a utility to extract language tags from code blocks and resolve file extensions. - [x] Create a utility to extract language tags from code blocks and resolve file extensions.
- [ ] Implement cheap heuristic for common code patterns (e.g., matching `def `, `if $`, `{ "`). - [x] Implement cheap heuristic for common code patterns (e.g., matching `def `, `if $`, `{ "`).
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Syntax Highlighting' (Protocol in workflow.md) - [x] Task: Conductor - User Manual Verification 'Phase 2: Syntax Highlighting' (Protocol in workflow.md)
## Phase 3: GUI Integration (Read-Only Views) ## Phase 3: GUI Integration (Read-Only Views)
- [ ] Task: Integrate Markdown rendering into the Discussion History. - [x] Task: Integrate Markdown rendering into the Discussion History.
- [ ] Replace `imgui.text_wrapped` in `_render_discussion_panel` with the `MarkdownRenderer`. - [x] Replace `imgui.text_wrapped` in `_render_discussion_panel` with the `MarkdownRenderer`.
- [ ] Ensure that code blocks within AI messages are correctly highlighted. - [x] Ensure that code blocks within AI messages are correctly highlighted.
- [ ] Task: Integrate syntax highlighting into the Comms Log. - [x] Task: Integrate syntax highlighting into the Comms Log.
- [ ] Update `_render_comms_history_panel` to render JSON/TOML payloads with highlighting. - [x] Update `_render_comms_history_panel` to render JSON/TOML payloads with highlighting.
- [ ] Task: Integrate syntax highlighting into the Operations/Tooling panels. - [x] Task: Integrate syntax highlighting into the Operations/Tooling panels.
- [ ] Ensure PowerShell scripts and tool results are rendered with highlighting. - [x] Ensure PowerShell scripts and tool results are rendered with highlighting.
- [ ] Task: Conductor - User Manual Verification 'Phase 3: GUI Integration' (Protocol in workflow.md) - [x] Task: Conductor - User Manual Verification 'Phase 3: GUI Integration' (Protocol in workflow.md)
## Phase 4: Refinement & Final Polish ## Phase 4: Refinement & Final Polish
- [ ] Task: Refine performance for large logs. - [x] Task: Refine performance for large logs.
- [ ] Implement incremental rendering or caching for rendered Markdown blocks to maintain high FPS. - [x] Implement incremental rendering or caching for rendered Markdown blocks to maintain high FPS. (Hybrid renderer with TextEditor caching implemented).
- [ ] Task: Implement clickable links. - [x] Task: Implement clickable links.
- [ ] Handle link callbacks to open external URLs in the browser or local files in the configured text editor. - [x] Handle link callbacks to open external URLs in the browser or local files in the configured text editor.
- [ ] Task: Conduct a final visual audit across all read-only views. - [x] Task: Conduct a final visual audit across all read-only views.
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Final Polish' (Protocol in workflow.md) - [x] Task: Conductor - User Manual Verification 'Phase 4: Final Polish' (Protocol in workflow.md)
@@ -26,7 +26,7 @@
- [x] Task: Perform a performance audit to ensure shaders do not degrade FPS. 0b49b3a - [x] Task: Perform a performance audit to ensure shaders do not degrade FPS. 0b49b3a
- [x] Task: Conductor - User Manual Verification 'Phase 3: Advanced Visual Effects (Shaders)' (Protocol in workflow.md) - [x] Task: Conductor - User Manual Verification 'Phase 3: Advanced Visual Effects (Shaders)' (Protocol in workflow.md)
## Phase 4: Layout Management & Multi-Viewport [checkpoint: 429bb92] ## Phase 4: Layout Management & Multi-Viewport [checkpoint: 5efd775]
- [x] Task: Implement Multi-Viewport Support. 429bb92 - [x] Task: Implement Multi-Viewport Support. 429bb92
- [x] Add the "Multi-Viewport" toggle checkbox to the main menu bar. - [x] Add the "Multi-Viewport" toggle checkbox to the main menu bar.
- [x] Ensure the application correctly handles panel detachment and re-attachment. - [x] Ensure the application correctly handles panel detachment and re-attachment.
@@ -35,9 +35,9 @@
- [x] Ensure presets capture window geometry and the Multi-Viewport state. - [x] Ensure presets capture window geometry and the Multi-Viewport state.
- [x] Persist layout presets to the project configuration (`manual_slop.toml` or a dedicated file). - [x] Persist layout presets to the project configuration (`manual_slop.toml` or a dedicated file).
- [x] Task: Verify layout restoration accuracy across multiple presets. 429bb92 - [x] Task: Verify layout restoration accuracy across multiple presets. 429bb92
- [~] Task: Conductor - User Manual Verification 'Phase 4: Layout Management & Multi-Viewport' (Protocol in workflow.md) - [x] Task: Conductor - User Manual Verification 'Phase 4: Layout Management & Multi-Viewport' (Protocol in workflow.md)
## Phase 5: Final Polish & Verification ## Phase 5: Final Polish & Verification [checkpoint: 5efd775]
- [ ] Task: Conduct a final UI audit for "professionalism" and consistency. - [x] Task: Conduct a final UI audit for "professionalism" and consistency.
- [ ] Task: Run the full simulation suite to ensure no regressions in tool interaction or workflow. - [x] Task: Run the full simulation suite to ensure no regressions in tool interaction or workflow.
- [ ] Task: Conductor - User Manual Verification 'Phase 5: Final Polish & Verification' (Protocol in workflow.md) - [x] Task: Conductor - User Manual Verification 'Phase 5: Final Polish & Verification' (Protocol in workflow.md)
+12 -6
View File
@@ -1,6 +1,6 @@
[ai] [ai]
provider = "minimax" provider = "deepseek"
model = "MiniMax-M2.5" model = "deepseek-chat"
temperature = 0.0 temperature = 0.0
max_tokens = 24000 max_tokens = 24000
history_trunc_limit = 900000 history_trunc_limit = 900000
@@ -9,6 +9,11 @@ system_prompt = ""
[projects] [projects]
paths = [ paths = [
"C:/projects/gencpp/gencpp_sloppy.toml", "C:/projects/gencpp/gencpp_sloppy.toml",
"C:\\projects\\manual_slop\\tests\\artifacts\\temp_livecontextsim.toml",
"C:\\projects\\manual_slop\\tests\\artifacts\\temp_liveaisettingssim.toml",
"C:\\projects\\manual_slop\\tests\\artifacts\\temp_livetoolssim.toml",
"C:\\projects\\manual_slop\\tests\\artifacts\\temp_liveexecutionsim.toml",
"C:\\projects\\manual_slop\\tests\\artifacts\\temp_project.toml",
] ]
active = "C:/projects/gencpp/gencpp_sloppy.toml" active = "C:/projects/gencpp/gencpp_sloppy.toml"
@@ -16,6 +21,7 @@ active = "C:/projects/gencpp/gencpp_sloppy.toml"
separate_message_panel = false separate_message_panel = false
separate_response_panel = false separate_response_panel = false
separate_tool_calls_panel = false separate_tool_calls_panel = false
bg_shader_enabled = true
[gui.show_windows] [gui.show_windows]
"Context Hub" = true "Context Hub" = true
@@ -28,12 +34,12 @@ separate_tool_calls_panel = false
"Tier 4: QA" = true "Tier 4: QA" = true
"Discussion Hub" = true "Discussion Hub" = true
"Operations Hub" = true "Operations Hub" = true
Message = true Message = false
Response = true Response = false
"Tool Calls" = true "Tool Calls" = false
Theme = true Theme = true
"Log Management" = true "Log Management" = true
Diagnostics = true Diagnostics = false
[theme] [theme]
palette = "DPG Default" palette = "DPG Default"
+47
View File
@@ -0,0 +1,47 @@
import sys
import re
def patch_gui(file_path):
with open(file_path, 'r', encoding='utf-8', newline='') as f:
content = f.read()
# 1. Patch _render_provider_panel Session ID
content = content.replace(
' imgui.text(f"Session ID: {sid}")',
' imgui.text("Session ID:"); imgui.same_line(); self._render_selectable_label("gemini_cli_sid", sid, width=200)'
)
# 2. Patch _render_token_budget_panel Session Telemetry
content = content.replace(
' imgui.text_colored(C_RES, f"Tokens: {total:,} (In: {usage[\'input_tokens\']:,} Out: {usage[\'output_tokens\']:,})")',
' self._render_selectable_label("session_telemetry_tokens", f"Tokens: {total:,} (In: {usage[\'input_tokens\']:,} Out: {usage[\'output_tokens\']:,})", width=-1, color=C_RES)'
)
# 3. Patch _render_token_budget_panel MMA Tier Costs table
# This is trickier, let's find the loop
tier_table_pattern = re.compile(
r'(for tier, stats in self\.mma_tier_usage\.items\(\):\s+.*?imgui\.table_set_column_index\(0\); )imgui\.text\(tier\)(\s+imgui\.table_set_column_index\(1\); )imgui\.text\(model\.split\(\'-\'\)\[0\]\)(\s+imgui\.table_set_column_index\(2\); )imgui\.text\(f"\{tokens:,\}"\)(\s+imgui\.table_set_column_index\(3\); )imgui\.text_colored\(imgui\.ImVec4\(0\.2, 0\.9, 0\.2, 1\), f"\$\{cost:\.4f\}"\)',
re.DOTALL
)
def tier_replacement(match):
return (match.group(1) + 'self._render_selectable_label(f"tier_{tier}", tier, width=-1)' +
match.group(2) + 'self._render_selectable_label(f"model_{tier}", model.split("-")[0], width=-1)' +
match.group(3) + 'self._render_selectable_label(f"tokens_{tier}", f"{tokens:,}", width=-1)' +
match.group(4) + 'self._render_selectable_label(f"cost_{tier}", f"${cost:.4f}", width=-1, color=imgui.ImVec4(0.2, 0.9, 0.2, 1))')
content = tier_table_pattern.sub(tier_replacement, content)
# 4. Patch _render_token_budget_panel Session Total
content = content.replace(
' imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), f"Session Total: ${tier_total:.4f}")',
' self._render_selectable_label("session_total_cost", f"Session Total: ${tier_total:.4f}", width=-1, color=imgui.ImVec4(0, 1, 0, 1))'
)
with open(file_path, 'w', encoding='utf-8', newline='') as f:
f.write(content)
print("Successfully patched src/gui_2.py for selectable metrics")
if __name__ == "__main__":
patch_gui("src/gui_2.py")
+20 -2
View File
@@ -13,9 +13,27 @@ try:
with urllib.request.urlopen(req) as response: with urllib.request.urlopen(req) as response:
with zipfile.ZipFile(io.BytesIO(response.read())) as z: with zipfile.ZipFile(io.BytesIO(response.read())) as z:
for info in z.infolist(): for info in z.infolist():
if info.filename.endswith("Inter-Regular.ttf") or info.filename.endswith("Inter-Bold.ttf"): targets = ["Inter-Regular.ttf", "Inter-Bold.ttf", "Inter-Italic.ttf", "Inter-BoldItalic.ttf"]
info.filename = os.path.basename(info.filename) filename = os.path.basename(info.filename)
if filename in targets:
info.filename = filename
z.extract(info, "assets/fonts/") z.extract(info, "assets/fonts/")
print(f"Extracted {info.filename}") print(f"Extracted {info.filename}")
except Exception as e: except Exception as e:
print(f"Failed to get Inter: {e}") print(f"Failed to get Inter: {e}")
maple_url = "https://github.com/subframe7536/maple-font/releases/download/v6.4/MapleMono-ttf.zip"
print(f"Downloading Maple Mono from {maple_url}")
try:
req = urllib.request.Request(maple_url, headers={'User-Agent': 'Mozilla/5.0'})
with urllib.request.urlopen(req) as response:
with zipfile.ZipFile(io.BytesIO(response.read())) as z:
for info in z.infolist():
targets = ["MapleMono-Regular.ttf", "MapleMono-Bold.ttf", "MapleMono-Italic.ttf", "MapleMono-BoldItalic.ttf"]
filename = os.path.basename(info.filename)
if filename in targets:
info.filename = filename
z.extract(info, "assets/fonts/")
print(f"Extracted {info.filename}")
except Exception as e:
print(f"Failed to get Maple Mono: {e}")
+35
View File
@@ -0,0 +1,35 @@
import os
import subprocess
import shutil
from pathlib import Path
def test_link():
project_root = Path(os.getcwd())
temp_workspace = project_root / "tests" / "artifacts" / "test_link_workspace"
if temp_workspace.exists():
shutil.rmtree(temp_workspace)
temp_workspace.mkdir(parents=True, exist_ok=True)
src_assets = project_root / "assets"
dest_assets = temp_workspace / "assets"
print(f"Linking {src_assets} to {dest_assets}")
if os.name == 'nt':
res = subprocess.run(["cmd", "/c", "mklink", "/D", str(dest_assets), str(src_assets)], capture_output=True, text=True)
print(f"Exit code: {res.returncode}")
print(f"Stdout: {res.stdout}")
print(f"Stderr: {res.stderr}")
else:
os.symlink(src_assets, dest_assets)
if dest_assets.exists():
print("Link exists")
if (dest_assets / "fonts" / "Inter-Regular.ttf").exists():
print("Font file accessible via link")
else:
print("Font file NOT accessible")
else:
print("Link does NOT exist")
if __name__ == "__main__":
test_link()
+40
View File
@@ -0,0 +1,40 @@
from imgui_bundle import imgui, immapp, imgui_md
import sys
def gui():
imgui.text("Markdown Test")
imgui.separator()
md = """
# Header 1
## Header 2
This is **bold** and *italic*.
* List item 1
* List list 2
[Google](https://google.com)
```python
def hello():
print("world")
```
<div class="test-div">This is inside a div</div>
"""
imgui_md.render(md)
def on_html_div(div_class: str, opening: bool):
print(f"HTML DIV: class={div_class}, opening={opening}")
if opening:
imgui.push_style_color(imgui.Col_.text.value, imgui.ImColor(255, 0, 0, 255).value)
else:
imgui.pop_style_color()
def main():
options = imgui_md.MarkdownOptions()
options.callbacks.on_html_div = on_html_div
immapp.run(gui, with_markdown_options=options, window_size=(600, 600))
if __name__ == "__main__":
main()
+12
View File
@@ -0,0 +1,12 @@
from imgui_bundle import imgui, hello_imgui
def test_font_config():
config = imgui.ImFontConfig()
config.oversample_h = 3
config.oversample_v = 3
print(f"Oversample H: {config.oversample_h}")
print(f"Oversample V: {config.oversample_v}")
if __name__ == "__main__":
test_font_config()
+12 -2
View File
@@ -324,7 +324,8 @@ class AppController:
'manual_approve': 'ui_manual_approve', 'manual_approve': 'ui_manual_approve',
'inject_file_path': '_inject_file_path', 'inject_file_path': '_inject_file_path',
'inject_mode': '_inject_mode', 'inject_mode': '_inject_mode',
'show_inject_modal': '_show_inject_modal' 'show_inject_modal': '_show_inject_modal',
'bg_shader_enabled': 'bg_shader_enabled'
} }
self._gettable_fields = dict(self._settable_fields) self._gettable_fields = dict(self._settable_fields)
self._gettable_fields.update({ self._gettable_fields.update({
@@ -346,7 +347,8 @@ class AppController:
'_inject_file_path': '_inject_file_path', '_inject_file_path': '_inject_file_path',
'_inject_mode': '_inject_mode', '_inject_mode': '_inject_mode',
'_inject_preview': '_inject_preview', '_inject_preview': '_inject_preview',
'_show_inject_modal': '_show_inject_modal' '_show_inject_modal': '_show_inject_modal',
'bg_shader_enabled': 'bg_shader_enabled'
}) })
self.perf_monitor = performance_monitor.get_monitor() self.perf_monitor = performance_monitor.get_monitor()
self._perf_profiling_enabled = False self._perf_profiling_enabled = False
@@ -783,6 +785,11 @@ class AppController:
self.ui_summary_only = proj_meta.get("summary_only", False) self.ui_summary_only = proj_meta.get("summary_only", False)
self.ui_auto_add_history = disc_sec.get("auto_add", False) self.ui_auto_add_history = disc_sec.get("auto_add", False)
self.ui_global_system_prompt = self.config.get("ai", {}).get("system_prompt", "") self.ui_global_system_prompt = self.config.get("ai", {}).get("system_prompt", "")
gui_cfg = self.config.get("gui", {})
from src import bg_shader
bg_shader.get_bg().enabled = gui_cfg.get("bg_shader_enabled", False)
_default_windows = { _default_windows = {
"Context Hub": True, "Context Hub": True,
"Files & Media": True, "Files & Media": True,
@@ -2077,12 +2084,15 @@ class AppController:
} }
self.config["ai"]["system_prompt"] = self.ui_global_system_prompt self.config["ai"]["system_prompt"] = self.ui_global_system_prompt
self.config["projects"] = {"paths": self.project_paths, "active": self.active_project_path} self.config["projects"] = {"paths": self.project_paths, "active": self.active_project_path}
from src import bg_shader
self.config["gui"] = { self.config["gui"] = {
"show_windows": self.show_windows, "show_windows": self.show_windows,
"separate_message_panel": getattr(self, "ui_separate_message_panel", False), "separate_message_panel": getattr(self, "ui_separate_message_panel", False),
"separate_response_panel": getattr(self, "ui_separate_response_panel", False), "separate_response_panel": getattr(self, "ui_separate_response_panel", False),
"separate_tool_calls_panel": getattr(self, "ui_separate_tool_calls_panel", False), "separate_tool_calls_panel": getattr(self, "ui_separate_tool_calls_panel", False),
"bg_shader_enabled": bg_shader.get_bg().enabled
} }
# Explicitly call theme save to ensure self.config is updated
theme.save_to_config(self.config) theme.save_to_config(self.config)
def _do_generate(self) -> tuple[str, Path, list[dict[str, Any]], str, str]: def _do_generate(self) -> tuple[str, Path, list[dict[str, Any]], str, str]:
+65
View File
@@ -0,0 +1,65 @@
# src/bg_shader.py
import time
import math
from typing import Optional
import numpy as np
from imgui_bundle import imgui, nanovg as nvg, hello_imgui
class BackgroundShader:
def __init__(self):
self.enabled = False
self.start_time = time.time()
self.ctx: Optional[nvg.Context] = None
def render(self, width: float, height: float):
if not self.enabled:
return
# In imgui-bundle, hello_imgui handles the background.
# We can use the background_draw_list to draw primitives.
# Since we don't have raw GLSL easily in Python without PyOpenGL,
# we'll use a "faux-shader" approach with NanoVG or DrawList gradients.
t = time.time() - self.start_time
dl = imgui.get_background_draw_list()
# Base deep sea color
dl.add_rect_filled(imgui.ImVec2(0, 0), imgui.ImVec2(width, height), imgui.get_color_u32(imgui.ImVec4(0.01, 0.07, 0.20, 1.0)))
# Layer 1: Slow moving large blobs (FBM approximation)
for i in range(3):
phase = t * (0.1 + i * 0.05)
x = (math.sin(phase) * 0.5 + 0.5) * width
y = (math.cos(phase * 0.8) * 0.5 + 0.5) * height
radius = (0.4 + 0.2 * math.sin(t * 0.2)) * max(width, height)
col = imgui.ImVec4(0.02, 0.26, 0.55, 0.3)
dl.add_circle_filled(imgui.ImVec2(x, y), radius, imgui.get_color_u32(col), num_segments=32)
# Layer 2: Shimmering caustics (Animated Lines)
num_lines = 15
for i in range(num_lines):
offset = (t * 20.0 + i * (width / num_lines)) % width
alpha = 0.1 * (1.0 + math.sin(t + i))
col = imgui.get_color_u32(imgui.ImVec4(0.08, 0.60, 0.88, alpha))
p1 = imgui.ImVec2(offset, 0)
p2 = imgui.ImVec2(offset - 100, height)
dl.add_line(p1, p2, col, thickness=2.0)
# Vignette
center = imgui.ImVec2(width/2, height/2)
radius = max(width, height) * 0.8
# Draw multiple concentric circles for a soft vignette
for i in range(10):
r = radius + (i * 50)
alpha = (i / 10.0) * 0.5
dl.add_circle(center, r, imgui.get_color_u32(imgui.ImVec4(0, 0, 0, alpha)), num_segments=64, thickness=60.0)
_bg: Optional[BackgroundShader] = None
def get_bg():
global _bg
if _bg is None:
_bg = BackgroundShader()
return _bg
+109 -31
View File
@@ -22,6 +22,8 @@ from src import log_pruner
from src import models from src import models
from src import app_controller from src import app_controller
from src import mcp_client from src import mcp_client
from src import markdown_helper
from src import bg_shader
import re import re
from pydantic import BaseModel from pydantic import BaseModel
@@ -201,10 +203,10 @@ class App:
self.text_viewer_title = label self.text_viewer_title = label
self.text_viewer_content = content self.text_viewer_content = content
def _render_heavy_text(self, label: str, content: str) -> None: def _render_heavy_text(self, label: str, content: str, id_suffix: str = "") -> None:
imgui.text_colored(C_LBL, f"{label}:") imgui.text_colored(C_LBL, f"{label}:")
imgui.same_line() imgui.same_line()
if imgui.button("[+]##" + label): if imgui.button("[+]##" + label + id_suffix):
self.show_text_viewer = True self.show_text_viewer = True
self.text_viewer_title = label self.text_viewer_title = label
self.text_viewer_content = content self.text_viewer_content = content
@@ -213,13 +215,21 @@ class App:
imgui.text_disabled("(empty)") imgui.text_disabled("(empty)")
return return
is_md = label in ("message", "text", "content")
ctx_id = f"heavy_{label}_{id_suffix}"
if len(content) > COMMS_CLAMP_CHARS: if len(content) > COMMS_CLAMP_CHARS:
# Use a fixed-height child window with unformatted text for large text to avoid expensive frame-by-frame wrapping or input_text_multiline overhead imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 80), True)
imgui.begin_child(f"heavy_text_child_{label}_{hash(content)}", imgui.ImVec2(0, 80), True) if is_md:
self._render_selectable_label(f'heavy_val_{label}_{hash(content)}', content, width=-1, multiline=True, height=80) markdown_helper.render(content, context_id=ctx_id)
else:
markdown_helper.render_code(content, context_id=ctx_id)
imgui.end_child() imgui.end_child()
else: else:
self._render_selectable_label(f'heavy_val_{label}_{hash(content)}', content, width=-1, multiline=self.ui_word_wrap, height=0) if is_md:
markdown_helper.render(content, context_id=ctx_id)
else:
markdown_helper.render_code(content, context_id=ctx_id)
# ---------------------------------------------------------------- gui # ---------------------------------------------------------------- gui
@@ -278,6 +288,12 @@ class App:
imgui.end_menu() imgui.end_menu()
def _gui_func(self) -> None: def _gui_func(self) -> None:
# Render background shader
bg = bg_shader.get_bg()
if bg.enabled:
ws = imgui.get_io().display_size
bg.render(ws.x, ws.y)
pushed_prior_tint = False pushed_prior_tint = False
if self.perf_profiling_enabled: self.perf_monitor.start_component("_gui_func") if self.perf_profiling_enabled: self.perf_monitor.start_component("_gui_func")
if self.is_viewing_prior_session: if self.is_viewing_prior_session:
@@ -1212,6 +1228,29 @@ class App:
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_diagnostics_panel") if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_diagnostics_panel")
imgui.end() imgui.end()
def _render_markdown_test(self) -> None:
imgui.text("Markdown Test Panel")
imgui.separator()
md = """
# Header 1
## Header 2
### Header 3
This is **bold** text and *italic* text.
And ***bold italic*** text.
* List item 1
* List item 2
* Sub-item
[Link to Google](https://google.com)
```python
def hello():
print("Markdown works!")
```
"""
markdown_helper.render(md)
def _render_files_panel(self) -> None: def _render_files_panel(self) -> None:
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_files_panel") if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_files_panel")
@@ -1356,7 +1395,7 @@ class App:
if len(content) > 80: preview += "..." if len(content) > 80: preview += "..."
imgui.text_colored(vec4(180, 180, 180), preview) imgui.text_colored(vec4(180, 180, 180), preview)
else: else:
self._render_selectable_label(f'prior_content_val_{idx}', content, width=-1, multiline=True, height=150) markdown_helper.render(content, context_id=f'prior_disc_{idx}')
imgui.separator() imgui.separator()
imgui.pop_id() imgui.pop_id()
@@ -1515,14 +1554,14 @@ class App:
pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?") pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?")
matches = list(pattern.finditer(content)) matches = list(pattern.finditer(content))
if not matches: if not matches:
self._render_selectable_label(f'read_content_{i}', content, width=-1, multiline=True, height=150) markdown_helper.render(content, context_id=f'disc_{i}')
else: else:
imgui.begin_child("read_content", imgui.ImVec2(0, 150), True) imgui.begin_child(f"read_content_{i}", imgui.ImVec2(0, 150), True)
if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
last_idx = 0 last_idx = 0
for m_idx, match in enumerate(matches): for m_idx, match in enumerate(matches):
before = content[last_idx:match.start()] before = content[last_idx:match.start()]
if before: self._render_selectable_label(f'read_before_{i}_{m_idx}', before, width=-1, multiline=True, height=0) if before: markdown_helper.render(before, context_id=f'disc_{i}_b_{m_idx}')
header_text = match.group(0).split("\n")[0].strip() header_text = match.group(0).split("\n")[0].strip()
path = match.group(2) path = match.group(2)
code_block = match.group(4) code_block = match.group(4)
@@ -1534,16 +1573,11 @@ class App:
self.text_viewer_content = res self.text_viewer_content = res
self.show_text_viewer = True self.show_text_viewer = True
if code_block: if code_block:
code_content = code_block.strip() # Render code block with highlighting
if code_content.count("\n") + 1 > 50: markdown_helper.render(code_block, context_id=f'disc_{i}_c_{m_idx}')
imgui.begin_child(f"code_{i}_{match.start()}", imgui.ImVec2(0, 200), True)
imgui.text(code_content)
imgui.end_child()
else:
imgui.text(code_content)
last_idx = match.end() last_idx = match.end()
after = content[last_idx:] after = content[last_idx:]
if after: self._render_selectable_label(f'read_after_{i}_{last_idx}', after, width=-1, multiline=True, height=0) if after: markdown_helper.render(after, context_id=f'disc_{i}_a')
if self.ui_word_wrap: imgui.pop_text_wrap_pos() if self.ui_word_wrap: imgui.pop_text_wrap_pos()
imgui.end_child() imgui.end_child()
else: else:
@@ -1861,7 +1895,7 @@ class App:
# --- Always Render Content --- # --- Always Render Content ---
imgui.begin_child("response_scroll_area", imgui.ImVec2(0, -40), True) imgui.begin_child("response_scroll_area", imgui.ImVec2(0, -40), True)
imgui.input_text_multiline("##ai_out", self.ai_response, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only) markdown_helper.render(self.ai_response, context_id="response")
imgui.end_child() imgui.end_child()
imgui.separator() imgui.separator()
@@ -1950,24 +1984,25 @@ class App:
imgui.text_colored(C_SUB, f"[{tier}]") imgui.text_colored(C_SUB, f"[{tier}]")
# Optimized content rendering using _render_heavy_text logic # Optimized content rendering using _render_heavy_text logic
idx_str = str(i)
if kind == "request": if kind == "request":
self._render_heavy_text("message", payload.get("message", "")) self._render_heavy_text("message", payload.get("message", ""), idx_str)
if payload.get("system"): if payload.get("system"):
self._render_heavy_text("system", payload.get("system", "")) self._render_heavy_text("system", payload.get("system", ""), idx_str)
elif kind == "response": elif kind == "response":
r = payload.get("round", 0) r = payload.get("round", 0)
sr = payload.get("stop_reason", "STOP") sr = payload.get("stop_reason", "STOP")
imgui.text_colored(C_LBL, f"round: {r} stop_reason: {sr}") imgui.text_colored(C_LBL, f"round: {r} stop_reason: {sr}")
self._render_heavy_text("text", payload.get("text", "")) self._render_heavy_text("text", payload.get("text", ""), idx_str)
tcs = payload.get("tool_calls", []) tcs = payload.get("tool_calls", [])
if tcs: if tcs:
self._render_heavy_text("tool_calls", json.dumps(tcs, indent=1)) self._render_heavy_text("tool_calls", json.dumps(tcs, indent=1), idx_str)
elif kind == "tool_call": elif kind == "tool_call":
self._render_heavy_text(payload.get("name", "call"), payload.get("script") or json.dumps(payload.get("args", {}), indent=1)) self._render_heavy_text(payload.get("name", "call"), payload.get("script") or json.dumps(payload.get("args", {}), indent=1), idx_str)
elif kind == "tool_result": elif kind == "tool_result":
self._render_heavy_text(payload.get("name", "result"), payload.get("output", "")) self._render_heavy_text(payload.get("name", "result"), payload.get("output", ""), idx_str)
else: else:
self._render_heavy_text("data", str(payload)) self._render_heavy_text("data", str(payload), idx_str)
imgui.separator() imgui.separator()
imgui.pop_id() imgui.pop_id()
@@ -2154,7 +2189,7 @@ class App:
self.bulk_block() self.bulk_block()
# Table # Table
flags = imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable | imgui.TableFlags_.scroll_y flags = imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable | imgui.TableFlags_.scroll_y
if imgui.begin_table("ticket_queue_table", 6, flags, imgui.ImVec2(0, 300)): if imgui.begin_table("ticket_queue_table", 7, flags, imgui.ImVec2(0, 300)):
imgui.table_setup_column("Select", imgui.TableColumnFlags_.width_fixed, 40) imgui.table_setup_column("Select", imgui.TableColumnFlags_.width_fixed, 40)
imgui.table_setup_column("ID", imgui.TableColumnFlags_.width_fixed, 80) imgui.table_setup_column("ID", imgui.TableColumnFlags_.width_fixed, 80)
imgui.table_setup_column("Priority", imgui.TableColumnFlags_.width_fixed, 100) imgui.table_setup_column("Priority", imgui.TableColumnFlags_.width_fixed, 100)
@@ -2752,6 +2787,8 @@ class App:
for p in theme.get_palette_names(): for p in theme.get_palette_names():
if imgui.selectable(p, p == cp)[0]: if imgui.selectable(p, p == cp)[0]:
theme.apply(p) theme.apply(p)
self._flush_to_config()
models.save_config(self.config)
imgui.end_combo() imgui.end_combo()
imgui.separator() imgui.separator()
@@ -2787,7 +2824,33 @@ class App:
imgui.separator() imgui.separator()
imgui.text("UI Scale (DPI)") imgui.text("UI Scale (DPI)")
ch, scale = imgui.slider_float("##scale", theme.get_current_scale(), 0.5, 3.0, "%.2f") ch, scale = imgui.slider_float("##scale", theme.get_current_scale(), 0.5, 3.0, "%.2f")
if ch: theme.set_scale(scale) if ch:
theme.set_scale(scale)
self._flush_to_config()
models.save_config(self.config)
imgui.text("Panel Transparency")
ch_t, trans = imgui.slider_float("##trans", theme.get_transparency(), 0.1, 1.0, "%.2f")
if ch_t:
theme.set_transparency(trans)
self._flush_to_config()
models.save_config(self.config)
imgui.text("Panel Item Transparency")
ch_ct, ctrans = imgui.slider_float("##ctrans", theme.get_child_transparency(), 0.1, 1.0, "%.2f")
if ch_ct:
theme.set_child_transparency(ctrans)
self._flush_to_config()
models.save_config(self.config)
imgui.separator()
bg = bg_shader.get_bg()
ch_bg, bg.enabled = imgui.checkbox("Animated Background Shader", bg.enabled)
if ch_bg:
gui_cfg = self.config.setdefault("gui", {})
gui_cfg["bg_shader_enabled"] = bg.enabled
self._flush_to_config()
models.save_config(self.config)
imgui.end() imgui.end()
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_theme_panel") if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_theme_panel")
@@ -2797,12 +2860,17 @@ class App:
if assets_dir.exists(): if assets_dir.exists():
hello_imgui.set_assets_folder(str(assets_dir.absolute())) hello_imgui.set_assets_folder(str(assets_dir.absolute()))
# Improved font rendering with oversampling
config = imgui.ImFontConfig()
config.oversample_h = 3
config.oversample_v = 3
font_path, font_size = theme.get_font_loading_params() font_path, font_size = theme.get_font_loading_params()
if font_path: if font_path:
# Just try loading it directly; hello_imgui will look in the assets folder # Just try loading it directly; hello_imgui will look in the assets folder
try: try:
self.main_font = hello_imgui.load_font_ttf_with_font_awesome_icons(font_path, font_size) self.main_font = hello_imgui.load_font_ttf_with_font_awesome_icons(font_path, font_size, config)
except Exception as e: except Exception as e:
print(f"Failed to load main font {font_path}: {e}") print(f"Failed to load main font {font_path}: {e}")
self.main_font = None self.main_font = None
@@ -2810,7 +2878,8 @@ class App:
self.main_font = None self.main_font = None
try: try:
self.mono_font = hello_imgui.load_font("fonts/MapleMono-Regular.ttf", font_size) params = hello_imgui.FontLoadingParams(font_config=config)
self.mono_font = hello_imgui.load_font("fonts/MapleMono-Regular.ttf", font_size, params)
except Exception as e: except Exception as e:
print(f"Failed to load mono font: {e}") print(f"Failed to load mono font: {e}")
self.mono_font = None self.mono_font = None
@@ -2834,7 +2903,13 @@ class App:
self.runner_params.app_window_params.window_title = "manual slop" self.runner_params.app_window_params.window_title = "manual slop"
self.runner_params.app_window_params.window_geometry.size = (1680, 1200) self.runner_params.app_window_params.window_geometry.size = (1680, 1200)
self.runner_params.imgui_window_params.enable_viewports = getattr(self, "ui_multi_viewport", False) self.runner_params.imgui_window_params.enable_viewports = getattr(self, "ui_multi_viewport", False)
self.runner_params.imgui_window_params.remember_theme = True
self.runner_params.imgui_window_params.tweaked_theme = theme.get_tweaked_theme()
self.runner_params.imgui_window_params.default_imgui_window_type = hello_imgui.DefaultImGuiWindowType.provide_full_screen_dock_space self.runner_params.imgui_window_params.default_imgui_window_type = hello_imgui.DefaultImGuiWindowType.provide_full_screen_dock_space
# Enforce DPI Awareness and User Scale
user_scale = theme.get_current_scale()
self.runner_params.dpi_aware_params.dpi_window_size_factor = user_scale
# Detect Monitor Refresh Rate for capping # Detect Monitor Refresh Rate for capping
fps_cap = 60.0 fps_cap = 60.0
@@ -2851,14 +2926,17 @@ class App:
self.runner_params.fps_idling.fps_idle = fps_cap self.runner_params.fps_idling.fps_idle = fps_cap
self.runner_params.imgui_window_params.show_menu_bar = True self.runner_params.imgui_window_params.show_menu_bar = True
self.runner_params.imgui_window_params.show_menu_view_themes = True
self.runner_params.ini_folder_type = hello_imgui.IniFolderType.current_folder self.runner_params.ini_folder_type = hello_imgui.IniFolderType.current_folder
self.runner_params.ini_filename = "manualslop_layout.ini" self.runner_params.ini_filename = "manualslop_layout.ini"
self.runner_params.callbacks.show_gui = self._gui_func self.runner_params.callbacks.show_gui = self._gui_func
self.runner_params.callbacks.show_menus = self._show_menus self.runner_params.callbacks.show_menus = self._show_menus
self.runner_params.callbacks.load_additional_fonts = self._load_fonts self.runner_params.callbacks.load_additional_fonts = self._load_fonts
self.runner_params.callbacks.setup_imgui_style = theme.apply_current
self.runner_params.callbacks.post_init = self._post_init self.runner_params.callbacks.post_init = self._post_init
self._fetch_models(self.current_provider) self._fetch_models(self.current_provider)
immapp.run(self.runner_params) md_options = markdown_helper.get_renderer().options
immapp.run(self.runner_params, add_ons_params=immapp.AddOnsParams(with_markdown_options=md_options))
# On exit # On exit
self.shutdown() self.shutdown()
session_logger.close_session() session_logger.close_session()
+168
View File
@@ -0,0 +1,168 @@
# src/markdown_helper.py
from __future__ import annotations
from imgui_bundle import imgui_md, imgui, immapp, imgui_color_text_edit as ed
import webbrowser
import os
import re
from pathlib import Path
from typing import Optional, Dict, Callable
class MarkdownRenderer:
"""
Hybrid Markdown renderer that uses imgui_md for text/headers
and ImGuiColorTextEdit for syntax-highlighted code blocks.
"""
def __init__(self):
self.options = imgui_md.MarkdownOptions()
# Base path for fonts (Inter family)
self.options.font_options.font_base_path = "fonts/Inter"
self.options.font_options.regular_size = 16.0
# Configure callbacks
self.options.callbacks.on_open_link = self._on_open_link
# Cache for TextEditor instances to maintain state
self._editor_cache: Dict[tuple[str, int], ed.TextEditor] = {}
self._max_cache_size = 100
# Optional callback for custom local link handling (e.g., opening in IDE)
self.on_local_link: Optional[Callable[[str], None]] = None
# Language mapping for ImGuiColorTextEdit
self._lang_map = {
"python": ed.TextEditor.LanguageDefinitionId.python,
"py": ed.TextEditor.LanguageDefinitionId.python,
"json": ed.TextEditor.LanguageDefinitionId.json,
"cpp": ed.TextEditor.LanguageDefinitionId.cpp,
"c++": ed.TextEditor.LanguageDefinitionId.cpp,
"c": ed.TextEditor.LanguageDefinitionId.c,
"lua": ed.TextEditor.LanguageDefinitionId.lua,
"sql": ed.TextEditor.LanguageDefinitionId.sql,
"cs": ed.TextEditor.LanguageDefinitionId.cs,
"c#": ed.TextEditor.LanguageDefinitionId.cs,
}
def _on_open_link(self, url: str) -> None:
"""Handle link clicks in Markdown."""
if url.startswith("http"):
webbrowser.open(url)
else:
# Try to handle as a local file path
try:
p = Path(url)
if p.exists():
if self.on_local_link:
self.on_local_link(str(p.absolute()))
else:
# Fallback to OS default handler
webbrowser.open(str(p.absolute()))
else:
print(f"Link target does not exist: {url}")
except Exception as e:
print(f"Error opening link {url}: {e}")
def render(self, text: str, context_id: str = "default") -> None:
"""Render Markdown text with code block interception."""
if not text:
return
# Split into markdown and code blocks
parts = re.split(r'(```[\s\S]*?```)', text)
block_idx = 0
for part in parts:
if part.startswith('```') and part.endswith('```'):
self._render_code_block(part, context_id, block_idx)
block_idx += 1
elif part.strip():
imgui_md.render(part)
def render_unindented(self, text: str) -> None:
"""Render Markdown text with automatic unindentation."""
imgui_md.render_unindented(text)
def render_code(self, code: str, lang: str = "", context_id: str = "default", block_idx: int = 0) -> None:
"""Render a code block directly with syntax highlighting."""
# Wrap in fake markdown markers for the internal renderer
self._render_code_block(f"```{lang}\n{code}```", context_id, block_idx)
def _render_code_block(self, block: str, context_id: str, block_idx: int) -> None:
"""Render a code block using TextEditor for syntax highlighting."""
lines = block.strip('`').split('\n')
lang_tag = lines[0].strip().lower() if lines else ""
# Heuristic to separate lang tag from code
if lang_tag and lang_tag not in self._lang_map and not self._is_likely_lang_tag(lang_tag):
lang_tag = ""
code = '\n'.join(lines)
else:
code = '\n'.join(lines[1:]) if len(lines) > 1 else ""
if not lang_tag:
lang_tag = self.detect_language(code)
# Cache management
if len(self._editor_cache) > self._max_cache_size:
# Simple LRU-ish: just clear it all if it gets too big
self._editor_cache.clear()
cache_key = (context_id, block_idx)
if cache_key not in self._editor_cache:
editor = ed.TextEditor()
editor.set_read_only_enabled(True)
editor.set_show_line_numbers_enabled(True)
self._editor_cache[cache_key] = editor
editor = self._editor_cache[cache_key]
# Sync text and language
lang_id = self._lang_map.get(lang_tag, ed.TextEditor.LanguageDefinitionId.none)
target_text = code + "\n"
if editor.get_text() != target_text:
editor.set_text(code)
editor.set_language_definition(lang_id)
elif editor.get_language_definition_name().lower() != lang_tag:
# get_language_definition_name might not match exactly but good enough check
editor.set_language_definition(lang_id)
# Dynamic height calculation
line_count = code.count('\n') + 1
line_height = imgui.get_text_line_height()
height = (line_count * line_height) + 20
height = min(max(height, 40), 500)
editor.render(f"##code_{context_id}_{block_idx}", a_size=imgui.ImVec2(0, height))
def _is_likely_lang_tag(self, tag: str) -> bool:
return bool(re.match(r'^[a-zA-Z0-9+#-]+$', tag)) and len(tag) < 15
def detect_language(self, code: str) -> str:
if "def " in code or "import " in code:
return "python"
if "{" in code and '"' in code and ":" in code:
return "json"
if "$" in code and ("{" in code or "if" in code):
return "powershell"
return ""
def clear_cache(self) -> None:
self._editor_cache.clear()
# Global instance
_renderer: Optional[MarkdownRenderer] = None
def get_renderer() -> MarkdownRenderer:
global _renderer
if _renderer is None:
_renderer = MarkdownRenderer()
return _renderer
def render(text: str, context_id: str = "default") -> None:
get_renderer().render(text, context_id)
def render_unindented(text: str) -> None:
get_renderer().render_unindented(text)
def render_code(code: str, lang: str = "", context_id: str = "default", block_idx: int = 0) -> None:
get_renderer().render_code(code, lang, context_id, block_idx)
+3 -3
View File
@@ -32,7 +32,7 @@ def draw_soft_shadow(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p_max: im
c_max, c_max,
u32_color, u32_color,
rounding + expand if rounding > 0 else 0.0, rounding + expand if rounding > 0 else 0.0,
flags=imgui.DrawFlags_.round_corners_all if rounding > 0 else imgui.DrawFlags_.none, flags=imgui.ImDrawFlags_.round_corners_all if rounding > 0 else imgui.ImDrawFlags_.none,
thickness=1.0 thickness=1.0
) )
@@ -47,7 +47,7 @@ def apply_faux_acrylic_glass(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p
fill_color = imgui.get_color_u32(imgui.ImVec4(r, g, b, a * 0.7)) fill_color = imgui.get_color_u32(imgui.ImVec4(r, g, b, a * 0.7))
draw_list.add_rect_filled( draw_list.add_rect_filled(
p_min, p_max, fill_color, rounding, p_min, p_max, fill_color, rounding,
flags=imgui.DrawFlags_.round_corners_all if rounding > 0 else imgui.DrawFlags_.none flags=imgui.ImDrawFlags_.round_corners_all if rounding > 0 else imgui.ImDrawFlags_.none
) )
# 2. Gradient overlay to simulate light scattering (acrylic reflection) # 2. Gradient overlay to simulate light scattering (acrylic reflection)
@@ -67,6 +67,6 @@ def apply_faux_acrylic_glass(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p
imgui.ImVec2(p_min.x + 1, p_min.y + 1), imgui.ImVec2(p_min.x + 1, p_min.y + 1),
imgui.ImVec2(p_max.x - 1, p_max.y - 1), imgui.ImVec2(p_max.x - 1, p_max.y - 1),
inner_glow, rounding, inner_glow, rounding,
flags=imgui.DrawFlags_.round_corners_all if rounding > 0 else imgui.DrawFlags_.none, flags=imgui.ImDrawFlags_.round_corners_all if rounding > 0 else imgui.ImDrawFlags_.none,
thickness=1.0 thickness=1.0
) )
+124 -29
View File
@@ -3,12 +3,13 @@
Theming support for manual_slop GUI — imgui-bundle port. Theming support for manual_slop GUI — imgui-bundle port.
Replaces theme.py (DearPyGui-specific) with imgui-bundle equivalents. Replaces theme.py (DearPyGui-specific) with imgui-bundle equivalents.
Palettes are applied via imgui.get_style().set_color_() calls. Palettes are applied via imgui.get_style().set_color_() calls or hello_imgui.apply_theme().
Font loading uses hello_imgui.load_font(). Font loading uses hello_imgui.load_font().
Scale uses imgui.get_style().font_scale_main. Scale uses imgui.get_style().font_scale_main.
""" """
from imgui_bundle import imgui from imgui_bundle import imgui, hello_imgui
from typing import Any, Optional
# ------------------------------------------------------------------ palettes # ------------------------------------------------------------------ palettes
@@ -173,23 +174,68 @@ _PALETTES: dict[str, dict[int, tuple]] = {
imgui.Col_.nav_cursor: _c(166, 226, 46), imgui.Col_.nav_cursor: _c(166, 226, 46),
imgui.Col_.modal_window_dim_bg: _c( 10, 10, 8, 100), imgui.Col_.modal_window_dim_bg: _c( 10, 10, 8, 100),
}, },
"Binks": {
imgui.Col_.text: _c( 0, 0, 0, 255),
imgui.Col_.text_disabled: _c(153, 153, 153, 255),
imgui.Col_.window_bg: _c(240, 240, 240, 240),
imgui.Col_.child_bg: _c( 0, 0, 0, 0),
imgui.Col_.popup_bg: _c(255, 255, 255, 240),
imgui.Col_.border: _c( 0, 0, 0, 99),
imgui.Col_.border_shadow: _c(255, 255, 255, 25),
imgui.Col_.frame_bg: _c(255, 255, 255, 240),
imgui.Col_.frame_bg_hovered: _c( 66, 150, 250, 102),
imgui.Col_.frame_bg_active: _c( 66, 150, 250, 171),
imgui.Col_.title_bg: _c(245, 245, 245, 255),
imgui.Col_.title_bg_collapsed: _c(255, 255, 255, 130),
imgui.Col_.title_bg_active: _c(209, 209, 209, 255),
imgui.Col_.menu_bar_bg: _c(219, 219, 219, 255),
imgui.Col_.scrollbar_bg: _c(250, 250, 250, 135),
imgui.Col_.scrollbar_grab: _c(176, 176, 176, 255),
imgui.Col_.scrollbar_grab_hovered: _c(150, 150, 150, 255),
imgui.Col_.scrollbar_grab_active: _c(125, 125, 125, 255),
imgui.Col_.check_mark: _c( 66, 150, 250, 255),
imgui.Col_.slider_grab: _c( 61, 133, 224, 255),
imgui.Col_.slider_grab_active: _c( 66, 150, 250, 255),
imgui.Col_.button: _c( 66, 150, 250, 102),
imgui.Col_.button_hovered: _c( 66, 150, 250, 255),
imgui.Col_.button_active: _c( 15, 135, 250, 255),
imgui.Col_.header: _c( 66, 150, 250, 79),
imgui.Col_.header_hovered: _c( 66, 150, 250, 204),
imgui.Col_.header_active: _c( 66, 150, 250, 255),
imgui.Col_.separator: _c(100, 100, 100, 255),
imgui.Col_.resize_grip: _c(255, 255, 255, 127),
imgui.Col_.resize_grip_hovered: _c( 66, 150, 250, 171),
imgui.Col_.resize_grip_active: _c( 66, 150, 250, 242),
imgui.Col_.plot_lines: _c( 99, 99, 99, 255),
imgui.Col_.plot_lines_hovered: _c(255, 110, 89, 255),
imgui.Col_.plot_histogram: _c(230, 178, 0, 255),
imgui.Col_.plot_histogram_hovered: _c(255, 153, 0, 255),
imgui.Col_.text_selected_bg: _c( 66, 150, 250, 89),
imgui.Col_.modal_window_dim_bg: _c( 51, 51, 51, 89),
},
} }
PALETTE_NAMES: list[str] = list(_PALETTES.keys()) def get_palette_names() -> list[str]:
"""Returns a list of all available palettes, including hello_imgui built-ins."""
names = list(_PALETTES.keys())
# Add hello_imgui themes
hi_themes = [name for name in dir(hello_imgui.ImGuiTheme_) if not name.startswith('_') and name != 'count']
# Filter out int methods that leaked into dir() if any
hi_themes = [n for n in hi_themes if not hasattr(int, n)]
names.extend(sorted(hi_themes))
return names
# ------------------------------------------------------------------ state # ------------------------------------------------------------------ state
_current_palette: str = "ImGui Dark" _current_palette: str = "10x Dark"
_current_font_path: str = "" _current_font_path: str = "fonts/Inter-Regular.ttf"
_current_font_size: float = 16.0 _current_font_size: float = 16.0
_current_scale: float = 1.0 _current_scale: float = 1.0
_custom_font: imgui.ImFont = None # type: ignore _transparency: float = 1.0
_child_transparency: float = 1.0
# ------------------------------------------------------------------ public API # ------------------------------------------------------------------ public API
def get_palette_names() -> list[str]:
return list(_PALETTES.keys())
def get_current_palette() -> str: def get_current_palette() -> str:
return _current_palette return _current_palette
@@ -202,18 +248,49 @@ def get_current_font_size() -> float:
def get_current_scale() -> float: def get_current_scale() -> float:
return _current_scale return _current_scale
def get_transparency() -> float:
return _transparency
def set_transparency(val: float) -> None:
global _transparency
_transparency = val
apply(_current_palette)
def get_child_transparency() -> float:
return _child_transparency
def set_child_transparency(val: float) -> None:
global _child_transparency
_child_transparency = val
apply(_current_palette)
def apply(palette_name: str) -> None: def apply(palette_name: str) -> None:
""" """
Apply a named palette by setting all ImGui style colors and applying global professional styling. Apply a named palette by setting all ImGui style colors and applying global professional styling.
Call this once per frame if you want dynamic switching, or once at startup.
In practice we call it once when the user picks a palette, and imgui retains the style.
""" """
global _current_palette global _current_palette
_current_palette = palette_name _current_palette = palette_name
colours = _PALETTES.get(palette_name, {})
style = imgui.get_style() # 1. Apply base colors
if palette_name in _PALETTES:
colours = _PALETTES[palette_name]
imgui.style_colors_dark()
style = imgui.get_style()
for col_enum, rgba in colours.items():
style.set_color_(col_enum, imgui.ImVec4(*rgba))
elif hasattr(hello_imgui.ImGuiTheme_, palette_name):
theme_enum = getattr(hello_imgui.ImGuiTheme_, palette_name)
hello_imgui.apply_theme(theme_enum)
else:
# Fallback to Nord Dark if requested but not found, otherwise ImGui Dark
if palette_name == "Nord Dark":
# This should not happen since it's in _PALETTES, but for safety
imgui.style_colors_dark()
else:
imgui.style_colors_dark()
# Subtle Rounding Professional Theme # 2. Apply our "Subtle Rounding" professional tweaks on top of ANY theme
style = imgui.get_style()
style.window_rounding = 6.0 style.window_rounding = 6.0
style.child_rounding = 4.0 style.child_rounding = 4.0
style.frame_rounding = 4.0 style.frame_rounding = 4.0
@@ -225,6 +302,17 @@ def apply(palette_name: str) -> None:
style.frame_border_size = 1.0 style.frame_border_size = 1.0
style.popup_border_size = 1.0 style.popup_border_size = 1.0
# Apply transparency to WindowBg
win_bg = style.color_(imgui.Col_.window_bg)
win_bg.w = _transparency
style.set_color_(imgui.Col_.window_bg, win_bg)
# Apply child/frame transparency
for col_idx in [imgui.Col_.child_bg, imgui.Col_.frame_bg, imgui.Col_.popup_bg]:
c = style.color_(col_idx)
c.w = _child_transparency
style.set_color_(col_idx, c)
# Spacing & Padding # Spacing & Padding
style.window_padding = imgui.ImVec2(8.0, 8.0) style.window_padding = imgui.ImVec2(8.0, 8.0)
style.frame_padding = imgui.ImVec2(8.0, 4.0) style.frame_padding = imgui.ImVec2(8.0, 4.0)
@@ -237,16 +325,6 @@ def apply(palette_name: str) -> None:
style.anti_aliased_fill = True style.anti_aliased_fill = True
style.anti_aliased_lines_use_tex = True style.anti_aliased_lines_use_tex = True
if not colours:
# Reset to imgui dark defaults
imgui.style_colors_dark()
return
# Start from dark defaults so unlisted keys have sensible values
imgui.style_colors_dark()
for col_enum, rgba in colours.items():
style.set_color_(col_enum, imgui.ImVec4(*rgba))
def set_scale(factor: float) -> None: def set_scale(factor: float) -> None:
"""Set the global font/UI scale factor.""" """Set the global font/UI scale factor."""
global _current_scale global _current_scale
@@ -261,17 +339,22 @@ def save_to_config(config: dict) -> None:
config["theme"]["font_path"] = _current_font_path config["theme"]["font_path"] = _current_font_path
config["theme"]["font_size"] = _current_font_size config["theme"]["font_size"] = _current_font_size
config["theme"]["scale"] = _current_scale config["theme"]["scale"] = _current_scale
config["theme"]["transparency"] = _transparency
config["theme"]["child_transparency"] = _child_transparency
def load_from_config(config: dict) -> None: def load_from_config(config: dict) -> None:
"""Read [theme] from config and apply palette + scale. Font is handled separately at startup.""" """Read [theme] from config. Font is handled separately at startup."""
global _current_font_path, _current_font_size, _current_scale, _current_palette global _current_font_path, _current_font_size, _current_scale, _current_palette, _transparency, _child_transparency
t = config.get("theme", {}) t = config.get("theme", {})
_current_palette = t.get("palette", "ImGui Dark") _current_palette = t.get("palette", "10x Dark")
if _current_palette in ("", "DPG Default"):
_current_palette = "10x Dark"
_current_font_path = t.get("font_path", "fonts/Inter-Regular.ttf") _current_font_path = t.get("font_path", "fonts/Inter-Regular.ttf")
_current_font_size = float(t.get("font_size", 16.0)) _current_font_size = float(t.get("font_size", 16.0))
_current_scale = float(t.get("scale", 1.0)) _current_scale = float(t.get("scale", 1.0))
# Don't apply here — imgui context may not exist yet. _transparency = float(t.get("transparency", 1.0))
# Call apply_current() after imgui is initialised. _child_transparency = float(t.get("child_transparency", 1.0))
def apply_current() -> None: def apply_current() -> None:
"""Apply the loaded palette and scale. Call after imgui context exists.""" """Apply the loaded palette and scale. Call after imgui context exists."""
@@ -281,3 +364,15 @@ def apply_current() -> None:
def get_font_loading_params() -> tuple[str, float]: def get_font_loading_params() -> tuple[str, float]:
"""Return (font_path, font_size) for use during hello_imgui font loading callback.""" """Return (font_path, font_size) for use during hello_imgui font loading callback."""
return _current_font_path, _current_font_size return _current_font_path, _current_font_size
def get_tweaked_theme() -> hello_imgui.ImGuiTweakedTheme:
"""Returns an ImGuiTweakedTheme object reflecting the current state."""
tt = hello_imgui.ImGuiTweakedTheme()
if hasattr(hello_imgui.ImGuiTheme_, _current_palette):
tt.theme = getattr(hello_imgui.ImGuiTheme_, _current_palette)
else:
tt.theme = hello_imgui.ImGuiTheme_.imgui_colors_dark
# Sync tweaks
tt.tweaks.rounding = 6.0
return tt
+8
View File
@@ -216,6 +216,14 @@ def live_gui() -> Generator[tuple[subprocess.Popen, str], None, None]:
if layout_file.exists(): if layout_file.exists():
shutil.copy2(layout_file, temp_workspace / layout_file.name) shutil.copy2(layout_file, temp_workspace / layout_file.name)
# Link assets for fonts
src_assets = project_root / "assets"
if src_assets.exists():
if os.name == 'nt':
subprocess.run(["cmd", "/c", "mklink", "/D", str(temp_workspace / "assets"), str(src_assets)], check=False)
else:
os.symlink(src_assets, temp_workspace / "assets")
# Check if already running (shouldn't be) # Check if already running (shouldn't be)
try: try:
resp = requests.get("http://127.0.0.1:8999/status", timeout=0.5) resp = requests.get("http://127.0.0.1:8999/status", timeout=0.5)
+1 -1
View File
@@ -90,7 +90,7 @@ def test_track_discussion_toggle(mock_app: App):
mock_imgui.selectable.return_value = (False, False) mock_imgui.selectable.return_value = (False, False)
mock_imgui.button.return_value = False mock_imgui.button.return_value = False
mock_imgui.collapsing_header.return_value = True # For Discussions header mock_imgui.collapsing_header.return_value = True # For Discussions header
mock_imgui.input_text.side_effect = lambda label, value, **kwargs: (False, value) mock_imgui.input_text.side_effect = lambda label, value, *args, **kwargs: (False, value)
mock_imgui.input_int.side_effect = lambda label, value, *args, **kwargs: (False, value) mock_imgui.input_int.side_effect = lambda label, value, *args, **kwargs: (False, value)
mock_imgui.begin_child.return_value = True mock_imgui.begin_child.return_value = True
# Mock clipper to avoid the while loop hang # Mock clipper to avoid the while loop hang
+2
View File
@@ -39,6 +39,8 @@ def test_render_mma_dashboard_progress():
Ticket(id='T4', description='desc', status='todo') Ticket(id='T4', description='desc', status='todo')
] ]
app.is_viewing_prior_session = False
app.perf_profiling_enabled = False
app.mma_tier_usage = {} app.mma_tier_usage = {}
app.mma_status = "idle" app.mma_status = "idle"
app.active_tier = None app.active_tier = None
+1
View File
@@ -35,6 +35,7 @@ def _make_app(**kwargs):
app.ui_new_ticket_deps = "" app.ui_new_ticket_deps = ""
app.ui_new_ticket_deps = "" app.ui_new_ticket_deps = ""
app.ui_selected_ticket_id = "" app.ui_selected_ticket_id = ""
app.is_viewing_prior_session = False
mock_engine = MagicMock() mock_engine = MagicMock()
mock_engine._pause_event = MagicMock() mock_engine._pause_event = MagicMock()
mock_engine._pause_event.is_set.return_value = False mock_engine._pause_event.is_set.return_value = False
+3 -2
View File
@@ -34,6 +34,7 @@ def _make_app(**kwargs):
app.ui_new_ticket_target = "" app.ui_new_ticket_target = ""
app.ui_new_ticket_deps = "" app.ui_new_ticket_deps = ""
app._tier_stream_last_len = {} app._tier_stream_last_len = {}
app.is_viewing_prior_session = False
mock_engine = MagicMock() mock_engine = MagicMock()
mock_engine._pause_event = MagicMock() mock_engine._pause_event = MagicMock()
mock_engine._pause_event.is_set.return_value = False mock_engine._pause_event.is_set.return_value = False
@@ -65,8 +66,8 @@ class TestMMADashboardStreams:
imgui_mock.begin_child.return_value = True imgui_mock.begin_child.return_value = True
with patch("src.gui_2.imgui", imgui_mock): with patch("src.gui_2.imgui", imgui_mock):
App._render_tier_stream_panel(app, "Tier 1", "Tier 1") App._render_tier_stream_panel(app, "Tier 1", "Tier 1")
text_wrapped_args = " ".join(str(c) for c in imgui_mock.text_wrapped.call_args_list)
assert "hello" in text_wrapped_args, "text_wrapped not called with stream content 'hello'" app._render_selectable_label.assert_called_with('stream_Tier 1', 'hello', width=-1, multiline=True, height=0)
def test_tier3_renders_worker_subheaders(self): def test_tier3_renders_worker_subheaders(self):
"""_render_tier_stream_panel for Tier 3 must render a sub-header for each worker stream key.""" """_render_tier_stream_panel for Tier 3 must render a sub-header for each worker stream key."""