Compare commits
5 Commits
2e44d0ea2e
...
e1f8045e27
| Author | SHA1 | Date | |
|---|---|---|---|
| e1f8045e27 | |||
| 4c8915909d | |||
| 78e47a13f9 | |||
| f1605682fc | |||
| 5956b4b9de |
@@ -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)**
|
2. [x] **Track: Deep AST-Driven Context Pruning (RAG for Code)**
|
||||||
*Link: [./tracks/deep_ast_context_pruning_20260306/](./tracks/deep_ast_context_pruning_20260306/)*
|
*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/)*
|
*Link: [./tracks/visual_dag_ticket_editing_20260306/](./tracks/visual_dag_ticket_editing_20260306/)*
|
||||||
|
|
||||||
4. [ ] **Track: Advanced Tier 4 QA Auto-Patching**
|
4. [ ] **Track: Advanced Tier 4 QA Auto-Patching**
|
||||||
|
|||||||
@@ -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)
|
> **Reference:** [Spec](./spec.md) | [Architecture Guide](../../../docs/guide_architecture.md)
|
||||||
|
|
||||||
## Phase 1: Node Editor Setup
|
## Phase 1: Node Editor Setup
|
||||||
Focus: Verify ImGui Bundle node editor
|
Focus: Verify ImGui Bundle node editor
|
||||||
|
|
||||||
- [ ] Task 1.1: Initialize MMA Environment
|
- [x] Task 1.1: Initialize MMA Environment
|
||||||
- [ ] Task 1.2: Verify imgui_bundle node editor available
|
- [x] Task 1.2: Verify imgui_bundle node editor available
|
||||||
- WHERE: Check imports
|
|
||||||
- HOW: `import imgui_bundle.node_editor as ed`
|
|
||||||
|
|
||||||
## Phase 2: Basic Node Rendering
|
## Phase 2: Basic Node Rendering
|
||||||
Focus: Render tickets as nodes
|
Focus: Render tickets as nodes
|
||||||
|
|
||||||
- [ ] Task 2.1: Create node editor context
|
- [x] Task 2.1: Create node editor context
|
||||||
- WHERE: `src/gui_2.py` MMA dashboard
|
- [x] Task 2.2: Set node positions
|
||||||
- WHAT: Begin/end node editor
|
- [x] Task 2.3: Add status colors
|
||||||
- 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)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Phase 3: Dependency Links
|
## Phase 3: Dependency Links
|
||||||
Focus: Draw lines between nodes
|
Focus: Draw lines between nodes
|
||||||
|
|
||||||
- [ ] Task 3.1: Create links for dependencies
|
- [x] 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)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Phase 4: Interactive Editing
|
## Phase 4: Interactive Editing
|
||||||
Focus: Allow creating/removing dependencies
|
Focus: Allow creating/removing dependencies
|
||||||
|
|
||||||
- [ ] Task 4.1: Handle link creation
|
- [x] Task 4.1: Handle link creation
|
||||||
- WHERE: `src/gui_2.py`
|
- [x] Task 4.2: Handle link deletion
|
||||||
- WHAT: Detect new links and update Ticket
|
- [x] Task 4.3: Validate DAG after edit
|
||||||
- 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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Phase 5: Testing
|
## Phase 5: Testing
|
||||||
- [ ] Task 5.1: Write unit tests
|
- [x] Task 5.1: Write unit tests
|
||||||
- [ ] Task 5.2: Conductor - Phase Verification
|
- [ ] Task 5.2: Conductor - Phase Verification
|
||||||
|
|||||||
182
src/gui_2.py
182
src/gui_2.py
@@ -23,7 +23,7 @@ from src import models
|
|||||||
from src import app_controller
|
from src import app_controller
|
||||||
|
|
||||||
from pydantic import BaseModel
|
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"]
|
PROVIDERS: list[str] = ["gemini", "anthropic", "gemini_cli", "deepseek"]
|
||||||
COMMS_CLAMP_CHARS: int = 300
|
COMMS_CLAMP_CHARS: int = 300
|
||||||
@@ -103,6 +103,10 @@ class App:
|
|||||||
self._api_event_queue_lock = self.controller._api_event_queue_lock
|
self._api_event_queue_lock = self.controller._api_event_queue_lock
|
||||||
self._discussion_names_cache: list[str] = []
|
self._discussion_names_cache: list[str] = []
|
||||||
self._discussion_names_dirty: bool = True
|
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:
|
def _handle_approve_tool(self, user_data=None) -> None:
|
||||||
"""UI-level wrapper for approving a pending tool execution ask."""
|
"""UI-level wrapper for approving a pending tool execution ask."""
|
||||||
@@ -568,6 +572,13 @@ class App:
|
|||||||
self._handle_mma_respond(approved=False, abort=True)
|
self._handle_mma_respond(approved=False, abort=True)
|
||||||
imgui.close_current_popup()
|
imgui.close_current_popup()
|
||||||
imgui.end_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.show_script_output:
|
||||||
if self._trigger_script_blink:
|
if self._trigger_script_blink:
|
||||||
self._trigger_script_blink = False
|
self._trigger_script_blink = False
|
||||||
@@ -1542,25 +1553,68 @@ class App:
|
|||||||
# 4. Task DAG Visualizer
|
# 4. Task DAG Visualizer
|
||||||
imgui.text("Task DAG")
|
imgui.text("Task DAG")
|
||||||
if self.active_track:
|
if self.active_track:
|
||||||
tickets_by_id = {str(t.get('id') or ''): t for t in self.active_tickets}
|
ed.set_current_editor(self.node_editor_ctx)
|
||||||
all_ids = set(tickets_by_id.keys())
|
ed.begin('Visual DAG')
|
||||||
# Build children map
|
# Selection detection
|
||||||
children_map: dict[str, list[str]] = {}
|
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:
|
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', []):
|
for dep in t.get('depends_on', []):
|
||||||
if dep not in children_map: children_map[dep] = []
|
ed.link(abs(hash(dep + "_" + tid)), abs(hash(dep + "_out")), abs(hash(tid + "_in")))
|
||||||
children_map[dep].append(str(t.get('id') or ''))
|
# Handle link deletion
|
||||||
# Roots are those whose depends_on elements are NOT in all_ids
|
if ed.begin_delete():
|
||||||
roots = []
|
deleted = ed.get_deleted_link()
|
||||||
for t in self.active_tickets:
|
if deleted:
|
||||||
deps = t.get('depends_on', [])
|
link_id = deleted[0]
|
||||||
has_local_dep = any(d in all_ids for d in deps)
|
for t in self.active_tickets:
|
||||||
if not has_local_dep:
|
tid = str(t.get('id', ''))
|
||||||
roots.append(t)
|
for d in t.get('depends_on', []):
|
||||||
rendered: set[str] = set()
|
if abs(hash(d + "_" + tid)) == link_id:
|
||||||
for root in roots:
|
t['depends_on'] = [dep for dep in t['depends_on'] if dep != d]
|
||||||
self._render_ticket_dag_node(root, tickets_by_id, children_map, rendered)
|
self._push_mma_state_update()
|
||||||
# 5. Add Ticket Form
|
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()
|
imgui.separator()
|
||||||
if imgui.button("Add Ticket"):
|
if imgui.button("Add Ticket"):
|
||||||
self._show_add_ticket_form = not self._show_add_ticket_form
|
self._show_add_ticket_form = not self._show_add_ticket_form
|
||||||
@@ -1602,6 +1656,25 @@ class App:
|
|||||||
else:
|
else:
|
||||||
imgui.text_disabled("No active MMA track.")
|
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:
|
def _render_tier_stream_panel(self, tier_key: str, stream_key: str | None) -> None:
|
||||||
if stream_key is not None:
|
if stream_key is not None:
|
||||||
content = self.mma_streams.get(stream_key, "")
|
content = self.mma_streams.get(stream_key, "")
|
||||||
@@ -1632,79 +1705,6 @@ class App:
|
|||||||
pass
|
pass
|
||||||
imgui.end_child()
|
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.text_colored(vec4(200, 220, 160), f"Status: {self.ai_status}")
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("Clear##comms"):
|
if imgui.button("Clear##comms"):
|
||||||
|
|||||||
17
tests/test_mma_node_editor.py
Normal file
17
tests/test_mma_node_editor.py
Normal 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
|
||||||
Reference in New Issue
Block a user