Private
Public Access
0
0

chore(conductor): Add new tracks for Phase 7

- Add 'structural_file_editor_20260601' track.
- Add 'discussion_metrics_and_compression_20260601' track.
This commit is contained in:
2026-06-02 00:25:10 -04:00
parent 4baaadd88d
commit 797f283f44
10 changed files with 178 additions and 105 deletions
+11 -1
View File
@@ -298,5 +298,15 @@ This file tracks all major tracks for the project. Each track has its own detail
---
- [ ] **Track: UX Refinements for Context Composition and Discussion Entries**
- [~] **Track: UX Refinements for Context Composition and Discussion Entries**
*Link: [./tracks/context_composition_ux_20260601/](./tracks/context_composition_ux_20260601/)*
---
- [ ] **Track: Combine AST Inspector and Slices Editor into a unified Structural File Editor**
*Link: [./tracks/structural_file_editor_20260601/](./tracks/structural_file_editor_20260601/)*
---
- [ ] **Track: Add per-response token metrics and AI-assisted history compression**
*Link: [./tracks/discussion_metrics_and_compression_20260601/](./tracks/discussion_metrics_and_compression_20260601/)*
@@ -0,0 +1,5 @@
# Track discussion_metrics_and_compression_20260601 Context
- [Specification](./spec.md)
- [Implementation Plan](./plan.md)
- [Metadata](./metadata.json)
@@ -0,0 +1,8 @@
{
"track_id": "discussion_metrics_and_compression_20260601",
"type": "feature",
"status": "new",
"created_at": "2026-06-01T00:00:00Z",
"updated_at": "2026-06-01T00:00:00Z",
"description": "Add per-response token metrics and AI-assisted history compression"
}
@@ -0,0 +1,19 @@
# Implementation Plan: Discussion Metrics and Compression
## Phase 1: Metrics Visibility
- [ ] Task: Update UI for Token Metrics
- [ ] Modify `_render_comms_history_panel` and `render_discussion_entry` in `src/gui_2.py` to extract and prominently display `usage` stats (input, output, cache) from the entry payloads.
## Phase 2: Compression Helper Agent
- [ ] Task: Implement Compression Agent
- [ ] Create a new agent definition or function in `src/ai_client.py` (or a dedicated module) capable of receiving a discussion history and a system prompt instructing it to summarize and compact the history.
- [ ] Task: Implement UI Triggers
- [ ] Add a "Compress Discussion" button to the Discussion Hub UI.
- [ ] Wire the button to dispatch the compression task to the background executor and display a loading indicator.
- [ ] Upon completion, replace the older entries with the generated summary block.
## Phase 3: Verification
- [ ] Task: Verification
- [ ] Verify token metrics are visible per response.
- [ ] Run the "Compress Discussion" tool on a heavy discussion and verify the history is successfully summarized without losing core context.
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Verification' (Protocol in workflow.md)
@@ -0,0 +1,21 @@
# Specification: Discussion Metrics and Compression
## 1. Overview
The user requested better visibility into token usage on a per-response and per-discussion basis, rather than just session-wide totals. Additionally, the current history truncation is deemed too naive. The user wants a smarter compression strategy utilizing a helper agent to summarize, categorize, and compact discussion entries (especially tool/vendor logs) when the token limit is approached.
## 2. Functional Requirements
* **Per-Response Metrics:** Update the Comms History and Discussion Hub panels to display token usage (input/output/cache) and remaining quota for each specific response.
* **Discussion-Level Metrics:** Add a summary of total token usage for the active discussion.
* **Helper Agent Compression:**
* Implement an asynchronous helper agent capable of analyzing a `discussion`'s history.
* The agent should categorize entries (e.g., tool calls, user prompts, AI thinking) and generate a compacted summary.
* Provide UI options to trigger this compression manually or set thresholds for automatic suggestions.
* **New Session Prompt:** Allow the AI to generate a "summary prompt" and "target prompt" to effortlessly transition a heavy, exhausted discussion into a fresh session with adjusted context.
## 3. Non-Functional Requirements
* **Performance:** The helper agent must run asynchronously to avoid blocking the main GUI thread.
## 4. Acceptance Criteria
* Token metrics are clearly visible per entry in the Comms History.
* A new "Compress Discussion" button triggers the helper agent, which successfully produces a compacted history.
* The system can transition an exhausted discussion into a new one using an AI-generated summary.
@@ -0,0 +1,5 @@
# Track structural_file_editor_20260601 Context
- [Specification](./spec.md)
- [Implementation Plan](./plan.md)
- [Metadata](./metadata.json)
@@ -0,0 +1,8 @@
{
"track_id": "structural_file_editor_20260601",
"type": "feature",
"status": "new",
"created_at": "2026-06-01T00:00:00Z",
"updated_at": "2026-06-01T00:00:00Z",
"description": "Combine AST Inspector and Slices Editor into a unified Structural File Editor"
}
@@ -0,0 +1,17 @@
# Implementation Plan: Structural File Editor
## Phase 1: Unification
- [ ] Task: Create Structural File Editor Modal
- [ ] Add `app.show_structural_editor_modal = False` to `App.__init__`.
- [ ] Create `render_structural_file_editor_modal(app: App)` in `src/gui_2.py`.
- [ ] Port the tree rendering logic from `render_ast_inspector_modal` into this new modal.
- [ ] Port the custom slice management UI from `render_slices_editor_modal` into this new modal, positioning it logically alongside or above the AST tree.
- [ ] Task: Update File Row UI
- [ ] In `render_context_files_table`, replace the separate "AST" and "Slices" buttons with a single "Structure" button that sets `app.show_structural_editor_modal = True` and sets the target file.
## Phase 2: Verification
- [ ] Task: Verification
- [ ] Open the Structural File Editor for a complex Python file.
- [ ] Verify both AST nodes and custom slices are visible and interactive.
- [ ] Verify that adding a custom slice works correctly within the unified interface.
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Verification' (Protocol in workflow.md)
@@ -0,0 +1,18 @@
# Specification: Structural File Editor
## 1. Overview
The user wants to combine the current AST Inspector and Slices Editor into a single, unified "Structural File Editor". This modal will provide a comprehensive visualization of both auto-detected AST elements and user-defined custom slices, allowing users to precisely tailor the context sent to the AI, especially in combination with "skeleton" or other filtered view modes.
## 2. Functional Requirements
* **Unified Interface:** Merge the UI elements of `render_ast_inspector_modal` and `render_slices_editor_modal` into a new `render_structural_file_editor_modal`.
* **Visualization:** Display a unified graph or list showing the file's structure. This includes AST nodes (classes, functions, etc.) and custom slices seamlessly integrated.
* **Custom Slices:** Allow users to define arbitrary text ranges as "custom slices" that exist independently of the AST graph but are visualized alongside it.
* **Filtering:** The editor must clearly indicate which parts of the file will be included based on the current `view_mode` (e.g., if `view_mode='skeleton'`, only signatures and explicitly selected custom slices/AST nodes are included).
## 3. Non-Functional Requirements
* **Performance:** The unified editor must remain responsive even for large files with complex ASTs.
## 4. Acceptance Criteria
* Clicking a "Structure" or "Edit Slices" button on a file opens the new unified modal.
* The modal clearly displays both AST elements and custom slices.
* Users can add, modify, and delete custom slices within this single interface.
+66 -104
View File
@@ -2642,87 +2642,45 @@ def render_files_and_media(app: App) -> None:
with imscope.child("Files_child", -1, split_y, True):
if not hasattr(app, 'files_last_selected'): app.files_last_selected = -1
with imscope.table("files_table", 5, imgui.TableFlags_.resizable | imgui.TableFlags_.borders):
imgui.table_setup_column("", imgui.TableColumnFlags_.width_fixed, 20)
with imscope.table("files_table", 3, imgui.TableFlags_.resizable | imgui.TableFlags_.borders):
imgui.table_setup_column("", imgui.TableColumnFlags_.width_fixed, 25)
imgui.table_setup_column("Path", imgui.TableColumnFlags_.width_stretch)
imgui.table_setup_column("Agg", imgui.TableColumnFlags_.width_fixed, 0)
imgui.table_setup_column("Full", imgui.TableColumnFlags_.width_fixed, 0)
imgui.table_setup_column("Cache", imgui.TableColumnFlags_.width_fixed, 0)
imgui.table_setup_column("Status", imgui.TableColumnFlags_.width_fixed, 60)
imgui.table_headers_row()
app.files.sort(key=lambda f: f.path.lower() if hasattr(f, 'path') else str(f).lower())
for i, f_item in enumerate(app.files):
imgui.table_next_row(); imgui.table_set_column_index(0)
clicked, f_item.selected = imgui.checkbox(f"##{i}", f_item.selected)
if clicked:
if (imgui.is_key_down(imgui.Key.left_shift) or imgui.is_key_down(imgui.Key.right_shift)) and app.files_last_selected >= 0:
start_i = min(app.files_last_selected, i)
end_i = max(app.files_last_selected, i)
for j in range(start_i, end_i + 1): app.files[j].selected = True
app.files_last_selected = i
imgui.table_set_column_index(1); imgui.text(f_item.path if hasattr(f_item, 'path') else str(f_item))
imgui.table_set_column_index(2)
if f_item.auto_aggregate: imgui.text_colored(imgui.ImVec4(0.3, 0.8, 1, 1), "A")
else: imgui.text_disabled(" ")
imgui.same_line(spacing=1)
if imgui.invisible_button(f"agg{i}", imgui.ImVec2(15, 15)):
f_item.auto_aggregate = not f_item.auto_aggregate
if f_item.auto_aggregate: f_item.force_full = False
imgui.table_set_column_index(3)
if f_item.force_full: imgui.text_colored(imgui.ImVec4(1, 0.6, 0.3, 1), "F")
else: imgui.text_disabled(" ")
imgui.same_line(spacing=1)
if imgui.invisible_button(f"full{i}", imgui.ImVec2(15, 15)):
f_item.force_full = not f_item.force_full
if f_item.force_full: f_item.auto_aggregate = False
imgui.table_set_column_index(4)
fpath = f_item.path if hasattr(f_item, 'path') else str(f_item)
is_cached = any(fpath in c for c in getattr(app, '_cached_files', []))
in_context = any((cf.path if hasattr(cf, 'path') else str(cf)) == fpath for cf in app.context_files)
if imgui.button(f"+##add_f_{i}"):
if not in_context:
from src import models
new_item = models.FileItem(path=fpath)
app.context_files.append(new_item)
app._populate_auto_slices(new_item)
imgui.table_set_column_index(1); imgui.text(fpath)
imgui.table_set_column_index(2)
if in_context:
imgui.text_colored(imgui.ImVec4(0.3, 0.8, 0.3, 1), "Active")
else:
imgui.text_disabled(" - ")
if is_cached: imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), "Y")
else: imgui.text_colored(imgui.ImVec4(0.5, 0.5, 0.5, 1), "-")
if imgui.button("Add Files##addf"):
if imgui.button("Add Files to Inventory"):
r = hide_tk_root(); paths = filedialog.askopenfilenames(); r.destroy()
for p in paths:
if p not in [f.path if hasattr(f, 'path') else f for f in app.files]: app.files.append(models.FileItem(path=p))
imgui.same_line()
if imgui.button("Sel All##selall"):
for f in app.files:
f.selected = True
if imgui.button("Clear Selection##inv"):
for f in app.files: f.selected = False
imgui.same_line()
if imgui.button("Unsel##unselall"):
for f in app.files:
f.selected = False
imgui.same_line()
if imgui.button("None##nonesel"):
for f in app.files:
if f.selected:
f.auto_aggregate = False
f.force_full = False
imgui.same_line()
if imgui.button("Agg##aggsel"):
for f in app.files:
if f.selected:
f.auto_aggregate = True
f.force_full = False
imgui.same_line()
if imgui.button("Full##fullsel"):
for f in app.files:
if f.selected:
f.force_full = True
f.auto_aggregate = False
imgui.same_line()
if imgui.button("Del##dels"):
if imgui.button("Remove from Inventory"):
app.files = [f for f in app.files if not f.selected]
imgui.separator()
@@ -3440,9 +3398,16 @@ def render_discussion_hub(app: App) -> None:
def render_discussion_entry(app: App, entry: dict, index: int) -> None:
with imscope.id(f"disc_{index}"):
collapsed, read_mode = entry.get("collapsed", False), entry.get("read_mode", False)
if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed
imgui.same_line(); render_text_viewer(app, f"Entry #{index+1}", entry["content"], id_suffix=f"disc_btn_{index}"); imgui.same_line(); imgui.set_next_item_width(120)
role = entry.get("role", "User")
bg_col = vec4(20, 20, 20, 255) # default
if role == "User": bg_col = vec4(30, 40, 55, 200)
elif role == "AI": bg_col = vec4(35, 50, 40, 200)
elif role == "Vendor API": bg_col = vec4(45, 40, 30, 200)
with imscope.style_color(imgui.Col_.child_bg, bg_col):
collapsed, read_mode = entry.get("collapsed", False), entry.get("read_mode", False)
if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed
imgui.same_line(); render_text_viewer(app, f"Entry #{index+1}", entry["content"], id_suffix=f"disc_btn_{index}"); imgui.same_line(); imgui.set_next_item_width(120)
if imgui.begin_combo("##role", entry["role"]):
for r in app.disc_roles:
if imgui.selectable(r, r == entry["role"])[0]: entry["role"] = r
@@ -4117,43 +4082,39 @@ def render_tool_calls_panel(app: App) -> None:
imgui.table_setup_column("Tier", imgui.TableColumnFlags_.width_fixed, 60)
imgui.table_setup_column("Script", imgui.TableColumnFlags_.width_stretch)
imgui.table_setup_column("Result", imgui.TableColumnFlags_.width_fixed, 100)
imgui.table_headers_row()
clipper = imgui.ListClipper()
clipper.begin(len(log_to_render))
while clipper.step():
for i in range(clipper.display_start, clipper.display_end):
entry = log_to_render[i]
imgui.table_next_row()
script = entry.get("script", "")
res = entry.get("result", "")
combined = f"**COMMAND:**\n```powershell\n{script}\n```\n\n---\n**OUTPUT:**\n```text\n{res}\n```"
imgui.table_next_column()
# Use selectable for the entire row trigger
opened_details = imgui.selectable(f"#{i+1}##row_{i}", False, imgui.SelectableFlags_.span_all_columns)[0]
if opened_details:
app.text_viewer_title = f"Tool Call #{i+1} Details"
app.text_viewer_content = combined
app.text_viewer_type = 'markdown'
app.show_windows["Text Viewer"] = True
imgui.table_next_column()
imgui.text_colored(C_SUB, f"[{entry.get('source_tier', 'main')}]")
imgui.table_next_column()
script_preview = script.replace("\n", " ")[:150]
if len(script) > 150: script_preview += "..."
render_selectable_label(app, f'tc_script_{i}', script_preview, width=-1)
imgui.table_next_column()
res_preview = res.replace("\n", " ")[:30]
if len(res) > 30: res_preview += "..."
render_selectable_label(app, f'tc_res_{i}', res_preview, width=-1)
imgui.end_table()
for i, entry in enumerate(log_to_render):
imgui.table_next_row()
script = entry.get("script", "")
res = entry.get("result", "")
combined = f"**COMMAND:**\n```powershell\n{script}\n```\n\n---\n**OUTPUT:**\n```text\n{res}\n```"
imgui.table_next_column()
# Use selectable for the entire row trigger
opened_details = imgui.selectable(f"#{i+1}##row_{i}", False, imgui.SelectableFlags_.span_all_columns)[0]
if opened_details:
app.text_viewer_title = f"Tool Call #{i+1} Details"
app.text_viewer_content = combined
app.text_viewer_type = 'markdown'
app.show_windows["Text Viewer"] = True
imgui.table_next_column()
imgui.text_colored(C_SUB, f"[{entry.get('source_tier', 'main')}]")
imgui.table_next_column()
script_preview = script.replace("\n", " ")[:150]
if len(script) > 150: script_preview += "..."
render_selectable_label(app, f'tc_script_{i}', script_preview, width=-1)
imgui.table_next_column()
res_preview = res.replace("\n", " ")[:30]
if len(res) > 30: res_preview += "..."
render_selectable_label(app, f'tc_res_{i}', res_preview, width=-1)
imgui.end_table()
if app._scroll_tool_calls_to_bottom:
imgui.set_scroll_here_y(1.0)
@@ -5453,6 +5414,7 @@ def render_empty_context_modal(app: App) -> None:
def render_context_modals(app: App) -> None:
render_empty_context_modal(app)
render_add_context_files_modal(app)
if app.show_missing_files_modal:
imgui.open_popup("Missing Files Warning")