Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5446a2407c | |||
| fde0f29e72 | |||
| bfbcfcc2af | |||
| 502a47fd92 | |||
| 5f0168c4f2 | |||
| e802c6675f |
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.
@@ -7,7 +7,8 @@
|
||||
## UX & UI Principles
|
||||
|
||||
- **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.
|
||||
- **Multi-Viewport Capabilities:** Leverage dockable, floatable panels to allow users to build custom workspaces suitable for multi-monitor setups.
|
||||
|
||||
|
||||
@@ -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/`.
|
||||
- **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.
|
||||
- **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).
|
||||
- **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.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
## GUI Frameworks
|
||||
|
||||
- **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
|
||||
|
||||
@@ -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 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).
|
||||
- **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.
|
||||
|
||||
|
||||
+2
-2
@@ -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/)*
|
||||
*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/)*
|
||||
*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/)*
|
||||
*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/)*
|
||||
*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
|
||||
|
||||
## Phase 1: Markdown Integration & Setup
|
||||
- [ ] Task: Research and configure `imgui_markdown` within the existing `imgui-bundle` environment.
|
||||
- [ ] 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.).
|
||||
- [ ] Task: Implement basic Markdown rendering in a test panel.
|
||||
- [ ] 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: Research and configure `imgui_markdown` within the existing `imgui-bundle` environment.
|
||||
- [x] Identify required font assets for Markdown (bold, italic, headers).
|
||||
- [x] Create a `MarkdownRenderer` wrapper class in `src/markdown_helper.py` to manage styling and callbacks (links, etc.).
|
||||
- [x] Task: Implement basic Markdown rendering in a test panel.
|
||||
- [x] Verify that bold, italic, and headers render correctly using the defined theme fonts.
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 1: Markdown Integration' (Protocol in workflow.md)
|
||||
|
||||
## Phase 2: Syntax Highlighting Implementation
|
||||
- [ ] 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).
|
||||
- [ ] Define language-specific color palettes that match the "Professional" theme.
|
||||
- [ ] Task: Implement the language resolution logic.
|
||||
- [ ] 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 $`, `{ "`).
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Syntax Highlighting' (Protocol in workflow.md)
|
||||
- [x] Task: Implement syntax highlighting for PowerShell, Python, and JSON/TOML.
|
||||
- [x] Research `imgui-bundle`'s recommended approach for syntax highlighting (e.g., using `ImGuiColorTextEdit` or specialized Markdown callbacks).
|
||||
- [x] Define language-specific color palettes that match the "Professional" theme.
|
||||
- [x] Task: Implement the language resolution logic.
|
||||
- [x] Create a utility to extract language tags from code blocks and resolve file extensions.
|
||||
- [x] Implement cheap heuristic for common code patterns (e.g., matching `def `, `if $`, `{ "`).
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 2: Syntax Highlighting' (Protocol in workflow.md)
|
||||
|
||||
## Phase 3: GUI Integration (Read-Only Views)
|
||||
- [ ] Task: Integrate Markdown rendering into the Discussion History.
|
||||
- [ ] Replace `imgui.text_wrapped` in `_render_discussion_panel` with the `MarkdownRenderer`.
|
||||
- [ ] Ensure that code blocks within AI messages are correctly highlighted.
|
||||
- [ ] Task: Integrate syntax highlighting into the Comms Log.
|
||||
- [ ] Update `_render_comms_history_panel` to render JSON/TOML payloads with highlighting.
|
||||
- [ ] Task: Integrate syntax highlighting into the Operations/Tooling panels.
|
||||
- [ ] 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: Integrate Markdown rendering into the Discussion History.
|
||||
- [x] Replace `imgui.text_wrapped` in `_render_discussion_panel` with the `MarkdownRenderer`.
|
||||
- [x] Ensure that code blocks within AI messages are correctly highlighted.
|
||||
- [x] Task: Integrate syntax highlighting into the Comms Log.
|
||||
- [x] Update `_render_comms_history_panel` to render JSON/TOML payloads with highlighting.
|
||||
- [x] Task: Integrate syntax highlighting into the Operations/Tooling panels.
|
||||
- [x] Ensure PowerShell scripts and tool results are rendered with highlighting.
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 3: GUI Integration' (Protocol in workflow.md)
|
||||
|
||||
## Phase 4: Refinement & Final Polish
|
||||
- [ ] Task: Refine performance for large logs.
|
||||
- [ ] Implement incremental rendering or caching for rendered Markdown blocks to maintain high FPS.
|
||||
- [ ] Task: Implement clickable links.
|
||||
- [ ] 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.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Final Polish' (Protocol in workflow.md)
|
||||
- [x] Task: Refine performance for large logs.
|
||||
- [x] Implement incremental rendering or caching for rendered Markdown blocks to maintain high FPS. (Hybrid renderer with TextEditor caching implemented).
|
||||
- [x] Task: Implement clickable links.
|
||||
- [x] Handle link callbacks to open external URLs in the browser or local files in the configured text editor.
|
||||
- [x] Task: Conduct a final visual audit across all read-only views.
|
||||
- [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: 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] Add the "Multi-Viewport" toggle checkbox to the main menu bar.
|
||||
- [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] Persist layout presets to the project configuration (`manual_slop.toml` or a dedicated file).
|
||||
- [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
|
||||
- [ ] 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.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 5: Final Polish & Verification' (Protocol in workflow.md)
|
||||
## Phase 5: Final Polish & Verification [checkpoint: 5efd775]
|
||||
- [x] Task: Conduct a final UI audit for "professionalism" and consistency.
|
||||
- [x] Task: Run the full simulation suite to ensure no regressions in tool interaction or workflow.
|
||||
- [x] Task: Conductor - User Manual Verification 'Phase 5: Final Polish & Verification' (Protocol in workflow.md)
|
||||
|
||||
+12
-6
@@ -1,6 +1,6 @@
|
||||
[ai]
|
||||
provider = "minimax"
|
||||
model = "MiniMax-M2.5"
|
||||
provider = "deepseek"
|
||||
model = "deepseek-chat"
|
||||
temperature = 0.0
|
||||
max_tokens = 24000
|
||||
history_trunc_limit = 900000
|
||||
@@ -9,6 +9,11 @@ system_prompt = ""
|
||||
[projects]
|
||||
paths = [
|
||||
"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"
|
||||
|
||||
@@ -16,6 +21,7 @@ active = "C:/projects/gencpp/gencpp_sloppy.toml"
|
||||
separate_message_panel = false
|
||||
separate_response_panel = false
|
||||
separate_tool_calls_panel = false
|
||||
bg_shader_enabled = true
|
||||
|
||||
[gui.show_windows]
|
||||
"Context Hub" = true
|
||||
@@ -28,12 +34,12 @@ separate_tool_calls_panel = false
|
||||
"Tier 4: QA" = true
|
||||
"Discussion Hub" = true
|
||||
"Operations Hub" = true
|
||||
Message = true
|
||||
Response = true
|
||||
"Tool Calls" = true
|
||||
Message = false
|
||||
Response = false
|
||||
"Tool Calls" = false
|
||||
Theme = true
|
||||
"Log Management" = true
|
||||
Diagnostics = true
|
||||
Diagnostics = false
|
||||
|
||||
[theme]
|
||||
palette = "DPG Default"
|
||||
|
||||
@@ -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")
|
||||
@@ -13,9 +13,27 @@ try:
|
||||
with urllib.request.urlopen(req) as response:
|
||||
with zipfile.ZipFile(io.BytesIO(response.read())) as z:
|
||||
for info in z.infolist():
|
||||
if info.filename.endswith("Inter-Regular.ttf") or info.filename.endswith("Inter-Bold.ttf"):
|
||||
info.filename = os.path.basename(info.filename)
|
||||
targets = ["Inter-Regular.ttf", "Inter-Bold.ttf", "Inter-Italic.ttf", "Inter-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 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}")
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
@@ -324,7 +324,8 @@ class AppController:
|
||||
'manual_approve': 'ui_manual_approve',
|
||||
'inject_file_path': '_inject_file_path',
|
||||
'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.update({
|
||||
@@ -346,7 +347,8 @@ class AppController:
|
||||
'_inject_file_path': '_inject_file_path',
|
||||
'_inject_mode': '_inject_mode',
|
||||
'_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_profiling_enabled = False
|
||||
@@ -783,6 +785,11 @@ class AppController:
|
||||
self.ui_summary_only = proj_meta.get("summary_only", False)
|
||||
self.ui_auto_add_history = disc_sec.get("auto_add", False)
|
||||
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 = {
|
||||
"Context Hub": True,
|
||||
"Files & Media": True,
|
||||
@@ -2077,12 +2084,15 @@ class AppController:
|
||||
}
|
||||
self.config["ai"]["system_prompt"] = self.ui_global_system_prompt
|
||||
self.config["projects"] = {"paths": self.project_paths, "active": self.active_project_path}
|
||||
from src import bg_shader
|
||||
self.config["gui"] = {
|
||||
"show_windows": self.show_windows,
|
||||
"separate_message_panel": getattr(self, "ui_separate_message_panel", False),
|
||||
"separate_response_panel": getattr(self, "ui_separate_response_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)
|
||||
|
||||
def _do_generate(self) -> tuple[str, Path, list[dict[str, Any]], str, str]:
|
||||
|
||||
@@ -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
@@ -22,6 +22,8 @@ from src import log_pruner
|
||||
from src import models
|
||||
from src import app_controller
|
||||
from src import mcp_client
|
||||
from src import markdown_helper
|
||||
from src import bg_shader
|
||||
import re
|
||||
|
||||
from pydantic import BaseModel
|
||||
@@ -201,10 +203,10 @@ class App:
|
||||
self.text_viewer_title = label
|
||||
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.same_line()
|
||||
if imgui.button("[+]##" + label):
|
||||
if imgui.button("[+]##" + label + id_suffix):
|
||||
self.show_text_viewer = True
|
||||
self.text_viewer_title = label
|
||||
self.text_viewer_content = content
|
||||
@@ -213,13 +215,21 @@ class App:
|
||||
imgui.text_disabled("(empty)")
|
||||
return
|
||||
|
||||
is_md = label in ("message", "text", "content")
|
||||
ctx_id = f"heavy_{label}_{id_suffix}"
|
||||
|
||||
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}_{hash(content)}", imgui.ImVec2(0, 80), True)
|
||||
self._render_selectable_label(f'heavy_val_{label}_{hash(content)}', content, width=-1, multiline=True, height=80)
|
||||
imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 80), True)
|
||||
if is_md:
|
||||
markdown_helper.render(content, context_id=ctx_id)
|
||||
else:
|
||||
markdown_helper.render_code(content, context_id=ctx_id)
|
||||
imgui.end_child()
|
||||
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
|
||||
|
||||
|
||||
@@ -278,6 +288,12 @@ class App:
|
||||
imgui.end_menu()
|
||||
|
||||
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
|
||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_gui_func")
|
||||
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")
|
||||
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:
|
||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_files_panel")
|
||||
@@ -1356,7 +1395,7 @@ class App:
|
||||
if len(content) > 80: preview += "..."
|
||||
imgui.text_colored(vec4(180, 180, 180), preview)
|
||||
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.pop_id()
|
||||
@@ -1515,14 +1554,14 @@ class App:
|
||||
pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?")
|
||||
matches = list(pattern.finditer(content))
|
||||
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:
|
||||
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)
|
||||
last_idx = 0
|
||||
for m_idx, match in enumerate(matches):
|
||||
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()
|
||||
path = match.group(2)
|
||||
code_block = match.group(4)
|
||||
@@ -1534,16 +1573,11 @@ class App:
|
||||
self.text_viewer_content = res
|
||||
self.show_text_viewer = True
|
||||
if code_block:
|
||||
code_content = code_block.strip()
|
||||
if code_content.count("\n") + 1 > 50:
|
||||
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)
|
||||
# Render code block with highlighting
|
||||
markdown_helper.render(code_block, context_id=f'disc_{i}_c_{m_idx}')
|
||||
last_idx = match.end()
|
||||
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()
|
||||
imgui.end_child()
|
||||
else:
|
||||
@@ -1861,7 +1895,7 @@ class App:
|
||||
# --- Always Render Content ---
|
||||
|
||||
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.separator()
|
||||
@@ -1950,24 +1984,25 @@ class App:
|
||||
imgui.text_colored(C_SUB, f"[{tier}]")
|
||||
|
||||
# Optimized content rendering using _render_heavy_text logic
|
||||
idx_str = str(i)
|
||||
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"):
|
||||
self._render_heavy_text("system", payload.get("system", ""))
|
||||
self._render_heavy_text("system", payload.get("system", ""), idx_str)
|
||||
elif kind == "response":
|
||||
r = payload.get("round", 0)
|
||||
sr = payload.get("stop_reason", "STOP")
|
||||
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", [])
|
||||
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":
|
||||
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":
|
||||
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:
|
||||
self._render_heavy_text("data", str(payload))
|
||||
self._render_heavy_text("data", str(payload), idx_str)
|
||||
|
||||
imgui.separator()
|
||||
imgui.pop_id()
|
||||
@@ -2154,7 +2189,7 @@ class App:
|
||||
self.bulk_block()
|
||||
# Table
|
||||
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("ID", imgui.TableColumnFlags_.width_fixed, 80)
|
||||
imgui.table_setup_column("Priority", imgui.TableColumnFlags_.width_fixed, 100)
|
||||
@@ -2752,6 +2787,8 @@ class App:
|
||||
for p in theme.get_palette_names():
|
||||
if imgui.selectable(p, p == cp)[0]:
|
||||
theme.apply(p)
|
||||
self._flush_to_config()
|
||||
models.save_config(self.config)
|
||||
imgui.end_combo()
|
||||
|
||||
imgui.separator()
|
||||
@@ -2787,7 +2824,33 @@ class App:
|
||||
imgui.separator()
|
||||
imgui.text("UI Scale (DPI)")
|
||||
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()
|
||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_theme_panel")
|
||||
|
||||
@@ -2797,12 +2860,17 @@ class App:
|
||||
if assets_dir.exists():
|
||||
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()
|
||||
|
||||
if font_path:
|
||||
# Just try loading it directly; hello_imgui will look in the assets folder
|
||||
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:
|
||||
print(f"Failed to load main font {font_path}: {e}")
|
||||
self.main_font = None
|
||||
@@ -2810,7 +2878,8 @@ class App:
|
||||
self.main_font = None
|
||||
|
||||
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:
|
||||
print(f"Failed to load mono font: {e}")
|
||||
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_geometry.size = (1680, 1200)
|
||||
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
|
||||
|
||||
# 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
|
||||
fps_cap = 60.0
|
||||
@@ -2851,14 +2926,17 @@ class App:
|
||||
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_view_themes = True
|
||||
self.runner_params.ini_folder_type = hello_imgui.IniFolderType.current_folder
|
||||
self.runner_params.ini_filename = "manualslop_layout.ini"
|
||||
self.runner_params.callbacks.show_gui = self._gui_func
|
||||
self.runner_params.callbacks.show_menus = self._show_menus
|
||||
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._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
|
||||
self.shutdown()
|
||||
session_logger.close_session()
|
||||
|
||||
@@ -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
@@ -32,7 +32,7 @@ def draw_soft_shadow(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p_max: im
|
||||
c_max,
|
||||
u32_color,
|
||||
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
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
draw_list.add_rect_filled(
|
||||
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)
|
||||
@@ -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_max.x - 1, p_max.y - 1),
|
||||
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
|
||||
)
|
||||
|
||||
+124
-29
@@ -3,12 +3,13 @@
|
||||
Theming support for manual_slop GUI — imgui-bundle port.
|
||||
|
||||
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().
|
||||
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
|
||||
|
||||
@@ -173,23 +174,68 @@ _PALETTES: dict[str, dict[int, tuple]] = {
|
||||
imgui.Col_.nav_cursor: _c(166, 226, 46),
|
||||
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
|
||||
|
||||
_current_palette: str = "ImGui Dark"
|
||||
_current_font_path: str = ""
|
||||
_current_palette: str = "10x Dark"
|
||||
_current_font_path: str = "fonts/Inter-Regular.ttf"
|
||||
_current_font_size: float = 16.0
|
||||
_current_scale: float = 1.0
|
||||
_custom_font: imgui.ImFont = None # type: ignore
|
||||
_transparency: float = 1.0
|
||||
_child_transparency: float = 1.0
|
||||
|
||||
# ------------------------------------------------------------------ public API
|
||||
|
||||
def get_palette_names() -> list[str]:
|
||||
return list(_PALETTES.keys())
|
||||
|
||||
def get_current_palette() -> str:
|
||||
return _current_palette
|
||||
|
||||
@@ -202,18 +248,49 @@ def get_current_font_size() -> float:
|
||||
def get_current_scale() -> float:
|
||||
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:
|
||||
"""
|
||||
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
|
||||
_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.child_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.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
|
||||
style.window_padding = imgui.ImVec2(8.0, 8.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_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:
|
||||
"""Set the global font/UI scale factor."""
|
||||
global _current_scale
|
||||
@@ -261,17 +339,22 @@ def save_to_config(config: dict) -> None:
|
||||
config["theme"]["font_path"] = _current_font_path
|
||||
config["theme"]["font_size"] = _current_font_size
|
||||
config["theme"]["scale"] = _current_scale
|
||||
config["theme"]["transparency"] = _transparency
|
||||
config["theme"]["child_transparency"] = _child_transparency
|
||||
|
||||
def load_from_config(config: dict) -> None:
|
||||
"""Read [theme] from config and apply palette + scale. Font is handled separately at startup."""
|
||||
global _current_font_path, _current_font_size, _current_scale, _current_palette
|
||||
"""Read [theme] from config. Font is handled separately at startup."""
|
||||
global _current_font_path, _current_font_size, _current_scale, _current_palette, _transparency, _child_transparency
|
||||
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_size = float(t.get("font_size", 16.0))
|
||||
_current_scale = float(t.get("scale", 1.0))
|
||||
# Don't apply here — imgui context may not exist yet.
|
||||
# Call apply_current() after imgui is initialised.
|
||||
_transparency = float(t.get("transparency", 1.0))
|
||||
_child_transparency = float(t.get("child_transparency", 1.0))
|
||||
|
||||
def apply_current() -> None:
|
||||
"""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]:
|
||||
"""Return (font_path, font_size) for use during hello_imgui font loading callback."""
|
||||
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
|
||||
|
||||
@@ -216,6 +216,14 @@ def live_gui() -> Generator[tuple[subprocess.Popen, str], None, None]:
|
||||
if layout_file.exists():
|
||||
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)
|
||||
try:
|
||||
resp = requests.get("http://127.0.0.1:8999/status", timeout=0.5)
|
||||
|
||||
@@ -90,7 +90,7 @@ def test_track_discussion_toggle(mock_app: App):
|
||||
mock_imgui.selectable.return_value = (False, False)
|
||||
mock_imgui.button.return_value = False
|
||||
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.begin_child.return_value = True
|
||||
# Mock clipper to avoid the while loop hang
|
||||
|
||||
@@ -39,6 +39,8 @@ def test_render_mma_dashboard_progress():
|
||||
Ticket(id='T4', description='desc', status='todo')
|
||||
]
|
||||
|
||||
app.is_viewing_prior_session = False
|
||||
app.perf_profiling_enabled = False
|
||||
app.mma_tier_usage = {}
|
||||
app.mma_status = "idle"
|
||||
app.active_tier = None
|
||||
|
||||
@@ -35,6 +35,7 @@ def _make_app(**kwargs):
|
||||
app.ui_new_ticket_deps = ""
|
||||
app.ui_new_ticket_deps = ""
|
||||
app.ui_selected_ticket_id = ""
|
||||
app.is_viewing_prior_session = False
|
||||
mock_engine = MagicMock()
|
||||
mock_engine._pause_event = MagicMock()
|
||||
mock_engine._pause_event.is_set.return_value = False
|
||||
|
||||
@@ -34,6 +34,7 @@ def _make_app(**kwargs):
|
||||
app.ui_new_ticket_target = ""
|
||||
app.ui_new_ticket_deps = ""
|
||||
app._tier_stream_last_len = {}
|
||||
app.is_viewing_prior_session = False
|
||||
mock_engine = MagicMock()
|
||||
mock_engine._pause_event = MagicMock()
|
||||
mock_engine._pause_event.is_set.return_value = False
|
||||
@@ -65,8 +66,8 @@ class TestMMADashboardStreams:
|
||||
imgui_mock.begin_child.return_value = True
|
||||
with patch("src.gui_2.imgui", imgui_mock):
|
||||
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):
|
||||
"""_render_tier_stream_panel for Tier 3 must render a sub-header for each worker stream key."""
|
||||
|
||||
Reference in New Issue
Block a user