Compare commits
25 Commits
8ee8862ae8
...
d89f971270
| Author | SHA1 | Date | |
|---|---|---|---|
| d89f971270 | |||
| f53e417aec | |||
| f770a4e093 | |||
| dcf10a55b3 | |||
| 2a8af5f728 | |||
| b9e8d70a53 | |||
| 2352a8251e | |||
| ab30c15422 | |||
| 253d3862cc | |||
| 0738f62d98 | |||
| a452c72e1b | |||
| 7d100fb340 | |||
| f0b8f7dedc | |||
| 343fb48959 | |||
| 510527c400 | |||
| 45bffb7387 | |||
| 9c67ee743c | |||
| b077aa8165 | |||
| 1f7880a8c6 | |||
| e48835f7ff | |||
| 3225125af0 | |||
| 54cc85b4f3 | |||
| 40395893c5 | |||
| 9f4fe8e313 | |||
| fefa06beb0 |
@@ -57,7 +57,7 @@ For deep implementation details when planning or implementing tracks, consult `d
|
|||||||
- **Enhanced MMA Observability:** Worker streams and ticket previews now support direct text selection, allowing for easy extraction of specific logs or reasoning fragments during parallel execution.
|
- **Enhanced MMA Observability:** Worker streams and ticket previews now support direct text selection, allowing for easy extraction of specific logs or reasoning fragments during parallel execution.
|
||||||
- **Transparent Context Visibility:** A dedicated **Session Hub** exposes the exact aggregated markdown and resolved system prompt sent to the AI.
|
- **Transparent Context Visibility:** A dedicated **Session Hub** exposes the exact aggregated markdown and resolved system prompt sent to the AI.
|
||||||
- **Injection Timeline:** Discussion history visually indicates the precise moments when files or screenshots were injected into the session context.
|
- **Injection Timeline:** Discussion history visually indicates the precise moments when files or screenshots were injected into the session context.
|
||||||
- **Detailed History Management:** Rich discussion history with branching, timestamping, and specific git commit linkage per conversation.
|
- **Detailed History Management:** Rich discussion history with non-linear timeline branching ("takes"), tabbed interface navigation, specific git commit linkage per conversation, and automated multi-take synthesis.
|
||||||
- **Advanced Log Management:** Optimizes log storage by offloading large data (AI-generated scripts and tool outputs) to unique files within the session directory, using compact `[REF:filename]` pointers in JSON-L logs to minimize token overhead during analysis. Features a dedicated **Log Management panel** for monitoring, whitelisting, and pruning session logs.
|
- **Advanced Log Management:** Optimizes log storage by offloading large data (AI-generated scripts and tool outputs) to unique files within the session directory, using compact `[REF:filename]` pointers in JSON-L logs to minimize token overhead during analysis. Features a dedicated **Log Management panel** for monitoring, whitelisting, and pruning session logs.
|
||||||
- **Full Session Restoration:** Allows users to load and reconstruct entire historical sessions from their log directories. Includes a dedicated, tinted **'Historical Replay' mode** that populates discussion history and provides a read-only view of prior agent activities.
|
- **Full Session Restoration:** Allows users to load and reconstruct entire historical sessions from their log directories. Includes a dedicated, tinted **'Historical Replay' mode** that populates discussion history and provides a read-only view of prior agent activities.
|
||||||
- **Dedicated Diagnostics Hub:** Consolidates real-time telemetry (FPS, CPU, Frame Time) and transient system warnings into a standalone **Diagnostics panel**, providing deep visibility into application health without polluting the discussion history.
|
- **Dedicated Diagnostics Hub:** Consolidates real-time telemetry (FPS, CPU, Frame Time) and transient system warnings into a standalone **Diagnostics panel**, providing deep visibility into application health without polluting the discussion history.
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ This file tracks all major tracks for the project. Each track has its own detail
|
|||||||
*Link: [./tracks/session_context_snapshots_20260311/](./tracks/session_context_snapshots_20260311/)*
|
*Link: [./tracks/session_context_snapshots_20260311/](./tracks/session_context_snapshots_20260311/)*
|
||||||
*Goal: Session-scoped context management, saving Context Presets, MMA assignment, and agent-focused session filtering in the UI.*
|
*Goal: Session-scoped context management, saving Context Presets, MMA assignment, and agent-focused session filtering in the UI.*
|
||||||
|
|
||||||
9. [ ] **Track: Discussion Takes & Timeline Branching**
|
9. [x] **Track: Discussion Takes & Timeline Branching**
|
||||||
*Link: [./tracks/discussion_takes_branching_20260311/](./tracks/discussion_takes_branching_20260311/)*
|
*Link: [./tracks/discussion_takes_branching_20260311/](./tracks/discussion_takes_branching_20260311/)*
|
||||||
*Goal: Non-linear discussion timelines via tabbed "takes", message branching, and synthesis generation workflows.*
|
*Goal: Non-linear discussion timelines via tabbed "takes", message branching, and synthesis generation workflows.*
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,28 @@
|
|||||||
# Implementation Plan: Discussion Takes & Timeline Branching
|
# Implementation Plan: Discussion Takes & Timeline Branching
|
||||||
|
|
||||||
## Phase 1: Backend Support for Timeline Branching
|
## Phase 1: Backend Support for Timeline Branching [checkpoint: 4039589]
|
||||||
- [ ] Task: Write failing tests for extending the session state model to support branching (tree-like history or parallel linear "takes" with a shared ancestor).
|
- [x] Task: Write failing tests for extending the session state model to support branching (tree-like history or parallel linear "takes" with a shared ancestor). [fefa06b]
|
||||||
- [ ] Task: Implement backend logic to branch a session history at a specific message index into a new take ID.
|
- [x] Task: Implement backend logic to branch a session history at a specific message index into a new take ID. [fefa06b]
|
||||||
- [ ] Task: Implement backend logic to promote a specific take ID into an independent, top-level session.
|
- [x] Task: Implement backend logic to promote a specific take ID into an independent, top-level session. [fefa06b]
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Backend Support for Timeline Branching' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 1: Backend Support for Timeline Branching' (Protocol in workflow.md)
|
||||||
|
|
||||||
## Phase 2: GUI Implementation for Tabbed Takes
|
## Phase 2: GUI Implementation for Tabbed Takes [checkpoint: 9c67ee7]
|
||||||
- [ ] Task: Write GUI tests verifying the rendering and navigation of multiple tabs for a single session.
|
- [x] Task: Write GUI tests verifying the rendering and navigation of multiple tabs for a single session. [3225125]
|
||||||
- [ ] Task: Implement a tabbed interface within the Discussion window to switch between different takes of the active session.
|
- [x] Task: Implement a tabbed interface within the Discussion window to switch between different takes of the active session. [3225125]
|
||||||
- [ ] Task: Add a "Split/Branch from here" action to individual message entries in the discussion history.
|
- [x] Task: Add a "Split/Branch from here" action to individual message entries in the discussion history. [e48835f]
|
||||||
- [ ] Task: Add a UI button/action to promote the currently active take to a new separate session.
|
- [x] Task: Add a UI button/action to promote the currently active take to a new separate session. [1f7880a]
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: GUI Implementation for Tabbed Takes' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 2: GUI Implementation for Tabbed Takes' (Protocol in workflow.md)
|
||||||
|
|
||||||
## Phase 3: Synthesis Workflow Formatting
|
## Phase 3: Synthesis Workflow Formatting [checkpoint: f0b8f7d]
|
||||||
- [ ] Task: Write tests for a new text formatting utility that takes multiple history sequences and generates a compressed, diff-like text representation.
|
- [x] Task: Write tests for a new text formatting utility that takes multiple history sequences and generates a compressed, diff-like text representation. [510527c]
|
||||||
- [ ] Task: Implement the sequence differencing and compression logic to clearly highlight variances between takes.
|
- [x] Task: Implement the sequence differencing and compression logic to clearly highlight variances between takes. [510527c]
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Synthesis Workflow Formatting' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 3: Synthesis Workflow Formatting' (Protocol in workflow.md)
|
||||||
|
|
||||||
## Phase 4: Synthesis UI & Agent Integration
|
## Phase 4: Synthesis UI & Agent Integration [checkpoint: 253d386]
|
||||||
- [ ] Task: Write GUI tests for the multi-take selection interface and synthesis action.
|
- [x] Task: Write GUI tests for the multi-take selection interface and synthesis action. [a452c72]
|
||||||
- [ ] Task: Implement a UI mechanism allowing users to select multiple takes and provide a synthesis prompt.
|
- [x] Task: Implement a UI mechanism allowing users to select multiple takes and provide a synthesis prompt. [a452c72]
|
||||||
- [ ] Task: Implement the execution pipeline to feed the compressed differences and user prompt to an AI agent, and route the generated synthesis to a new "take" tab.
|
- [x] Task: Implement the execution pipeline to feed the compressed differences and user prompt to an AI agent, and route the generated synthesis to a new "take" tab. [a452c72]
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Synthesis UI & Agent Integration' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 4: Synthesis UI & Agent Integration' (Protocol in workflow.md)
|
||||||
|
|
||||||
|
## Phase: Review Fixes
|
||||||
|
- [x] Task: Apply review suggestions [2a8af5f]
|
||||||
21
config.toml
21
config.toml
@@ -1,12 +1,12 @@
|
|||||||
[ai]
|
[ai]
|
||||||
provider = "minimax"
|
provider = "gemini_cli"
|
||||||
model = "MiniMax-M2.5"
|
model = "gemini-2.5-flash-lite"
|
||||||
temperature = 0.0
|
temperature = 0.0
|
||||||
top_p = 1.0
|
top_p = 1.0
|
||||||
max_tokens = 32000
|
max_tokens = 32000
|
||||||
history_trunc_limit = 900000
|
history_trunc_limit = 900000
|
||||||
active_preset = "Default"
|
active_preset = ""
|
||||||
system_prompt = ""
|
system_prompt = "Overridden Prompt"
|
||||||
|
|
||||||
[projects]
|
[projects]
|
||||||
paths = [
|
paths = [
|
||||||
@@ -38,8 +38,8 @@ separate_external_tools = false
|
|||||||
"Files & Media" = true
|
"Files & Media" = true
|
||||||
"AI Settings" = true
|
"AI Settings" = true
|
||||||
"MMA Dashboard" = false
|
"MMA Dashboard" = false
|
||||||
"Task DAG" = false
|
"Task DAG" = true
|
||||||
"Usage Analytics" = false
|
"Usage Analytics" = true
|
||||||
"Tier 1" = false
|
"Tier 1" = false
|
||||||
"Tier 2" = false
|
"Tier 2" = false
|
||||||
"Tier 3" = false
|
"Tier 3" = false
|
||||||
@@ -51,18 +51,19 @@ separate_external_tools = false
|
|||||||
"Discussion Hub" = true
|
"Discussion Hub" = true
|
||||||
"Operations Hub" = true
|
"Operations Hub" = true
|
||||||
Message = false
|
Message = false
|
||||||
Response = true
|
Response = false
|
||||||
"Tool Calls" = false
|
"Tool Calls" = false
|
||||||
Theme = true
|
Theme = true
|
||||||
"Log Management" = false
|
"Log Management" = false
|
||||||
Diagnostics = false
|
Diagnostics = false
|
||||||
"External Tools" = false
|
"External Tools" = false
|
||||||
"Shader Editor" = false
|
"Shader Editor" = false
|
||||||
|
"Session Hub" = false
|
||||||
|
|
||||||
[theme]
|
[theme]
|
||||||
palette = "Nord Dark"
|
palette = "10x Dark"
|
||||||
font_path = "C:/projects/manual_slop/assets/fonts/MapleMono-Regular.ttf"
|
font_path = "fonts/Inter-Regular.ttf"
|
||||||
font_size = 18.0
|
font_size = 16.0
|
||||||
scale = 1.0
|
scale = 1.0
|
||||||
transparency = 1.0
|
transparency = 1.0
|
||||||
child_transparency = 1.0
|
child_transparency = 1.0
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ Size=716,455
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Response]
|
[Window][Response]
|
||||||
Pos=1946,1000
|
Pos=245,1014
|
||||||
Size=1339,785
|
Size=1492,948
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Tool Calls]
|
[Window][Tool Calls]
|
||||||
@@ -74,8 +74,8 @@ Collapsed=0
|
|||||||
DockId=0xAFC85805,2
|
DockId=0xAFC85805,2
|
||||||
|
|
||||||
[Window][Theme]
|
[Window][Theme]
|
||||||
Pos=0,1010
|
Pos=0,249
|
||||||
Size=828,999
|
Size=32,951
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,2
|
DockId=0x00000002,2
|
||||||
|
|
||||||
@@ -91,8 +91,8 @@ Collapsed=0
|
|||||||
DockId=0x00000010,0
|
DockId=0x00000010,0
|
||||||
|
|
||||||
[Window][Context Hub]
|
[Window][Context Hub]
|
||||||
Pos=0,1010
|
Pos=0,249
|
||||||
Size=828,999
|
Size=32,951
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,1
|
DockId=0x00000002,1
|
||||||
|
|
||||||
@@ -103,26 +103,26 @@ Collapsed=0
|
|||||||
DockId=0x0000000D,0
|
DockId=0x0000000D,0
|
||||||
|
|
||||||
[Window][Discussion Hub]
|
[Window][Discussion Hub]
|
||||||
Pos=1768,26
|
Pos=807,26
|
||||||
Size=1263,1983
|
Size=873,1174
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000013,0
|
DockId=0x00000013,0
|
||||||
|
|
||||||
[Window][Operations Hub]
|
[Window][Operations Hub]
|
||||||
Pos=830,26
|
Pos=34,26
|
||||||
Size=936,1983
|
Size=771,1174
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000005,0
|
DockId=0x00000005,0
|
||||||
|
|
||||||
[Window][Files & Media]
|
[Window][Files & Media]
|
||||||
Pos=0,1010
|
Pos=0,249
|
||||||
Size=828,999
|
Size=32,951
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,0
|
DockId=0x00000002,0
|
||||||
|
|
||||||
[Window][AI Settings]
|
[Window][AI Settings]
|
||||||
Pos=0,26
|
Pos=0,26
|
||||||
Size=828,982
|
Size=32,221
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000001,0
|
DockId=0x00000001,0
|
||||||
|
|
||||||
@@ -175,8 +175,8 @@ Size=381,329
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Last Script Output]
|
[Window][Last Script Output]
|
||||||
Pos=2567,1006
|
Pos=1076,794
|
||||||
Size=746,548
|
Size=1085,1154
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - Log Entry #1 (request)]
|
[Window][Text Viewer - Log Entry #1 (request)]
|
||||||
@@ -513,17 +513,17 @@ Column 1 Weight=1.0000
|
|||||||
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
|
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
|
||||||
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
|
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
|
||||||
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
|
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
|
||||||
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,26 Size=3031,1983 Split=X
|
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,26 Size=1680,1174 Split=X
|
||||||
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2175,1183 Split=X
|
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2175,1183 Split=X
|
||||||
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2
|
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2
|
||||||
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=828,858 Split=Y Selected=0x8CA2375C
|
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=1071,858 Split=Y Selected=0x8CA2375C
|
||||||
DockNode ID=0x00000001 Parent=0x00000007 SizeRef=824,1056 CentralNode=1 Selected=0x7BD57D6A
|
DockNode ID=0x00000001 Parent=0x00000007 SizeRef=824,1037 CentralNode=1 Selected=0x7BD57D6A
|
||||||
DockNode ID=0x00000002 Parent=0x00000007 SizeRef=824,999 Selected=0xF4139CA2
|
DockNode ID=0x00000002 Parent=0x00000007 SizeRef=824,951 Selected=0x1DCB2623
|
||||||
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=2201,858 Split=X Selected=0x418C7449
|
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=2767,858 Split=X Selected=0x418C7449
|
||||||
DockNode ID=0x00000012 Parent=0x0000000E SizeRef=936,402 Split=Y Selected=0x418C7449
|
DockNode ID=0x00000012 Parent=0x0000000E SizeRef=1297,402 Split=Y Selected=0x418C7449
|
||||||
DockNode ID=0x00000005 Parent=0x00000012 SizeRef=876,1749 Selected=0x418C7449
|
DockNode ID=0x00000005 Parent=0x00000012 SizeRef=876,1749 Selected=0x418C7449
|
||||||
DockNode ID=0x00000006 Parent=0x00000012 SizeRef=876,362 Selected=0x1D56B311
|
DockNode ID=0x00000006 Parent=0x00000012 SizeRef=876,362 Selected=0x1D56B311
|
||||||
DockNode ID=0x00000013 Parent=0x0000000E SizeRef=1263,402 Selected=0x6F2B5B04
|
DockNode ID=0x00000013 Parent=0x0000000E SizeRef=1468,402 Selected=0x6F2B5B04
|
||||||
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
|
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
|
||||||
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=1162,1183 Split=Y Selected=0x3AEC3498
|
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=1162,1183 Split=Y Selected=0x3AEC3498
|
||||||
DockNode ID=0x00000010 Parent=0x00000004 SizeRef=1199,1689 Selected=0xB4CBF21A
|
DockNode ID=0x00000010 Parent=0x00000004 SizeRef=1199,1689 Selected=0xB4CBF21A
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,8 @@ paths = []
|
|||||||
base_dir = "."
|
base_dir = "."
|
||||||
paths = []
|
paths = []
|
||||||
|
|
||||||
|
[context_presets]
|
||||||
|
|
||||||
[gemini_cli]
|
[gemini_cli]
|
||||||
binary_path = "gemini"
|
binary_path = "gemini"
|
||||||
|
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ active = "main"
|
|||||||
|
|
||||||
[discussions.main]
|
[discussions.main]
|
||||||
git_commit = ""
|
git_commit = ""
|
||||||
last_updated = "2026-03-14T09:29:30"
|
last_updated = "2026-03-21T15:21:34"
|
||||||
history = []
|
history = []
|
||||||
|
|||||||
@@ -2191,6 +2191,20 @@ class AppController:
|
|||||||
discussions[name] = project_manager.default_discussion()
|
discussions[name] = project_manager.default_discussion()
|
||||||
self._switch_discussion(name)
|
self._switch_discussion(name)
|
||||||
|
|
||||||
|
def _branch_discussion(self, index: int) -> None:
|
||||||
|
self._flush_disc_entries_to_project()
|
||||||
|
# Generate a unique branch name
|
||||||
|
base_name = self.active_discussion.split("_take_")[0]
|
||||||
|
counter = 1
|
||||||
|
new_name = f"{base_name}_take_{counter}"
|
||||||
|
disc_sec = self.project.get("discussion", {})
|
||||||
|
discussions = disc_sec.get("discussions", {})
|
||||||
|
while new_name in discussions:
|
||||||
|
counter += 1
|
||||||
|
new_name = f"{base_name}_take_{counter}"
|
||||||
|
|
||||||
|
project_manager.branch_discussion(self.project, self.active_discussion, new_name, index)
|
||||||
|
self._switch_discussion(new_name)
|
||||||
def _rename_discussion(self, old_name: str, new_name: str) -> None:
|
def _rename_discussion(self, old_name: str, new_name: str) -> None:
|
||||||
disc_sec = self.project.get("discussion", {})
|
disc_sec = self.project.get("discussion", {})
|
||||||
discussions = disc_sec.get("discussions", {})
|
discussions = disc_sec.get("discussions", {})
|
||||||
|
|||||||
@@ -91,7 +91,14 @@ class AsyncEventQueue:
|
|||||||
"""
|
"""
|
||||||
self._queue.put((event_name, payload))
|
self._queue.put((event_name, payload))
|
||||||
if self.websocket_server:
|
if self.websocket_server:
|
||||||
self.websocket_server.broadcast("events", {"event": event_name, "payload": payload})
|
# Ensure payload is JSON serializable for websocket broadcast
|
||||||
|
serializable_payload = payload
|
||||||
|
if hasattr(payload, 'to_dict'):
|
||||||
|
serializable_payload = payload.to_dict()
|
||||||
|
elif hasattr(payload, '__dict__'):
|
||||||
|
serializable_payload = vars(payload)
|
||||||
|
|
||||||
|
self.websocket_server.broadcast("events", {"event": event_name, "payload": serializable_payload})
|
||||||
|
|
||||||
def get(self) -> Tuple[str, Any]:
|
def get(self) -> Tuple[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
129
src/gui_2.py
129
src/gui_2.py
@@ -2253,7 +2253,7 @@ def hello():
|
|||||||
|
|
||||||
def _render_discussion_panel(self) -> None:
|
def _render_discussion_panel(self) -> None:
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_discussion_panel")
|
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_discussion_panel")
|
||||||
# THINKING indicator
|
# THINKING indicator
|
||||||
is_thinking = self.ai_status in ['sending...', 'streaming...', 'running powershell...']
|
is_thinking = self.ai_status in ['sending...', 'streaming...', 'running powershell...']
|
||||||
if is_thinking:
|
if is_thinking:
|
||||||
val = math.sin(time.time() * 10 * math.pi)
|
val = math.sin(time.time() * 10 * math.pi)
|
||||||
@@ -2262,12 +2262,10 @@ def hello():
|
|||||||
if theme.is_nerv_active():
|
if theme.is_nerv_active():
|
||||||
c = vec4(255, 50, 50, alpha) # More vibrant for NERV
|
c = vec4(255, 50, 50, alpha) # More vibrant for NERV
|
||||||
imgui.text_colored(c, "THINKING...")
|
imgui.text_colored(c, "THINKING...")
|
||||||
imgui.separator()
|
imgui.same_line()
|
||||||
# Prior session viewing mode
|
|
||||||
if self.is_viewing_prior_session:
|
if self.is_viewing_prior_session:
|
||||||
imgui.push_style_color(imgui.Col_.child_bg, vec4(50, 40, 20))
|
imgui.push_style_color(imgui.Col_.child_bg, vec4(50, 40, 20))
|
||||||
imgui.text_colored(vec4(255, 200, 100), "VIEWING PRIOR SESSION")
|
|
||||||
imgui.same_line()
|
|
||||||
if imgui.button("Exit Prior Session"):
|
if imgui.button("Exit Prior Session"):
|
||||||
self.controller.cb_exit_prior_session()
|
self.controller.cb_exit_prior_session()
|
||||||
self._comms_log_dirty = True
|
self._comms_log_dirty = True
|
||||||
@@ -2289,7 +2287,7 @@ def hello():
|
|||||||
if ts:
|
if ts:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
imgui.text_colored(vec4(160, 160, 160), str(ts))
|
imgui.text_colored(vec4(160, 160, 160), str(ts))
|
||||||
|
|
||||||
content = entry.get("content", "")
|
content = entry.get("content", "")
|
||||||
if collapsed:
|
if collapsed:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
@@ -2301,22 +2299,67 @@ def hello():
|
|||||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||||
markdown_helper.render(content, context_id=f'prior_disc_{idx}')
|
markdown_helper.render(content, context_id=f'prior_disc_{idx}')
|
||||||
if is_nerv: imgui.pop_style_color()
|
if is_nerv: imgui.pop_style_color()
|
||||||
|
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
imgui.pop_id()
|
imgui.pop_id()
|
||||||
imgui.end_child()
|
imgui.end_child()
|
||||||
imgui.pop_style_color()
|
imgui.pop_style_color()
|
||||||
|
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_discussion_panel")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.is_viewing_prior_session and imgui.collapsing_header("Discussions", imgui.TreeNodeFlags_.default_open):
|
if not self.is_viewing_prior_session and imgui.collapsing_header("Discussions", imgui.TreeNodeFlags_.default_open):
|
||||||
names = self._get_discussion_names()
|
names = self._get_discussion_names()
|
||||||
if imgui.begin_combo("##disc_sel", self.active_discussion):
|
grouped_discussions = {}
|
||||||
for name in names:
|
for name in names:
|
||||||
is_selected = (name == self.active_discussion)
|
base = name.split("_take_")[0]
|
||||||
if imgui.selectable(name, is_selected)[0]:
|
grouped_discussions.setdefault(base, []).append(name)
|
||||||
self._switch_discussion(name)
|
|
||||||
|
active_base = self.active_discussion.split("_take_")[0]
|
||||||
|
if active_base not in grouped_discussions:
|
||||||
|
active_base = names[0] if names else ""
|
||||||
|
|
||||||
|
base_names = sorted(grouped_discussions.keys())
|
||||||
|
if imgui.begin_combo("##disc_sel", active_base):
|
||||||
|
for bname in base_names:
|
||||||
|
is_selected = (bname == active_base)
|
||||||
|
if imgui.selectable(bname, is_selected)[0]:
|
||||||
|
target = bname if bname in names else grouped_discussions[bname][0]
|
||||||
|
if target != self.active_discussion:
|
||||||
|
self._switch_discussion(target)
|
||||||
if is_selected:
|
if is_selected:
|
||||||
imgui.set_item_default_focus()
|
imgui.set_item_default_focus()
|
||||||
imgui.end_combo()
|
imgui.end_combo()
|
||||||
|
|
||||||
|
current_takes = grouped_discussions.get(active_base, [])
|
||||||
|
if imgui.begin_tab_bar("discussion_takes_tabs"):
|
||||||
|
for take_name in current_takes:
|
||||||
|
label = "Original" if take_name == active_base else take_name.replace(f"{active_base}_", "").replace("_", " ").title()
|
||||||
|
flags = imgui.TabItemFlags_.set_selected if take_name == self.active_discussion else 0
|
||||||
|
res = imgui.begin_tab_item(f"{label}###{take_name}", None, flags)
|
||||||
|
if res[0]:
|
||||||
|
if take_name != self.active_discussion:
|
||||||
|
self._switch_discussion(take_name)
|
||||||
|
imgui.end_tab_item()
|
||||||
|
|
||||||
|
res_s = imgui.begin_tab_item("Synthesis###Synthesis")
|
||||||
|
if res_s[0]:
|
||||||
|
self._render_synthesis_panel()
|
||||||
|
imgui.end_tab_item()
|
||||||
|
|
||||||
|
imgui.end_tab_bar()
|
||||||
|
|
||||||
|
if "_take_" in self.active_discussion:
|
||||||
|
if imgui.button("Promote Take"):
|
||||||
|
base_name = self.active_discussion.split("_take_")[0]
|
||||||
|
new_name = f"{base_name}_promoted"
|
||||||
|
counter = 1
|
||||||
|
while new_name in names:
|
||||||
|
new_name = f"{base_name}_promoted_{counter}"
|
||||||
|
counter += 1
|
||||||
|
project_manager.promote_take(self.project, self.active_discussion, new_name)
|
||||||
|
self._switch_discussion(new_name)
|
||||||
|
imgui.same_line()
|
||||||
|
|
||||||
if self.active_track:
|
if self.active_track:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
changed, self._track_discussion_active = imgui.checkbox("Track Discussion", self._track_discussion_active)
|
changed, self._track_discussion_active = imgui.checkbox("Track Discussion", self._track_discussion_active)
|
||||||
@@ -2331,10 +2374,13 @@ def hello():
|
|||||||
self._flush_disc_entries_to_project()
|
self._flush_disc_entries_to_project()
|
||||||
# Restore project discussion
|
# Restore project discussion
|
||||||
self._switch_discussion(self.active_discussion)
|
self._switch_discussion(self.active_discussion)
|
||||||
|
self.ai_status = "track discussion disabled"
|
||||||
|
|
||||||
disc_sec = self.project.get("discussion", {})
|
disc_sec = self.project.get("discussion", {})
|
||||||
disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {})
|
disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {})
|
||||||
git_commit = disc_data.get("git_commit", "")
|
git_commit = disc_data.get("git_commit", "")
|
||||||
last_updated = disc_data.get("last_updated", "")
|
last_updated = disc_data.get("last_updated", "")
|
||||||
|
|
||||||
imgui.text_colored(C_LBL, "commit:")
|
imgui.text_colored(C_LBL, "commit:")
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
self._render_selectable_label('git_commit_val', git_commit[:12] if git_commit else '(none)', width=100, color=(C_IN if git_commit else C_LBL))
|
self._render_selectable_label('git_commit_val', git_commit[:12] if git_commit else '(none)', width=100, color=(C_IN if git_commit else C_LBL))
|
||||||
@@ -2347,9 +2393,11 @@ def hello():
|
|||||||
disc_data["git_commit"] = cmt
|
disc_data["git_commit"] = cmt
|
||||||
disc_data["last_updated"] = project_manager.now_ts()
|
disc_data["last_updated"] = project_manager.now_ts()
|
||||||
self.ai_status = f"commit: {cmt[:12]}"
|
self.ai_status = f"commit: {cmt[:12]}"
|
||||||
|
|
||||||
imgui.text_colored(C_LBL, "updated:")
|
imgui.text_colored(C_LBL, "updated:")
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
imgui.text_colored(C_SUB, last_updated if last_updated else "(never)")
|
imgui.text_colored(C_SUB, last_updated if last_updated else "(never)")
|
||||||
|
|
||||||
ch, self.ui_disc_new_name_input = imgui.input_text("##new_disc", self.ui_disc_new_name_input)
|
ch, self.ui_disc_new_name_input = imgui.input_text("##new_disc", self.ui_disc_new_name_input)
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("Create"):
|
if imgui.button("Create"):
|
||||||
@@ -2362,6 +2410,7 @@ def hello():
|
|||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("Delete"):
|
if imgui.button("Delete"):
|
||||||
self._delete_discussion(self.active_discussion)
|
self._delete_discussion(self.active_discussion)
|
||||||
|
|
||||||
if not self.is_viewing_prior_session:
|
if not self.is_viewing_prior_session:
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
if imgui.button("+ Entry"):
|
if imgui.button("+ Entry"):
|
||||||
@@ -2381,6 +2430,7 @@ def hello():
|
|||||||
self._flush_to_config()
|
self._flush_to_config()
|
||||||
models.save_config(self.config)
|
models.save_config(self.config)
|
||||||
self.ai_status = "discussion saved"
|
self.ai_status = "discussion saved"
|
||||||
|
|
||||||
ch, self.ui_auto_add_history = imgui.checkbox("Auto-add message & response to history", self.ui_auto_add_history)
|
ch, self.ui_auto_add_history = imgui.checkbox("Auto-add message & response to history", self.ui_auto_add_history)
|
||||||
# Truncation controls
|
# Truncation controls
|
||||||
imgui.text("Keep Pairs:")
|
imgui.text("Keep Pairs:")
|
||||||
@@ -2393,15 +2443,19 @@ def hello():
|
|||||||
with self._disc_entries_lock:
|
with self._disc_entries_lock:
|
||||||
self.disc_entries = truncate_entries(self.disc_entries, self.ui_disc_truncate_pairs)
|
self.disc_entries = truncate_entries(self.disc_entries, self.ui_disc_truncate_pairs)
|
||||||
self.ai_status = f"history truncated to {self.ui_disc_truncate_pairs} pairs"
|
self.ai_status = f"history truncated to {self.ui_disc_truncate_pairs} pairs"
|
||||||
|
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
if imgui.collapsing_header("Roles"):
|
if imgui.collapsing_header("Roles"):
|
||||||
imgui.begin_child("roles_scroll", imgui.ImVec2(0, 100), True)
|
imgui.begin_child("roles_scroll", imgui.ImVec2(0, 100), True)
|
||||||
for i, r in enumerate(self.disc_roles):
|
for i, r in enumerate(self.disc_roles):
|
||||||
if imgui.button(f"x##r{i}"):
|
imgui.push_id(f"role_{i}")
|
||||||
|
if imgui.button("X"):
|
||||||
self.disc_roles.pop(i)
|
self.disc_roles.pop(i)
|
||||||
|
imgui.pop_id()
|
||||||
break
|
break
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
imgui.text(r)
|
imgui.text(r)
|
||||||
|
imgui.pop_id()
|
||||||
imgui.end_child()
|
imgui.end_child()
|
||||||
ch, self.ui_disc_new_role_input = imgui.input_text("##new_role", self.ui_disc_new_role_input)
|
ch, self.ui_disc_new_role_input = imgui.input_text("##new_role", self.ui_disc_new_role_input)
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
@@ -2410,9 +2464,10 @@ def hello():
|
|||||||
if r and r not in self.disc_roles:
|
if r and r not in self.disc_roles:
|
||||||
self.disc_roles.append(r)
|
self.disc_roles.append(r)
|
||||||
self.ui_disc_new_role_input = ""
|
self.ui_disc_new_role_input = ""
|
||||||
|
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
imgui.begin_child("disc_scroll", imgui.ImVec2(0, 0), False)
|
imgui.begin_child("disc_scroll", imgui.ImVec2(0, 0), False)
|
||||||
|
|
||||||
# Filter entries based on focused agent persona
|
# Filter entries based on focused agent persona
|
||||||
display_entries = self.disc_entries
|
display_entries = self.disc_entries
|
||||||
if self.ui_focus_agent:
|
if self.ui_focus_agent:
|
||||||
@@ -2443,10 +2498,12 @@ def hello():
|
|||||||
if imgui.selectable(r, r == entry["role"])[0]:
|
if imgui.selectable(r, r == entry["role"])[0]:
|
||||||
entry["role"] = r
|
entry["role"] = r
|
||||||
imgui.end_combo()
|
imgui.end_combo()
|
||||||
|
|
||||||
if not collapsed:
|
if not collapsed:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("[Edit]" if read_mode else "[Read]"):
|
if imgui.button("[Edit]" if read_mode else "[Read]"):
|
||||||
entry["read_mode"] = not read_mode
|
entry["read_mode"] = not read_mode
|
||||||
|
|
||||||
ts_str = entry.get("ts", "")
|
ts_str = entry.get("ts", "")
|
||||||
if ts_str:
|
if ts_str:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
@@ -2467,6 +2524,7 @@ def hello():
|
|||||||
if imgui.is_item_hovered():
|
if imgui.is_item_hovered():
|
||||||
tooltip = "Files injected at this point:\n" + "\n".join([f.path for f in injected_here])
|
tooltip = "Files injected at this point:\n" + "\n".join([f.path for f in injected_here])
|
||||||
imgui.set_tooltip(tooltip)
|
imgui.set_tooltip(tooltip)
|
||||||
|
|
||||||
if collapsed:
|
if collapsed:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("Ins"):
|
if imgui.button("Ins"):
|
||||||
@@ -2477,10 +2535,13 @@ def hello():
|
|||||||
imgui.pop_id()
|
imgui.pop_id()
|
||||||
break # Break from inner loop, clipper will re-step
|
break # Break from inner loop, clipper will re-step
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
preview = entry["content"].replace("\\n", " ")[:60]
|
if imgui.button("Branch"):
|
||||||
|
self._branch_discussion(i)
|
||||||
|
imgui.same_line()
|
||||||
|
preview = entry["content"].replace("\n", " ")[:60]
|
||||||
if len(entry["content"]) > 60: preview += "..."
|
if len(entry["content"]) > 60: preview += "..."
|
||||||
if not preview.strip() and entry.get("thinking_segments"):
|
if not preview.strip() and entry.get("thinking_segments"):
|
||||||
preview = entry["thinking_segments"][0]["content"].replace("\\n", " ")[:60]
|
preview = entry["thinking_segments"][0]["content"].replace("\n", " ")[:60]
|
||||||
if len(entry["thinking_segments"][0]["content"]) > 60: preview += "..."
|
if len(entry["thinking_segments"][0]["content"]) > 60: preview += "..."
|
||||||
imgui.text_colored(vec4(160, 160, 150), preview)
|
imgui.text_colored(vec4(160, 160, 150), preview)
|
||||||
if not collapsed:
|
if not collapsed:
|
||||||
@@ -2537,10 +2598,46 @@ def hello():
|
|||||||
ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150))
|
ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150))
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
imgui.pop_id()
|
imgui.pop_id()
|
||||||
|
|
||||||
if self._scroll_disc_to_bottom:
|
if self._scroll_disc_to_bottom:
|
||||||
imgui.set_scroll_here_y(1.0)
|
imgui.set_scroll_here_y(1.0)
|
||||||
self._scroll_disc_to_bottom = False
|
self._scroll_disc_to_bottom = False
|
||||||
|
|
||||||
imgui.end_child()
|
imgui.end_child()
|
||||||
|
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_discussion_panel")
|
||||||
|
|
||||||
|
def _render_synthesis_panel(self) -> None:
|
||||||
|
"""Renders a panel for synthesizing multiple discussion takes."""
|
||||||
|
imgui.text("Select takes to synthesize:")
|
||||||
|
discussions = self.project.get('discussion', {}).get('discussions', {})
|
||||||
|
if not hasattr(self, 'ui_synthesis_selected_takes'):
|
||||||
|
self.ui_synthesis_selected_takes = {name: False for name in discussions}
|
||||||
|
if not hasattr(self, 'ui_synthesis_prompt'):
|
||||||
|
self.ui_synthesis_prompt = ""
|
||||||
|
for name in discussions:
|
||||||
|
_, self.ui_synthesis_selected_takes[name] = imgui.checkbox(name, self.ui_synthesis_selected_takes.get(name, False))
|
||||||
|
imgui.spacing()
|
||||||
|
imgui.text("Synthesis Prompt:")
|
||||||
|
_, self.ui_synthesis_prompt = imgui.input_text_multiline("##synthesis_prompt", self.ui_synthesis_prompt, imgui.ImVec2(-1, 100))
|
||||||
|
if imgui.button("Generate Synthesis"):
|
||||||
|
selected = [name for name, sel in self.ui_synthesis_selected_takes.items() if sel]
|
||||||
|
if len(selected) > 1:
|
||||||
|
from src import synthesis_formatter
|
||||||
|
discussions_dict = self.project.get('discussion', {}).get('discussions', {})
|
||||||
|
takes_dict = {name: discussions_dict.get(name, {}).get('history', []) for name in selected}
|
||||||
|
diff_text = synthesis_formatter.format_takes_diff(takes_dict)
|
||||||
|
prompt = f"{self.ui_synthesis_prompt}\n\nHere are the variations:\n{diff_text}"
|
||||||
|
|
||||||
|
new_name = "synthesis_take"
|
||||||
|
counter = 1
|
||||||
|
while new_name in discussions_dict:
|
||||||
|
new_name = f"synthesis_take_{counter}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
self._create_discussion(new_name)
|
||||||
|
with self._disc_entries_lock:
|
||||||
|
self.disc_entries.append({"role": "User", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()})
|
||||||
|
self._handle_generate_send()
|
||||||
|
|
||||||
def _render_persona_selector_panel(self) -> None:
|
def _render_persona_selector_panel(self) -> None:
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_persona_selector_panel")
|
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_persona_selector_panel")
|
||||||
|
|||||||
@@ -424,3 +424,36 @@ def calculate_track_progress(tickets: list) -> dict:
|
|||||||
"todo": todo
|
"todo": todo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def branch_discussion(project_dict: dict, source_id: str, new_id: str, message_index: int) -> None:
|
||||||
|
"""
|
||||||
|
Creates a new discussion in project_dict['discussion']['discussions'] by copying
|
||||||
|
the history from source_id up to (and including) message_index, and sets active to new_id.
|
||||||
|
"""
|
||||||
|
if "discussion" not in project_dict or "discussions" not in project_dict["discussion"]:
|
||||||
|
return
|
||||||
|
if source_id not in project_dict["discussion"]["discussions"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
source_disc = project_dict["discussion"]["discussions"][source_id]
|
||||||
|
new_disc = default_discussion()
|
||||||
|
new_disc["git_commit"] = source_disc.get("git_commit", "")
|
||||||
|
# Copy history up to and including message_index
|
||||||
|
new_disc["history"] = source_disc["history"][:message_index + 1]
|
||||||
|
|
||||||
|
project_dict["discussion"]["discussions"][new_id] = new_disc
|
||||||
|
project_dict["discussion"]["active"] = new_id
|
||||||
|
|
||||||
|
def promote_take(project_dict: dict, take_id: str, new_id: str) -> None:
|
||||||
|
"""Renames a take_id to new_id in the discussions dict."""
|
||||||
|
if "discussion" not in project_dict or "discussions" not in project_dict["discussion"]:
|
||||||
|
return
|
||||||
|
if take_id not in project_dict["discussion"]["discussions"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
disc = project_dict["discussion"]["discussions"].pop(take_id)
|
||||||
|
project_dict["discussion"]["discussions"][new_id] = disc
|
||||||
|
|
||||||
|
# If the take was active, update the active pointer
|
||||||
|
if project_dict["discussion"].get("active") == take_id:
|
||||||
|
project_dict["discussion"]["active"] = new_id
|
||||||
|
|||||||
42
src/synthesis_formatter.py
Normal file
42
src/synthesis_formatter.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
def format_takes_diff(takes: dict[str, list[dict]]) -> str:
|
||||||
|
if not takes:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
histories = list(takes.values())
|
||||||
|
if not histories:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
min_len = min(len(h) for h in histories)
|
||||||
|
common_prefix_len = 0
|
||||||
|
for i in range(min_len):
|
||||||
|
first_msg = histories[0][i]
|
||||||
|
if all(h[i] == first_msg for h in histories):
|
||||||
|
common_prefix_len += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
shared_lines = []
|
||||||
|
for i in range(common_prefix_len):
|
||||||
|
msg = histories[0][i]
|
||||||
|
shared_lines.append(f"{msg.get('role', 'unknown')}: {msg.get('content', '')}")
|
||||||
|
|
||||||
|
shared_text = "=== Shared History ==="
|
||||||
|
if shared_lines:
|
||||||
|
shared_text += "\n" + "\n".join(shared_lines)
|
||||||
|
|
||||||
|
variation_lines = []
|
||||||
|
if len(takes) > 1:
|
||||||
|
for take_name, history in takes.items():
|
||||||
|
if len(history) > common_prefix_len:
|
||||||
|
variation_lines.append(f"[{take_name}]")
|
||||||
|
for i in range(common_prefix_len, len(history)):
|
||||||
|
msg = history[i]
|
||||||
|
variation_lines.append(f"{msg.get('role', 'unknown')}: {msg.get('content', '')}")
|
||||||
|
variation_lines.append("")
|
||||||
|
else:
|
||||||
|
# Single take case
|
||||||
|
pass
|
||||||
|
|
||||||
|
variations_text = "=== Variations ===\n" + "\n".join(variation_lines)
|
||||||
|
|
||||||
|
return shared_text + "\n\n" + variations_text
|
||||||
50
tests/test_discussion_takes.py
Normal file
50
tests/test_discussion_takes.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import unittest
|
||||||
|
from src import project_manager
|
||||||
|
|
||||||
|
class TestDiscussionTakes(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.project_dict = project_manager.default_project("test_branching")
|
||||||
|
# Populate initial history in 'main'
|
||||||
|
self.project_dict["discussion"]["discussions"]["main"]["history"] = [
|
||||||
|
"User: Message 0",
|
||||||
|
"AI: Response 0",
|
||||||
|
"User: Message 1",
|
||||||
|
"AI: Response 1",
|
||||||
|
"User: Message 2"
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_branch_discussion_creates_new_take(self):
|
||||||
|
"""Verify that branch_discussion copies history up to index and sets active."""
|
||||||
|
source_id = "main"
|
||||||
|
new_id = "take_1"
|
||||||
|
message_index = 1
|
||||||
|
|
||||||
|
# This will fail with AttributeError until implemented in project_manager.py
|
||||||
|
project_manager.branch_discussion(self.project_dict, source_id, new_id, message_index)
|
||||||
|
|
||||||
|
# Asserts
|
||||||
|
self.assertIn(new_id, self.project_dict["discussion"]["discussions"])
|
||||||
|
new_history = self.project_dict["discussion"]["discussions"][new_id]["history"]
|
||||||
|
self.assertEqual(len(new_history), 2)
|
||||||
|
self.assertEqual(new_history[0], "User: Message 0")
|
||||||
|
self.assertEqual(new_history[1], "AI: Response 0")
|
||||||
|
self.assertEqual(self.project_dict["discussion"]["active"], new_id)
|
||||||
|
|
||||||
|
def test_promote_take_renames_discussion(self):
|
||||||
|
"""Verify that promote_take renames a discussion key."""
|
||||||
|
take_id = "take_experimental"
|
||||||
|
self.project_dict["discussion"]["discussions"][take_id] = project_manager.default_discussion()
|
||||||
|
self.project_dict["discussion"]["discussions"][take_id]["history"] = ["User: Experimental"]
|
||||||
|
|
||||||
|
new_id = "feature_refined"
|
||||||
|
|
||||||
|
# This will fail with AttributeError until implemented in project_manager.py
|
||||||
|
project_manager.promote_take(self.project_dict, take_id, new_id)
|
||||||
|
|
||||||
|
# Asserts
|
||||||
|
self.assertNotIn(take_id, self.project_dict["discussion"]["discussions"])
|
||||||
|
self.assertIn(new_id, self.project_dict["discussion"]["discussions"])
|
||||||
|
self.assertEqual(self.project_dict["discussion"]["discussions"][new_id]["history"], ["User: Experimental"])
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
96
tests/test_discussion_takes_gui.py
Normal file
96
tests/test_discussion_takes_gui.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import MagicMock, patch, call
|
||||||
|
from src.gui_2 import App
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app_instance():
|
||||||
|
with (
|
||||||
|
patch('src.models.load_config', return_value={'ai': {'provider': 'gemini', 'model': 'gemini-2.5-flash-lite'}, 'projects': {}}),
|
||||||
|
patch('src.models.save_config'),
|
||||||
|
patch('src.gui_2.project_manager'),
|
||||||
|
patch('src.gui_2.session_logger'),
|
||||||
|
patch('src.gui_2.immapp.run'),
|
||||||
|
patch('src.app_controller.AppController._load_active_project'),
|
||||||
|
patch('src.app_controller.AppController._fetch_models'),
|
||||||
|
patch.object(App, '_load_fonts'),
|
||||||
|
patch.object(App, '_post_init'),
|
||||||
|
patch('src.app_controller.AppController._prune_old_logs'),
|
||||||
|
patch('src.app_controller.AppController.start_services'),
|
||||||
|
patch('src.api_hooks.HookServer'),
|
||||||
|
patch('src.ai_client.set_provider'),
|
||||||
|
patch('src.ai_client.reset_session')
|
||||||
|
):
|
||||||
|
app = App()
|
||||||
|
# Setup project discussions
|
||||||
|
app.project = {
|
||||||
|
"discussion": {
|
||||||
|
"active": "main",
|
||||||
|
"discussions": {
|
||||||
|
"main": {"history": []},
|
||||||
|
"take_1": {"history": []},
|
||||||
|
"take_2": {"history": []}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.active_discussion = "main"
|
||||||
|
app.is_viewing_prior_session = False
|
||||||
|
app.ui_disc_new_name_input = ""
|
||||||
|
app.ui_disc_truncate_pairs = 1
|
||||||
|
yield app
|
||||||
|
|
||||||
|
def test_render_discussion_tabs(app_instance):
|
||||||
|
"""Verify that _render_discussion_panel uses tabs for discussions."""
|
||||||
|
with patch('src.gui_2.imgui') as mock_imgui:
|
||||||
|
# Setup defaults for common imgui calls to avoid unpacking errors
|
||||||
|
mock_imgui.collapsing_header.return_value = True
|
||||||
|
mock_imgui.begin_combo.return_value = False
|
||||||
|
mock_imgui.input_text.return_value = (False, "")
|
||||||
|
mock_imgui.input_int.return_value = (False, 0)
|
||||||
|
mock_imgui.button.return_value = False
|
||||||
|
mock_imgui.checkbox.return_value = (False, False)
|
||||||
|
mock_imgui.begin_child.return_value = True
|
||||||
|
mock_imgui.selectable.return_value = (False, False)
|
||||||
|
|
||||||
|
# Mock tab bar calls
|
||||||
|
mock_imgui.begin_tab_bar.return_value = True
|
||||||
|
mock_imgui.begin_tab_item.return_value = (False, False)
|
||||||
|
|
||||||
|
app_instance._render_discussion_panel()
|
||||||
|
|
||||||
|
# Check if begin_tab_bar was called
|
||||||
|
# This SHOULD fail if it's not implemented yet
|
||||||
|
mock_imgui.begin_tab_bar.assert_called_with("##discussion_tabs")
|
||||||
|
|
||||||
|
# Check if begin_tab_item was called for each discussion
|
||||||
|
names = sorted(["main", "take_1", "take_2"])
|
||||||
|
for name in names:
|
||||||
|
mock_imgui.begin_tab_item.assert_any_call(name)
|
||||||
|
|
||||||
|
def test_switching_discussion_via_tabs(app_instance):
|
||||||
|
"""Verify that clicking a tab switches the discussion."""
|
||||||
|
with patch('src.gui_2.imgui') as mock_imgui, \
|
||||||
|
patch('src.app_controller.AppController._switch_discussion') as mock_switch:
|
||||||
|
# Setup defaults
|
||||||
|
mock_imgui.collapsing_header.return_value = True
|
||||||
|
mock_imgui.begin_combo.return_value = False
|
||||||
|
mock_imgui.input_text.return_value = (False, "")
|
||||||
|
mock_imgui.input_int.return_value = (False, 0)
|
||||||
|
mock_imgui.button.return_value = False
|
||||||
|
mock_imgui.checkbox.return_value = (False, False)
|
||||||
|
mock_imgui.begin_child.return_value = True
|
||||||
|
mock_imgui.selectable.return_value = (False, False)
|
||||||
|
|
||||||
|
mock_imgui.begin_tab_bar.return_value = True
|
||||||
|
|
||||||
|
# Simulate 'take_1' being active/selected
|
||||||
|
def side_effect(name, flags=None):
|
||||||
|
if name == "take_1":
|
||||||
|
return (True, True)
|
||||||
|
return (False, True)
|
||||||
|
|
||||||
|
mock_imgui.begin_tab_item.side_effect = side_effect
|
||||||
|
|
||||||
|
app_instance._render_discussion_panel()
|
||||||
|
|
||||||
|
# If implemented with tabs, this should be called
|
||||||
|
mock_switch.assert_called_with("take_1")
|
||||||
@@ -7,6 +7,7 @@ def test_file_item_fields():
|
|||||||
assert item.path == "src/models.py"
|
assert item.path == "src/models.py"
|
||||||
assert item.auto_aggregate is True
|
assert item.auto_aggregate is True
|
||||||
assert item.force_full is False
|
assert item.force_full is False
|
||||||
|
assert item.injected_at is None
|
||||||
|
|
||||||
def test_file_item_to_dict():
|
def test_file_item_to_dict():
|
||||||
"""Test that FileItem can be serialized to a dict."""
|
"""Test that FileItem can be serialized to a dict."""
|
||||||
@@ -14,7 +15,8 @@ def test_file_item_to_dict():
|
|||||||
expected = {
|
expected = {
|
||||||
"path": "test.py",
|
"path": "test.py",
|
||||||
"auto_aggregate": False,
|
"auto_aggregate": False,
|
||||||
"force_full": True
|
"force_full": True,
|
||||||
|
"injected_at": None
|
||||||
}
|
}
|
||||||
assert item.to_dict() == expected
|
assert item.to_dict() == expected
|
||||||
|
|
||||||
@@ -23,12 +25,14 @@ def test_file_item_from_dict():
|
|||||||
data = {
|
data = {
|
||||||
"path": "test.py",
|
"path": "test.py",
|
||||||
"auto_aggregate": False,
|
"auto_aggregate": False,
|
||||||
"force_full": True
|
"force_full": True,
|
||||||
|
"injected_at": 123.456
|
||||||
}
|
}
|
||||||
item = FileItem.from_dict(data)
|
item = FileItem.from_dict(data)
|
||||||
assert item.path == "test.py"
|
assert item.path == "test.py"
|
||||||
assert item.auto_aggregate is False
|
assert item.auto_aggregate is False
|
||||||
assert item.force_full is True
|
assert item.force_full is True
|
||||||
|
assert item.injected_at == 123.456
|
||||||
|
|
||||||
def test_file_item_from_dict_defaults():
|
def test_file_item_from_dict_defaults():
|
||||||
"""Test that FileItem.from_dict handles missing fields."""
|
"""Test that FileItem.from_dict handles missing fields."""
|
||||||
@@ -37,3 +41,4 @@ def test_file_item_from_dict_defaults():
|
|||||||
assert item.path == "test.py"
|
assert item.path == "test.py"
|
||||||
assert item.auto_aggregate is True
|
assert item.auto_aggregate is True
|
||||||
assert item.force_full is False
|
assert item.force_full is False
|
||||||
|
assert item.injected_at is None
|
||||||
|
|||||||
53
tests/test_gui_discussion_tabs.py
Normal file
53
tests/test_gui_discussion_tabs.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import patch, MagicMock, PropertyMock
|
||||||
|
|
||||||
|
from src import gui_2
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_gui():
|
||||||
|
gui = gui_2.App()
|
||||||
|
gui.project = {
|
||||||
|
'discussion': {
|
||||||
|
'active': 'main',
|
||||||
|
'discussions': {
|
||||||
|
'main': {'history': []},
|
||||||
|
'main_take_1': {'history': []},
|
||||||
|
'other_topic': {'history': []}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gui.active_discussion = 'main'
|
||||||
|
gui.perf_profiling_enabled = False
|
||||||
|
gui.is_viewing_prior_session = False
|
||||||
|
gui._get_discussion_names = lambda: ['main', 'main_take_1', 'other_topic']
|
||||||
|
return gui
|
||||||
|
|
||||||
|
def test_discussion_tabs_rendered(mock_gui):
|
||||||
|
with patch('src.gui_2.imgui') as mock_imgui, \
|
||||||
|
patch('src.app_controller.AppController.active_project_root', new_callable=PropertyMock, return_value='.'):
|
||||||
|
|
||||||
|
# We expect a combo box for base discussion
|
||||||
|
mock_imgui.begin_combo.return_value = True
|
||||||
|
mock_imgui.selectable.return_value = (False, False)
|
||||||
|
|
||||||
|
# We expect a tab bar for takes
|
||||||
|
mock_imgui.begin_tab_bar.return_value = True
|
||||||
|
mock_imgui.begin_tab_item.return_value = (True, True)
|
||||||
|
mock_imgui.input_text.return_value = (False, "")
|
||||||
|
mock_imgui.input_text_multiline.return_value = (False, "")
|
||||||
|
mock_imgui.checkbox.return_value = (False, False)
|
||||||
|
mock_imgui.input_int.return_value = (False, 0)
|
||||||
|
|
||||||
|
mock_clipper = MagicMock()
|
||||||
|
mock_clipper.step.return_value = False
|
||||||
|
mock_imgui.ListClipper.return_value = mock_clipper
|
||||||
|
|
||||||
|
mock_gui._render_discussion_panel()
|
||||||
|
|
||||||
|
mock_imgui.begin_combo.assert_called_once_with("##disc_sel", 'main')
|
||||||
|
mock_imgui.begin_tab_bar.assert_called_once_with('discussion_takes_tabs')
|
||||||
|
|
||||||
|
calls = [c[0][0] for c in mock_imgui.begin_tab_item.call_args_list]
|
||||||
|
assert 'Original###main' in calls
|
||||||
|
assert 'Take 1###main_take_1' in calls
|
||||||
|
assert 'Synthesis###Synthesis' in calls
|
||||||
@@ -91,6 +91,7 @@ def test_track_discussion_toggle(mock_app: App):
|
|||||||
mock_imgui.button.return_value = False
|
mock_imgui.button.return_value = False
|
||||||
mock_imgui.collapsing_header.return_value = True # For Discussions header
|
mock_imgui.collapsing_header.return_value = True # For Discussions header
|
||||||
mock_imgui.input_text.side_effect = lambda label, value, *args, **kwargs: (False, value)
|
mock_imgui.input_text.side_effect = lambda label, value, *args, **kwargs: (False, value)
|
||||||
|
mock_imgui.input_text_multiline.side_effect = lambda label, value, *args, **kwargs: (False, value)
|
||||||
mock_imgui.input_int.side_effect = lambda label, value, *args, **kwargs: (False, value)
|
mock_imgui.input_int.side_effect = lambda label, value, *args, **kwargs: (False, value)
|
||||||
mock_imgui.begin_child.return_value = True
|
mock_imgui.begin_child.return_value = True
|
||||||
# Mock clipper to avoid the while loop hang
|
# Mock clipper to avoid the while loop hang
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ def test_render_discussion_panel_symbol_lookup(mock_app, role):
|
|||||||
with (
|
with (
|
||||||
patch('src.gui_2.imgui') as mock_imgui,
|
patch('src.gui_2.imgui') as mock_imgui,
|
||||||
patch('src.gui_2.mcp_client') as mock_mcp,
|
patch('src.gui_2.mcp_client') as mock_mcp,
|
||||||
patch('src.gui_2.project_manager') as mock_pm
|
patch('src.gui_2.project_manager') as mock_pm,
|
||||||
|
patch('src.markdown_helper.imgui_md') as mock_md
|
||||||
):
|
):
|
||||||
# Set up App instance state
|
# Set up App instance state
|
||||||
mock_app.perf_profiling_enabled = False
|
mock_app.perf_profiling_enabled = False
|
||||||
|
|||||||
56
tests/test_gui_synthesis.py
Normal file
56
tests/test_gui_synthesis.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import MagicMock, patch, ANY
|
||||||
|
from src.gui_2 import App
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app_instance():
|
||||||
|
with (
|
||||||
|
patch('src.models.load_config', return_value={'ai': {'provider': 'gemini', 'model': 'gemini-2.5-flash-lite'}, 'projects': {}}),
|
||||||
|
patch('src.models.save_config'),
|
||||||
|
patch('src.gui_2.project_manager'),
|
||||||
|
patch('src.gui_2.session_logger'),
|
||||||
|
patch('src.gui_2.immapp.run'),
|
||||||
|
patch('src.app_controller.AppController._load_active_project'),
|
||||||
|
patch('src.app_controller.AppController._fetch_models'),
|
||||||
|
patch.object(App, '_load_fonts'),
|
||||||
|
patch.object(App, '_post_init'),
|
||||||
|
patch('src.app_controller.AppController._prune_old_logs'),
|
||||||
|
patch('src.app_controller.AppController.start_services'),
|
||||||
|
patch('src.api_hooks.HookServer'),
|
||||||
|
patch('src.ai_client.set_provider'),
|
||||||
|
patch('src.ai_client.reset_session')
|
||||||
|
):
|
||||||
|
app = App()
|
||||||
|
app.project = {
|
||||||
|
"discussion": {
|
||||||
|
"active": "main",
|
||||||
|
"discussions": {
|
||||||
|
"main": {"history": []},
|
||||||
|
"take_1": {"history": []},
|
||||||
|
"take_2": {"history": []}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.ui_synthesis_prompt = "Summarize these takes"
|
||||||
|
yield app
|
||||||
|
|
||||||
|
def test_render_synthesis_panel(app_instance):
|
||||||
|
"""Verify that _render_synthesis_panel renders checkboxes for takes and input for prompt."""
|
||||||
|
with patch('src.gui_2.imgui') as mock_imgui:
|
||||||
|
mock_imgui.checkbox.return_value = (False, False)
|
||||||
|
mock_imgui.input_text_multiline.return_value = (False, app_instance.ui_synthesis_prompt)
|
||||||
|
mock_imgui.button.return_value = False
|
||||||
|
|
||||||
|
# Call the method we are testing
|
||||||
|
app_instance._render_synthesis_panel()
|
||||||
|
|
||||||
|
# 1. Assert imgui.checkbox is called for each take in project_dict['discussion']['discussions']
|
||||||
|
discussions = app_instance.project['discussion']['discussions']
|
||||||
|
for name in discussions:
|
||||||
|
mock_imgui.checkbox.assert_any_call(name, ANY)
|
||||||
|
|
||||||
|
# 2. Assert imgui.input_text_multiline is called for the prompt
|
||||||
|
mock_imgui.input_text_multiline.assert_called_with("##synthesis_prompt", app_instance.ui_synthesis_prompt, ANY)
|
||||||
|
|
||||||
|
# 3. Assert imgui.button is called for 'Generate Synthesis'
|
||||||
|
mock_imgui.button.assert_any_call("Generate Synthesis")
|
||||||
@@ -5,7 +5,7 @@ from src.gui_2 import App
|
|||||||
|
|
||||||
|
|
||||||
def _make_app(**kwargs):
|
def _make_app(**kwargs):
|
||||||
app = MagicMock(spec=App)
|
app = MagicMock()
|
||||||
app.mma_streams = kwargs.get("mma_streams", {})
|
app.mma_streams = kwargs.get("mma_streams", {})
|
||||||
app.mma_tier_usage = kwargs.get("mma_tier_usage", {
|
app.mma_tier_usage = kwargs.get("mma_tier_usage", {
|
||||||
"Tier 1": {"input": 0, "output": 0, "model": "gemini-3.1-pro-preview"},
|
"Tier 1": {"input": 0, "output": 0, "model": "gemini-3.1-pro-preview"},
|
||||||
@@ -13,6 +13,7 @@ def _make_app(**kwargs):
|
|||||||
"Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
"Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
||||||
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
||||||
})
|
})
|
||||||
|
app.ui_focus_agent = kwargs.get("ui_focus_agent", None)
|
||||||
app.tracks = kwargs.get("tracks", [])
|
app.tracks = kwargs.get("tracks", [])
|
||||||
app.active_track = kwargs.get("active_track", None)
|
app.active_track = kwargs.get("active_track", None)
|
||||||
app.active_tickets = kwargs.get("active_tickets", [])
|
app.active_tickets = kwargs.get("active_tickets", [])
|
||||||
|
|||||||
59
tests/test_synthesis_formatter.py
Normal file
59
tests/test_synthesis_formatter.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import pytest
|
||||||
|
from src.synthesis_formatter import format_takes_diff
|
||||||
|
|
||||||
|
def test_format_takes_diff_empty():
|
||||||
|
assert format_takes_diff({}) == ""
|
||||||
|
|
||||||
|
def test_format_takes_diff_single_take():
|
||||||
|
takes = {
|
||||||
|
"take1": [
|
||||||
|
{"role": "user", "content": "hello"},
|
||||||
|
{"role": "assistant", "content": "hi"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
expected = "=== Shared History ===\nuser: hello\nassistant: hi\n\n=== Variations ===\n"
|
||||||
|
assert format_takes_diff(takes) == expected
|
||||||
|
|
||||||
|
def test_format_takes_diff_common_prefix():
|
||||||
|
takes = {
|
||||||
|
"take1": [
|
||||||
|
{"role": "user", "content": "hello"},
|
||||||
|
{"role": "assistant", "content": "hi"},
|
||||||
|
{"role": "user", "content": "how are you?"},
|
||||||
|
{"role": "assistant", "content": "I am fine."}
|
||||||
|
],
|
||||||
|
"take2": [
|
||||||
|
{"role": "user", "content": "hello"},
|
||||||
|
{"role": "assistant", "content": "hi"},
|
||||||
|
{"role": "user", "content": "what is the time?"},
|
||||||
|
{"role": "assistant", "content": "It is noon."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
expected = (
|
||||||
|
"=== Shared History ===\n"
|
||||||
|
"user: hello\n"
|
||||||
|
"assistant: hi\n\n"
|
||||||
|
"=== Variations ===\n"
|
||||||
|
"[take1]\n"
|
||||||
|
"user: how are you?\n"
|
||||||
|
"assistant: I am fine.\n\n"
|
||||||
|
"[take2]\n"
|
||||||
|
"user: what is the time?\n"
|
||||||
|
"assistant: It is noon.\n"
|
||||||
|
)
|
||||||
|
assert format_takes_diff(takes) == expected
|
||||||
|
|
||||||
|
def test_format_takes_diff_no_common_prefix():
|
||||||
|
takes = {
|
||||||
|
"take1": [{"role": "user", "content": "a"}],
|
||||||
|
"take2": [{"role": "user", "content": "b"}]
|
||||||
|
}
|
||||||
|
expected = (
|
||||||
|
"=== Shared History ===\n\n"
|
||||||
|
"=== Variations ===\n"
|
||||||
|
"[take1]\n"
|
||||||
|
"user: a\n\n"
|
||||||
|
"[take2]\n"
|
||||||
|
"user: b\n"
|
||||||
|
)
|
||||||
|
assert format_takes_diff(takes) == expected
|
||||||
Reference in New Issue
Block a user