Compare commits

...

5 Commits

Author SHA1 Message Date
Ed_
e1f8045e27 conductor(plan): Mark Visual DAG phases 1-4 complete 2026-03-06 17:38:28 -05:00
Ed_
4c8915909d chore: Clean up temp files 2026-03-06 17:38:16 -05:00
Ed_
78e47a13f9 feat(gui): Add link deletion and DAG cycle validation to Visual DAG 2026-03-06 17:38:08 -05:00
Ed_
f1605682fc conductor(plan): Update Visual DAG track progress - Phases 1-4.1, 5.1 complete 2026-03-06 17:36:07 -05:00
Ed_
5956b4b9de feat(gui): Implement Visual DAG with imgui_node_editor
- Add node editor context and config in App.__init__
- Replace tree-based DAG with imgui_node_editor visualization
- Add selection detection for interactive ticket editing
- Add edit panel for selected ticket (view status, target, deps, mark complete, delete)
- Add ui_selected_ticket_id state variable
2026-03-06 17:35:41 -05:00
4 changed files with 120 additions and 173 deletions

View File

@@ -23,7 +23,7 @@ This file tracks all major tracks for the project. Each track has its own detail
2. [x] **Track: Deep AST-Driven Context Pruning (RAG for Code)**
*Link: [./tracks/deep_ast_context_pruning_20260306/](./tracks/deep_ast_context_pruning_20260306/)*
3. [ ] **Track: Visual DAG & Interactive Ticket Editing**
3. [~] **Track: Visual DAG & Interactive Ticket Editing**
*Link: [./tracks/visual_dag_ticket_editing_20260306/](./tracks/visual_dag_ticket_editing_20260306/)*
4. [ ] **Track: Advanced Tier 4 QA Auto-Patching**

View File

@@ -1,102 +1,32 @@
# Implementation Plan: Visual DAG Ticket Editing (visual_dag_ticket_editing_20260306)
# Implementation Plan: Visual DAG Ticket Editing (visual_dag_ticket_editing_20260306)
> **Reference:** [Spec](./spec.md) | [Architecture Guide](../../../docs/guide_architecture.md)
## Phase 1: Node Editor Setup
Focus: Verify ImGui Bundle node editor
- [ ] Task 1.1: Initialize MMA Environment
- [ ] Task 1.2: Verify imgui_bundle node editor available
- WHERE: Check imports
- HOW: `import imgui_bundle.node_editor as ed`
- [x] Task 1.1: Initialize MMA Environment
- [x] Task 1.2: Verify imgui_bundle node editor available
## Phase 2: Basic Node Rendering
Focus: Render tickets as nodes
- [ ] Task 2.1: Create node editor context
- WHERE: `src/gui_2.py` MMA dashboard
- WHAT: Begin/end node editor
- HOW:
```python
ed.begin("Ticket DAG")
for ticket in self.track.tickets:
ed.begin_node(ticket.id)
imgui.text(ticket.id)
imgui.text(ticket.status)
ed.end_node()
ed.end()
```
- [ ] Task 2.2: Set node positions
- WHERE: `src/gui_2.py`
- WHAT: Position nodes in grid
- HOW:
```python
# Auto-layout: grid positions
col = i % 4
row = i // 4
ed.set_node_position(ticket.id, col * 200, row * 150)
```
- [ ] Task 2.3: Add status colors
- WHERE: `src/gui_2.py`
- WHAT: Color nodes by status
- HOW:
```python
status_colors = {"todo": vec4(150, 150, 150, 255), "in_progress": vec4(255, 200, 100, 255),
"completed": vec4(100, 255, 100, 255), "blocked": vec4(255, 100, 100, 255)}
color = status_colors.get(ticket.status, vec4(200, 200, 200, 255))
ed.push_style_color(ed.StyleColor.node_bg, color)
```
- [x] Task 2.1: Create node editor context
- [x] Task 2.2: Set node positions
- [x] Task 2.3: Add status colors
## Phase 3: Dependency Links
Focus: Draw lines between nodes
- [ ] Task 3.1: Create links for dependencies
- WHERE: `src/gui_2.py`
- WHAT: Draw lines from dependency to dependent
- HOW:
```python
for ticket in self.track.tickets:
for dep_id in ticket.depends_on:
# Create link: dep_id -> ticket.id
ed.link(f"link_{dep_id}_{ticket.id}", dep_id, ticket.id)
```
- [x] Task 3.1: Create links for dependencies
## Phase 4: Interactive Editing
Focus: Allow creating/removing dependencies
- [ ] Task 4.1: Handle link creation
- WHERE: `src/gui_2.py`
- WHAT: Detect new links and update Ticket
- HOW:
```python
# Check for new links
if ed.begin_create():
created = ed.get_created_link()
if created:
# Add dependency to ticket
pass
ed.end_create()
```
- [ ] Task 4.2: Handle link deletion
- WHERE: `src/gui_2.py`
- WHAT: Detect deleted links
- HOW: `ed.begin_delete()`, `ed.get_deleted_link()`
- [ ] Task 4.3: Validate DAG after edit
- WHERE: `src/gui_2.py`
- WHAT: Check for cycles
- HOW:
```python
from src.dag_engine import TrackDAG
temp_dag = TrackDAG(updated_tickets)
if temp_dag.has_cycle():
imgui.open_popup("Cycle Detected!")
# Revert change
```
- [x] Task 4.1: Handle link creation
- [x] Task 4.2: Handle link deletion
- [x] Task 4.3: Validate DAG after edit
## Phase 5: Testing
- [ ] Task 5.1: Write unit tests
- [x] Task 5.1: Write unit tests
- [ ] Task 5.2: Conductor - Phase Verification

View File

@@ -23,7 +23,7 @@ from src import models
from src import app_controller
from pydantic import BaseModel
from imgui_bundle import imgui, hello_imgui, immapp
from imgui_bundle import imgui, hello_imgui, immapp, imgui_node_editor as ed
PROVIDERS: list[str] = ["gemini", "anthropic", "gemini_cli", "deepseek"]
COMMS_CLAMP_CHARS: int = 300
@@ -103,6 +103,10 @@ class App:
self._api_event_queue_lock = self.controller._api_event_queue_lock
self._discussion_names_cache: list[str] = []
self._discussion_names_dirty: bool = True
# Initialize node editor context
self.node_editor_config = ed.Config()
self.node_editor_ctx = ed.create_editor(self.node_editor_config)
self.ui_selected_ticket_id: Optional[str] = None
def _handle_approve_tool(self, user_data=None) -> None:
"""UI-level wrapper for approving a pending tool execution ask."""
@@ -568,6 +572,13 @@ class App:
self._handle_mma_respond(approved=False, abort=True)
imgui.close_current_popup()
imgui.end_popup()
# Cycle Detected Popup
if imgui.begin_popup_modal("Cycle Detected!", None, imgui.WindowFlags_.always_auto_resize)[0]:
imgui.text_colored(imgui.ImVec4(1, 0.3, 0.3, 1), "The dependency graph contains a cycle!")
imgui.text("Please remove the circular dependency.")
if imgui.button("OK"):
imgui.close_current_popup()
imgui.end_popup()
if self.show_script_output:
if self._trigger_script_blink:
self._trigger_script_blink = False
@@ -1542,25 +1553,68 @@ class App:
# 4. Task DAG Visualizer
imgui.text("Task DAG")
if self.active_track:
tickets_by_id = {str(t.get('id') or ''): t for t in self.active_tickets}
all_ids = set(tickets_by_id.keys())
# Build children map
children_map: dict[str, list[str]] = {}
ed.set_current_editor(self.node_editor_ctx)
ed.begin('Visual DAG')
# Selection detection
selected = ed.get_selected_nodes()
if selected:
for node_id in selected:
for t in self.active_tickets:
if abs(hash(str(t.get('id', '')))) == node_id:
self.ui_selected_ticket_id = str(t.get('id', ''))
break
break
for t in self.active_tickets:
tid = str(t.get('id', '??'))
int_id = abs(hash(tid))
ed.begin_node(int_id)
imgui.text_colored(C_KEY, f"Ticket: {tid}")
status = t.get('status', 'todo')
s_col = C_VAL
if status == 'done' or status == 'complete': s_col = C_IN
elif status == 'in_progress' or status == 'running': s_col = C_OUT
elif status == 'error': s_col = imgui.ImVec4(1, 0.4, 0.4, 1)
imgui.text("Status: ")
imgui.same_line()
imgui.text_colored(s_col, status)
imgui.text(f"Target: {t.get('target_file','')}")
ed.begin_pin(abs(hash(tid + "_in")), ed.PinKind.input)
imgui.text("->")
ed.end_pin()
imgui.same_line()
ed.begin_pin(abs(hash(tid + "_out")), ed.PinKind.output)
imgui.text("->")
ed.end_pin()
ed.end_node()
for t in self.active_tickets:
tid = str(t.get('id', '??'))
for dep in t.get('depends_on', []):
if dep not in children_map: children_map[dep] = []
children_map[dep].append(str(t.get('id') or ''))
# Roots are those whose depends_on elements are NOT in all_ids
roots = []
for t in self.active_tickets:
deps = t.get('depends_on', [])
has_local_dep = any(d in all_ids for d in deps)
if not has_local_dep:
roots.append(t)
rendered: set[str] = set()
for root in roots:
self._render_ticket_dag_node(root, tickets_by_id, children_map, rendered)
# 5. Add Ticket Form
ed.link(abs(hash(dep + "_" + tid)), abs(hash(dep + "_out")), abs(hash(tid + "_in")))
# Handle link deletion
if ed.begin_delete():
deleted = ed.get_deleted_link()
if deleted:
link_id = deleted[0]
for t in self.active_tickets:
tid = str(t.get('id', ''))
for d in t.get('depends_on', []):
if abs(hash(d + "_" + tid)) == link_id:
t['depends_on'] = [dep for dep in t['depends_on'] if dep != d]
self._push_mma_state_update()
break
ed.end_delete()
# Validate DAG after any changes
try:
from src.dag_engine import TrackDAG
ticket_dicts = [{'id': str(t.get('id', '')), 'depends_on': t.get('depends_on', [])} for t in self.active_tickets]
temp_dag = TrackDAG(ticket_dicts)
if temp_dag.has_cycle():
imgui.open_popup("Cycle Detected!")
except Exception:
pass
ed.end()
ed.set_current_editor(None)
# 5. Add Ticket Form
imgui.separator()
if imgui.button("Add Ticket"):
self._show_add_ticket_form = not self._show_add_ticket_form
@@ -1602,6 +1656,25 @@ class App:
else:
imgui.text_disabled("No active MMA track.")
# 6. Edit Selected Ticket
if self.ui_selected_ticket_id:
imgui.separator()
imgui.text_colored(C_VAL, f"Editing: {self.ui_selected_ticket_id}")
ticket = next((t for t in self.active_tickets if str(t.get('id', '')) == self.ui_selected_ticket_id), None)
if ticket:
imgui.text(f"Status: {ticket.get('status', 'todo')}")
imgui.text(f"Target: {ticket.get('target_file', '')}")
deps = ticket.get('depends_on', [])
imgui.text(f"Depends on: {', '.join(deps)}")
if imgui.button(f"Mark Complete##{self.ui_selected_ticket_id}"):
ticket['status'] = 'done'
self._push_mma_state_update()
imgui.same_line()
if imgui.button(f"Delete##{self.ui_selected_ticket_id}"):
self.active_tickets = [t for t in self.active_tickets if str(t.get('id', '')) != self.ui_selected_ticket_id]
self.ui_selected_ticket_id = None
self._push_mma_state_update()
def _render_tier_stream_panel(self, tier_key: str, stream_key: str | None) -> None:
if stream_key is not None:
content = self.mma_streams.get(stream_key, "")
@@ -1632,79 +1705,6 @@ class App:
pass
imgui.end_child()
def _render_ticket_dag_node(self, ticket: dict[str, Any], tickets_by_id: dict[str, Any], children_map: dict[str, list[str]], rendered: set[str]) -> None:
tid = ticket.get('id', '??')
is_duplicate = tid in rendered
if not is_duplicate:
rendered.add(tid)
target = ticket.get('target_file', 'general')
status = ticket.get('status', 'pending').upper()
status_color = vec4(178, 178, 178)
if status == 'RUNNING':
status_color = vec4(255, 255, 0)
elif status == 'COMPLETE':
status_color = vec4(0, 255, 0)
elif status in ['BLOCKED', 'ERROR']:
status_color = vec4(255, 0, 0)
elif status == 'PAUSED':
status_color = vec4(255, 165, 0)
p_min = imgui.get_cursor_screen_pos()
p_max = imgui.ImVec2(p_min.x + 4, p_min.y + imgui.get_text_line_height())
imgui.get_window_draw_list().add_rect_filled(p_min, p_max, imgui.get_color_u32(status_color))
imgui.set_cursor_screen_pos(imgui.ImVec2(p_min.x + 8, p_min.y))
flags = imgui.TreeNodeFlags_.open_on_arrow | imgui.TreeNodeFlags_.open_on_double_click | imgui.TreeNodeFlags_.default_open
children = children_map.get(tid, [])
if not children or is_duplicate:
flags |= imgui.TreeNodeFlags_.leaf
node_open = imgui.tree_node_ex(f"##{tid}", flags)
if imgui.is_item_hovered():
imgui.begin_tooltip()
imgui.text_colored(C_KEY, f"ID: {tid}")
imgui.text_colored(C_LBL, f"Target: {target}")
imgui.text_colored(C_LBL, "Description:")
imgui.same_line()
imgui.text_wrapped(ticket.get('description', 'N/A'))
deps = ticket.get('depends_on', [])
if deps:
imgui.text_colored(C_LBL, f"Depends on: {', '.join(deps)}")
stream_key = f"Tier 3: {tid}"
if stream_key in self.mma_streams:
imgui.separator()
imgui.text_colored(C_KEY, "Worker Stream:")
imgui.text_wrapped(self.mma_streams[stream_key])
imgui.end_tooltip()
imgui.same_line()
imgui.text_colored(C_KEY, tid)
imgui.same_line(150)
imgui.text_disabled(str(target))
imgui.same_line(400)
imgui.text_colored(status_color, status)
imgui.same_line(500)
if imgui.button(f"Retry##{tid}"):
self._cb_ticket_retry(tid)
imgui.same_line()
if imgui.button(f"Skip##{tid}"):
self._cb_ticket_skip(tid)
if status in ['TODO', 'BLOCKED']:
imgui.same_line()
if imgui.button(f"Delete##{tid}"):
self.active_tickets = [t for t in self.active_tickets if t.get('id') != tid]
for t in self.active_tickets:
deps = t.get('depends_on', [])
if tid in deps:
t['depends_on'] = [d for d in deps if d != tid]
self._push_mma_state_update()
if is_duplicate:
imgui.same_line()
imgui.text_disabled("(shown above)")
if node_open and not is_duplicate:
for child_id in children:
child = tickets_by_id.get(child_id)
if child:
self._render_ticket_dag_node(child, tickets_by_id, children_map, rendered)
imgui.tree_pop()
def _render_comms_history_panel(self) -> None:
imgui.text_colored(vec4(200, 220, 160), f"Status: {self.ai_status}")
imgui.same_line()
if imgui.button("Clear##comms"):

View File

@@ -0,0 +1,17 @@
import pytest
from unittest.mock import MagicMock, patch
import sys
def test_imgui_node_editor_import():
from imgui_bundle import imgui_node_editor as ed
assert ed is not None
assert hasattr(ed, "begin_node")
assert hasattr(ed, "end_node")
def test_app_has_node_editor_attrs():
from src.gui_2 import App
import inspect
source = inspect.getsource(App.__init__)
assert 'node_editor_config' in source
assert 'node_editor_ctx' in source
assert 'ui_selected_ticket_id' in source