Compare commits
52 Commits
b4396697dd
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d9d8a70e8 | |||
| cc6a651664 | |||
| e567223031 | |||
| a3c8d4b153 | |||
| e600d3fdcd | |||
| 266a67dcd9 | |||
| 2b73745cd9 | |||
| 51d05c15e0 | |||
| 9ddbcd2fd6 | |||
| c205c6d97c | |||
| 2ed9867e39 | |||
| f5d4913da2 | |||
| abe1c660ea | |||
| dd520dd4db | |||
| f6fe3baaf4 | |||
| 133fd60613 | |||
| 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 | |||
| 8ee8862ae8 | |||
| 0474df5958 | |||
| cf83aeeff3 | |||
| ca7d1b074f | |||
| 038c909ce3 | |||
| 84b6266610 | |||
| c5df29b760 | |||
| 791e1b7a81 | |||
| 573f5ee5d1 | |||
| 1e223b46b0 | |||
| 93a590cdc5 |
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
description: Fast, read-only agent for exploring the codebase structure
|
description: Fast, read-only agent for exploring the codebase structure
|
||||||
mode: subagent
|
mode: subagent
|
||||||
model: MiniMax-M2.5
|
model: minimax-coding-plan/MiniMax-M2.7
|
||||||
temperature: 0.2
|
temperature: 0.2
|
||||||
permission:
|
permission:
|
||||||
edit: deny
|
edit: deny
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
description: General-purpose agent for researching complex questions and executing multi-step tasks
|
description: General-purpose agent for researching complex questions and executing multi-step tasks
|
||||||
mode: subagent
|
mode: subagent
|
||||||
model: MiniMax-M2.5
|
model: minimax-coding-plan/MiniMax-M2.7
|
||||||
temperature: 0.3
|
temperature: 0.3
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
description: Tier 1 Orchestrator for product alignment, high-level planning, and track initialization
|
description: Tier 1 Orchestrator for product alignment, high-level planning, and track initialization
|
||||||
mode: primary
|
mode: primary
|
||||||
model: MiniMax-M2.5
|
model: minimax-coding-plan/MiniMax-M2.7
|
||||||
temperature: 0.5
|
temperature: 0.5
|
||||||
permission:
|
permission:
|
||||||
edit: ask
|
edit: ask
|
||||||
@@ -18,7 +18,7 @@ ONLY output the requested text. No pleasantries.
|
|||||||
|
|
||||||
## Context Management
|
## Context Management
|
||||||
|
|
||||||
**MANUAL COMPACTION ONLY** — Never rely on automatic context summarization.
|
**MANUAL COMPACTION ONLY** <EFBFBD> Never rely on automatic context summarization.
|
||||||
Use `/compact` command explicitly when context needs reduction.
|
Use `/compact` command explicitly when context needs reduction.
|
||||||
Preserve full context during track planning and spec creation.
|
Preserve full context during track planning and spec creation.
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ Use `manual-slop_py_get_code_outline`, `manual-slop_py_get_definition`,
|
|||||||
Document existing implementations with file:line references in a
|
Document existing implementations with file:line references in a
|
||||||
"Current State Audit" section in the spec.
|
"Current State Audit" section in the spec.
|
||||||
|
|
||||||
**FAILURE TO AUDIT = TRACK FAILURE** — Previous tracks failed because specs
|
**FAILURE TO AUDIT = TRACK FAILURE** <EFBFBD> Previous tracks failed because specs
|
||||||
asked to implement features that already existed.
|
asked to implement features that already existed.
|
||||||
|
|
||||||
### 2. Identify Gaps, Not Features
|
### 2. Identify Gaps, Not Features
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
description: Tier 2 Tech Lead for architectural design and track execution with persistent memory
|
description: Tier 2 Tech Lead for architectural design and track execution with persistent memory
|
||||||
mode: primary
|
mode: primary
|
||||||
model: MiniMax-M2.5
|
model: minimax-coding-plan/MiniMax-M2.7
|
||||||
temperature: 0.4
|
temperature: 0.4
|
||||||
permission:
|
permission:
|
||||||
edit: ask
|
edit: ask
|
||||||
@@ -14,9 +14,9 @@ ONLY output the requested text. No pleasantries.
|
|||||||
|
|
||||||
## Context Management
|
## Context Management
|
||||||
|
|
||||||
**MANUAL COMPACTION ONLY** — Never rely on automatic context summarization.
|
**MANUAL COMPACTION ONLY** <EFBFBD> Never rely on automatic context summarization.
|
||||||
Use `/compact` command explicitly when context needs reduction.
|
Use `/compact` command explicitly when context needs reduction.
|
||||||
You maintain PERSISTENT MEMORY throughout track execution — do NOT apply Context Amnesia to your own session.
|
You maintain PERSISTENT MEMORY throughout track execution <EFBFBD> do NOT apply Context Amnesia to your own session.
|
||||||
|
|
||||||
## CRITICAL: MCP Tools Only (Native Tools Banned)
|
## CRITICAL: MCP Tools Only (Native Tools Banned)
|
||||||
|
|
||||||
@@ -134,14 +134,14 @@ Before implementing:
|
|||||||
- Zero-assertion ban: Tests MUST have meaningful assertions
|
- Zero-assertion ban: Tests MUST have meaningful assertions
|
||||||
- Delegate test creation to Tier 3 Worker via Task tool
|
- Delegate test creation to Tier 3 Worker via Task tool
|
||||||
- Run tests and confirm they FAIL as expected
|
- Run tests and confirm they FAIL as expected
|
||||||
- **CONFIRM FAILURE** — this is the Red phase
|
- **CONFIRM FAILURE** <EFBFBD> this is the Red phase
|
||||||
|
|
||||||
### 3. Green Phase: Implement to Pass
|
### 3. Green Phase: Implement to Pass
|
||||||
|
|
||||||
- **Pre-delegation checkpoint**: Stage current progress (`git add .`)
|
- **Pre-delegation checkpoint**: Stage current progress (`git add .`)
|
||||||
- Delegate implementation to Tier 3 Worker via Task tool
|
- Delegate implementation to Tier 3 Worker via Task tool
|
||||||
- Run tests and confirm they PASS
|
- Run tests and confirm they PASS
|
||||||
- **CONFIRM PASS** — this is the Green phase
|
- **CONFIRM PASS** <EFBFBD> this is the Green phase
|
||||||
|
|
||||||
### 4. Refactor Phase (Optional)
|
### 4. Refactor Phase (Optional)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
description: Stateless Tier 3 Worker for surgical code implementation and TDD
|
description: Stateless Tier 3 Worker for surgical code implementation and TDD
|
||||||
mode: subagent
|
mode: subagent
|
||||||
model: MiniMax-M2.5
|
model: minimax-coding-plan/minimax-m2.7
|
||||||
temperature: 0.3
|
temperature: 0.3
|
||||||
permission:
|
permission:
|
||||||
edit: allow
|
edit: allow
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
description: Stateless Tier 4 QA Agent for error analysis and diagnostics
|
description: Stateless Tier 4 QA Agent for error analysis and diagnostics
|
||||||
mode: subagent
|
mode: subagent
|
||||||
model: MiniMax-M2.5
|
model: minimax-coding-plan/MiniMax-M2.7
|
||||||
temperature: 0.2
|
temperature: 0.2
|
||||||
permission:
|
permission:
|
||||||
edit: deny
|
edit: deny
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ For deep implementation details when planning or implementing tracks, consult `d
|
|||||||
## Primary Use Cases
|
## Primary Use Cases
|
||||||
|
|
||||||
- **Full Control over Vendor APIs:** Exposing detailed API metrics and configuring deep agent capabilities directly within the GUI.
|
- **Full Control over Vendor APIs:** Exposing detailed API metrics and configuring deep agent capabilities directly within the GUI.
|
||||||
- **Context & Memory Management:** Better visualization and management of token usage and context memory. Includes granular per-file flags (**Auto-Aggregate**, **Force Full**) and a dedicated **'Context' role** for manual injections, allowing developers to optimize prompt limits with expert precision.
|
- **Context & Memory Management:** Better visualization and management of token usage and context memory. Includes granular per-file flags (**Auto-Aggregate**, **Force Full**), a dedicated **'Context' role** for manual injections, and **Context Presets** for saving and loading named file/screenshot selections. Allows assigning specific context presets to MMA agent personas for granular cognitive load isolation.
|
||||||
- **Manual "Vibe Coding" Assistant:** Serving as an auxiliary, multi-provider assistant that natively interacts with the codebase via sandboxed PowerShell scripts and MCP-like file tools, emphasizing manual developer oversight and explicit confirmation.
|
- **Manual "Vibe Coding" Assistant:** Serving as an auxiliary, multi-provider assistant that natively interacts with the codebase via sandboxed PowerShell scripts and MCP-like file tools, emphasizing manual developer oversight and explicit confirmation.
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
@@ -33,6 +33,7 @@ For deep implementation details when planning or implementing tracks, consult `d
|
|||||||
- **Track Browser:** Real-time visualization of all implementation tracks with status indicators and progress bars. Includes a dedicated **Active Track Summary** featuring a color-coded progress bar, precise ticket status breakdown (Completed, In Progress, Blocked, Todo), and dynamic **ETA estimation** based on historical completion times.
|
- **Track Browser:** Real-time visualization of all implementation tracks with status indicators and progress bars. Includes a dedicated **Active Track Summary** featuring a color-coded progress bar, precise ticket status breakdown (Completed, In Progress, Blocked, Todo), and dynamic **ETA estimation** based on historical completion times.
|
||||||
- **Visual Task DAG:** An interactive, node-based visualizer for the active track's task dependencies using `imgui-node-editor`. Features color-coded state tracking (Ready, Running, Blocked, Done), drag-and-drop dependency creation, and right-click deletion.
|
- **Visual Task DAG:** An interactive, node-based visualizer for the active track's task dependencies using `imgui-node-editor`. Features color-coded state tracking (Ready, Running, Blocked, Done), drag-and-drop dependency creation, and right-click deletion.
|
||||||
- **Strategy Visualization:** Dedicated real-time output streams for Tier 1 (Strategic Planning) and Tier 2/3 (Execution) agents, allowing the user to follow the agent's reasoning chains alongside the task DAG.
|
- **Strategy Visualization:** Dedicated real-time output streams for Tier 1 (Strategic Planning) and Tier 2/3 (Execution) agents, allowing the user to follow the agent's reasoning chains alongside the task DAG.
|
||||||
|
- **Agent-Focused Filtering:** Allows the user to focus the entire GUI (Session Hub, Discussion Hub, Comms) on a specific agent's activities and scoped context.
|
||||||
- **Track-Scoped State Management:** Segregates discussion history and task progress into per-track state files. Supports **Project-Specific Conductor Directories**, defaulting to `./conductor` relative to each project's TOML file. Projects can define their own conductor path override in `manual_slop.toml` (`[conductor].dir`) via the Projects tab for isolated track management. This prevents global context pollution and ensures the Tech Lead session is isolated to the specific track's objective.
|
- **Track-Scoped State Management:** Segregates discussion history and task progress into per-track state files. Supports **Project-Specific Conductor Directories**, defaulting to `./conductor` relative to each project's TOML file. Projects can define their own conductor path override in `manual_slop.toml` (`[conductor].dir`) via the Projects tab for isolated track management. This prevents global context pollution and ensures the Tech Lead session is isolated to the specific track's objective.
|
||||||
**Native DAG Execution Engine:** Employs a Python-based Directed Acyclic Graph (DAG) engine to manage complex task dependencies. Supports automated topological sorting, robust cycle detection, and **transitive blocking propagation** (cascading `blocked` status to downstream dependents to prevent execution stalls).
|
**Native DAG Execution Engine:** Employs a Python-based Directed Acyclic Graph (DAG) engine to manage complex task dependencies. Supports automated topological sorting, robust cycle detection, and **transitive blocking propagation** (cascading `blocked` status to downstream dependents to prevent execution stalls).
|
||||||
|
|
||||||
@@ -54,7 +55,9 @@ For deep implementation details when planning or implementing tracks, consult `d
|
|||||||
- **High-Fidelity Selectable UI:** Most read-only labels and logs across the interface (including discussion history, comms payloads, tool outputs, and telemetry metrics) are now implemented as selectable text fields. This enables standard OS-level text selection and copying (Ctrl+C) while maintaining a high-density, non-editable aesthetic.
|
- **High-Fidelity Selectable UI:** Most read-only labels and logs across the interface (including discussion history, comms payloads, tool outputs, and telemetry metrics) are now implemented as selectable text fields. This enables standard OS-level text selection and copying (Ctrl+C) while maintaining a high-density, non-editable aesthetic.
|
||||||
- **High-Fidelity UI Rendering:** Employs advanced 3x font oversampling and sub-pixel positioning to ensure crisp, high-clarity text rendering across all resolutions, enhancing readability for dense logs and complex code fragments.
|
- **High-Fidelity UI Rendering:** Employs advanced 3x font oversampling and sub-pixel positioning to ensure crisp, high-clarity text rendering across all resolutions, enhancing readability for dense logs and complex code fragments.
|
||||||
- **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.
|
||||||
- **Detailed History Management:** Rich discussion history with branching, timestamping, and specific git commit linkage per conversation.
|
- **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.
|
||||||
|
- **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.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Project Tracks
|
# Project Tracks
|
||||||
|
|
||||||
This file tracks all major tracks for the project. Each track has its own detailed plan in its respective folder.
|
This file tracks all major tracks for the project. Each track has its own detailed plan in its respective folder.
|
||||||
|
|
||||||
@@ -38,6 +38,14 @@ This file tracks all major tracks for the project. Each track has its own detail
|
|||||||
8. [x] **Track: Rich Thinking Trace Handling** - *Parse and display AI thinking/reasoning traces*
|
8. [x] **Track: Rich Thinking Trace Handling** - *Parse and display AI thinking/reasoning traces*
|
||||||
*Link: [./tracks/thinking_trace_handling_20260313/](./tracks/thinking_trace_handling_20260313/)*
|
*Link: [./tracks/thinking_trace_handling_20260313/](./tracks/thinking_trace_handling_20260313/)*
|
||||||
|
|
||||||
|
9. [ ] **Track: Smarter Aggregation with Sub-Agent Summarization**
|
||||||
|
*Link: [./tracks/aggregation_smarter_summaries_20260322/](./tracks/aggregation_smarter_summaries_20260322/)*
|
||||||
|
*Goal: Sub-agent summarization during aggregation pass, hash-based caching for file summaries, smart outline generation for code vs text files.*
|
||||||
|
|
||||||
|
10. [ ] **Track: System Context Exposure**
|
||||||
|
*Link: [./tracks/system_context_exposure_20260322/](./tracks/system_context_exposure_20260322/)*
|
||||||
|
*Goal: Expose hidden _SYSTEM_PROMPT from ai_client.py to users for customization via AI Settings.*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### GUI Overhauls & Visualizations
|
### GUI Overhauls & Visualizations
|
||||||
@@ -67,14 +75,18 @@ This file tracks all major tracks for the project. Each track has its own detail
|
|||||||
*Link: [./tracks/presets_ai_settings_ux_20260311/](./tracks/presets_ai_settings_ux_20260311/)*
|
*Link: [./tracks/presets_ai_settings_ux_20260311/](./tracks/presets_ai_settings_ux_20260311/)*
|
||||||
*Goal: Improve the layout, scaling, and control ergonomics of the Preset windows (Personas, Prompts, Tools) and AI Settings panel. Includes dual-control sliders and categorized tool management.*
|
*Goal: Improve the layout, scaling, and control ergonomics of the Preset windows (Personas, Prompts, Tools) and AI Settings panel. Includes dual-control sliders and categorized tool management.*
|
||||||
|
|
||||||
8. [ ] **Track: Session Context Snapshots & Visibility**
|
8. [x] ~~**Track: Session Context Snapshots & Visibility**~~ (Archived 2026-03-22 - Replaced by discussion_hub_panel_reorganization)
|
||||||
*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**~~ (Archived 2026-03-22 - Replaced by discussion_hub_panel_reorganization)
|
||||||
*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.*
|
||||||
|
|
||||||
|
12. [ ] **Track: Discussion Hub Panel Reorganization**
|
||||||
|
*Link: [./tracks/discussion_hub_panel_reorganization_20260322/](./tracks/discussion_hub_panel_reorganization_20260322/)*
|
||||||
|
*Goal: Properly merge Session Hub into Discussion Hub (4 tabs: Discussion | Context Composition | Snapshot | Takes), establish Files & Media as project-level inventory, deprecate ui_summary_only, implement Context Composition and DAW-style Takes.*
|
||||||
|
|
||||||
10. [ ] **Track: Undo/Redo History Support**
|
10. [ ] **Track: Undo/Redo History Support**
|
||||||
*Link: [./tracks/undo_redo_history_20260311/](./tracks/undo_redo_history_20260311/)*
|
*Link: [./tracks/undo_redo_history_20260311/](./tracks/undo_redo_history_20260311/)*
|
||||||
*Goal: Robust, non-provider based undo/redo for text inputs, UI controls, discussion mutations, and context management. Includes hotkey support and a history list view.*
|
*Goal: Robust, non-provider based undo/redo for text inputs, UI controls, discussion mutations, and context management. Includes hotkey support and a history list view.*
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "aggregation_smarter_summaries",
|
||||||
|
"created": "2026-03-22",
|
||||||
|
"status": "future",
|
||||||
|
"priority": "medium",
|
||||||
|
"affected_files": [
|
||||||
|
"src/aggregate.py",
|
||||||
|
"src/file_cache.py",
|
||||||
|
"src/ai_client.py",
|
||||||
|
"src/models.py"
|
||||||
|
],
|
||||||
|
"related_tracks": [
|
||||||
|
"discussion_hub_panel_reorganization (in_progress)",
|
||||||
|
"system_context_exposure (future)"
|
||||||
|
],
|
||||||
|
"notes": "Deferred from discussion_hub_panel_reorganization planning. Improves aggregation with sub-agent summarization and hash-based caching."
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Implementation Plan: Smarter Aggregation with Sub-Agent Summarization
|
||||||
|
|
||||||
|
## Phase 1: Hash-Based Summary Cache
|
||||||
|
Focus: Implement file hashing and cache storage
|
||||||
|
|
||||||
|
- [ ] Task: Research existing file hash implementations in codebase
|
||||||
|
- [ ] Task: Design cache storage format (file-based vs project state)
|
||||||
|
- [ ] Task: Implement hash computation for aggregation files
|
||||||
|
- [ ] Task: Implement summary cache storage and retrieval
|
||||||
|
- [ ] Task: Add cache invalidation when file content changes
|
||||||
|
- [ ] Task: Write tests for hash computation and cache
|
||||||
|
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Hash-Based Summary Cache'
|
||||||
|
|
||||||
|
## Phase 2: Sub-Agent Summarization
|
||||||
|
Focus: Implement sub-agent summarization during aggregation
|
||||||
|
|
||||||
|
- [ ] Task: Audit current aggregate.py flow
|
||||||
|
- [ ] Task: Define summarization prompt strategy for code vs text files
|
||||||
|
- [ ] Task: Implement sub-agent invocation during aggregation
|
||||||
|
- [ ] Task: Handle provider-specific differences in sub-agent calls
|
||||||
|
- [ ] Task: Write tests for sub-agent summarization
|
||||||
|
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Sub-Agent Summarization'
|
||||||
|
|
||||||
|
## Phase 3: Tiered Aggregation Strategy
|
||||||
|
Focus: Respect tier-level aggregation configuration
|
||||||
|
|
||||||
|
- [ ] Task: Audit how tiers receive context currently
|
||||||
|
- [ ] Task: Implement tier-level aggregation strategy selection
|
||||||
|
- [ ] Task: Connect tier strategy to Persona configuration
|
||||||
|
- [ ] Task: Write tests for tiered aggregation
|
||||||
|
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Tiered Aggregation Strategy'
|
||||||
|
|
||||||
|
## Phase 4: UI Integration
|
||||||
|
Focus: Expose cache status and controls in UI
|
||||||
|
|
||||||
|
- [ ] Task: Add cache status indicator to Files & Media panel
|
||||||
|
- [ ] Task: Add "Clear Summary Cache" button
|
||||||
|
- [ ] Task: Add aggregation configuration to Project Settings or AI Settings
|
||||||
|
- [ ] Task: Write tests for UI integration
|
||||||
|
- [ ] Task: Conductor - User Manual Verification 'Phase 4: UI Integration'
|
||||||
|
|
||||||
|
## Phase 5: Cache Persistence & Optimization
|
||||||
|
Focus: Ensure cache persists and is performant
|
||||||
|
|
||||||
|
- [ ] Task: Implement persistent cache storage to disk
|
||||||
|
- [ ] Task: Add cache size management (max entries, LRU)
|
||||||
|
- [ ] Task: Performance testing with large codebases
|
||||||
|
- [ ] Task: Write tests for persistence
|
||||||
|
- [ ] Task: Conductor - User Manual Verification 'Phase 5: Cache Persistence & Optimization'
|
||||||
103
conductor/tracks/aggregation_smarter_summaries_20260322/spec.md
Normal file
103
conductor/tracks/aggregation_smarter_summaries_20260322/spec.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# Specification: Smarter Aggregation with Sub-Agent Summarization
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
This track improves the context aggregation system to use sub-agent passes for intelligent summarization and hash-based caching to avoid redundant work.
|
||||||
|
|
||||||
|
**Current Problem:**
|
||||||
|
- Aggregation is a simple pass that either injects full file content or a basic skeleton
|
||||||
|
- No intelligence applied to determine what level of detail is needed
|
||||||
|
- Same files get re-summarized on every discussion start even if unchanged
|
||||||
|
|
||||||
|
**Goal:**
|
||||||
|
- Use a sub-agent during aggregation pass for high-tier agents to generate succinct summaries
|
||||||
|
- Cache summaries based on file hash - only re-summarize if file changed
|
||||||
|
- Smart outline generation for code files, summary for text files
|
||||||
|
|
||||||
|
## 2. Current State Audit
|
||||||
|
|
||||||
|
### Existing Aggregation Behavior
|
||||||
|
- `aggregate.py` handles context aggregation
|
||||||
|
- `file_cache.py` provides AST parsing and skeleton generation
|
||||||
|
- Per-file flags: `Auto-Aggregate` (summarize), `Force Full` (inject raw)
|
||||||
|
- No caching of summarization results
|
||||||
|
|
||||||
|
### Provider API Considerations
|
||||||
|
- Different providers have different prompt/caching mechanisms
|
||||||
|
- Need to verify how each provider handles system context and caching
|
||||||
|
- May need provider-specific aggregation strategies
|
||||||
|
|
||||||
|
## 3. Functional Requirements
|
||||||
|
|
||||||
|
### 3.1 Hash-Based Summary Cache
|
||||||
|
- Generate SHA256 hash of file content
|
||||||
|
- Store summaries in a cache (file-based or in project state)
|
||||||
|
- Before summarizing, check if file hash matches cached summary
|
||||||
|
- Cache invalidation when file content changes
|
||||||
|
|
||||||
|
### 3.2 Sub-Agent Summarization Pass
|
||||||
|
- During aggregation, optionally invoke sub-agent for summarization
|
||||||
|
- Sub-agent generates concise summary of file purpose and key points
|
||||||
|
- Different strategies for:
|
||||||
|
- Code files: AST-based outline + key function signatures
|
||||||
|
- Text files: Paragraph-level summary
|
||||||
|
- Config files: Key-value extraction
|
||||||
|
|
||||||
|
### 3.3 Tiered Aggregation Strategy
|
||||||
|
- Tier 3/4 workers: Get skeleton outlines (fast, cheap)
|
||||||
|
- Tier 2 (Tech Lead): Get summaries with key details
|
||||||
|
- Tier 1 (Orchestrator): May get full content or enhanced summaries
|
||||||
|
- Configurable per-agent via Persona
|
||||||
|
|
||||||
|
### 3.4 Cache Persistence
|
||||||
|
- Summaries persist across sessions
|
||||||
|
- Stored in project directory or centralized cache location
|
||||||
|
- Manual cache clear option in UI
|
||||||
|
|
||||||
|
## 4. Data Model
|
||||||
|
|
||||||
|
### 4.1 Summary Cache Entry
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"file_path": str,
|
||||||
|
"file_hash": str, # SHA256 of content
|
||||||
|
"summary": str,
|
||||||
|
"outline": str, # For code files
|
||||||
|
"generated_at": str, # ISO timestamp
|
||||||
|
"generator_tier": str, # Which tier generated it
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Aggregation Config
|
||||||
|
```toml
|
||||||
|
[aggregation]
|
||||||
|
default_mode = "summarize" # "full", "summarize", "outline"
|
||||||
|
cache_enabled = true
|
||||||
|
cache_dir = ".slop_cache"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. UI Changes
|
||||||
|
|
||||||
|
- Add "Clear Summary Cache" button in Files & Media or Context Composition
|
||||||
|
- Show cached status indicator on files (similar to AST cache indicator)
|
||||||
|
- Configuration in AI Settings or Project Settings
|
||||||
|
|
||||||
|
## 6. Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] File hash computed before summarization
|
||||||
|
- [ ] Summary cache persists across app restarts
|
||||||
|
- [ ] Sub-agent generates better summaries than basic skeleton
|
||||||
|
- [ ] Aggregation respects tier-level configuration
|
||||||
|
- [ ] Cache can be manually cleared
|
||||||
|
- [ ] Provider APIs handle aggregated context correctly
|
||||||
|
|
||||||
|
## 7. Out of Scope
|
||||||
|
- Changes to provider API internals
|
||||||
|
- Vector store / embeddings for RAG (separate track)
|
||||||
|
- Changes to Session Hub / Discussion Hub layout
|
||||||
|
|
||||||
|
## 8. Dependencies
|
||||||
|
- `aggregate.py` - main aggregation logic
|
||||||
|
- `file_cache.py` - AST parsing and caching
|
||||||
|
- `ai_client.py` - sub-agent invocation
|
||||||
|
- `models.py` - may need new config structures
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "discussion_hub_panel_reorganization",
|
||||||
|
"created": "2026-03-22",
|
||||||
|
"status": "in_progress",
|
||||||
|
"priority": "high",
|
||||||
|
"affected_files": [
|
||||||
|
"src/gui_2.py",
|
||||||
|
"src/models.py",
|
||||||
|
"src/project_manager.py",
|
||||||
|
"tests/test_gui_context_presets.py",
|
||||||
|
"tests/test_discussion_takes.py"
|
||||||
|
],
|
||||||
|
"replaces": [
|
||||||
|
"session_context_snapshots_20260311",
|
||||||
|
"discussion_takes_branching_20260311"
|
||||||
|
],
|
||||||
|
"related_tracks": [
|
||||||
|
"aggregation_smarter_summaries (future)",
|
||||||
|
"system_context_exposure (future)"
|
||||||
|
],
|
||||||
|
"notes": "These earlier tracks were marked complete but the UI panel reorganization was not properly implemented. This track consolidates and properly executes the intended UX."
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Implementation Plan: Discussion Hub Panel Reorganization
|
||||||
|
|
||||||
|
## Phase 1: Cleanup & Project Settings Rename
|
||||||
|
Focus: Remove redundant ui_summary_only, rename Context Hub, establish project-level vs discussion-level separation
|
||||||
|
|
||||||
|
- [x] Task: Audit current ui_summary_only usages and document behavior to deprecate [f6fe3ba] (embedded audit)
|
||||||
|
- [x] Task: Remove ui_summary_only checkbox from _render_projects_panel (gui_2.py) [f5d4913]
|
||||||
|
- [x] Task: Rename Context Hub to "Project Settings" in _gui_func tab bar [2ed9867]
|
||||||
|
- [ ] Task: Remove Context Presets tab from Project Settings (Context Hub)
|
||||||
|
- [ ] Task: Rename Context Hub to "Project Settings" in _gui_func tab bar
|
||||||
|
- [x] Task: Remove Context Presets tab from Project Settings (Context Hub) [9ddbcd2]
|
||||||
|
- [x] Task: Update references in show_windows dict and any help text [2ed9867] (renamed Context Hub -> Project Settings)
|
||||||
|
- [x] Task: Write tests verifying ui_summary_only removal doesn't break existing functionality [f5d4913]
|
||||||
|
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Cleanup & Project Settings Rename'
|
||||||
|
|
||||||
|
## Phase 2: Merge Session Hub into Discussion Hub [checkpoint: 2b73745]
|
||||||
|
Focus: Move Session Hub tabs into Discussion Hub, eliminate separate Session Hub window
|
||||||
|
|
||||||
|
- [x] Task: Audit Session Hub (_render_session_hub) tab content [documented above]
|
||||||
|
- [x] Task: Add Snapshot tab to Discussion Hub containing Aggregate MD + System Prompt preview [2b73745]
|
||||||
|
- [x] Task: Remove Session Hub window from _gui_func [2b73745]
|
||||||
|
- [x] Task: Add Discussion Hub tab bar structure (Discussion | Context Composition | Snapshot | Takes) [2b73745]
|
||||||
|
- [x] Task: Write tests for new tab structure rendering [2b73745]
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 2: Merge Session Hub into Discussion Hub'
|
||||||
|
|
||||||
|
## Phase 3: Context Composition Tab [checkpoint: a3c8d4b]
|
||||||
|
Focus: Per-discussion file filter with save/load preset functionality
|
||||||
|
|
||||||
|
- [x] Task: Write tests for Context Composition state management [a3c8d4b]
|
||||||
|
- [x] Task: Create _render_context_composition_panel method [a3c8d4b]
|
||||||
|
- [x] Task: Implement file/screenshot selection display (filtered from Files & Media) [a3c8d4b]
|
||||||
|
- [x] Task: Implement per-file flags display (Auto-Aggregate, Force Full) [a3c8d4b]
|
||||||
|
- [x] Task: Implement Save as Preset / Load Preset buttons [a3c8d4b]
|
||||||
|
- [x] Task: Connect Context Presets storage to this panel [a3c8d4b]
|
||||||
|
- [ ] Task: Update Persona editor to reference Context Composition presets (NOTE: already done via existing context_preset field in Persona)
|
||||||
|
- [x] Task: Write tests for Context Composition preset save/load [a3c8d4b]
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 3: Context Composition Tab'
|
||||||
|
|
||||||
|
## Phase 4: Takes Timeline Integration [checkpoint: cc6a651]
|
||||||
|
Focus: DAW-style branching with proper visual timeline and synthesis
|
||||||
|
|
||||||
|
- [x] Task: Audit existing takes data structure and synthesis_formatter [documented above]
|
||||||
|
- [ ] Task: Enhance takes data model with parent_entry and parent_take tracking (deferred - existing model sufficient)
|
||||||
|
- [x] Task: Implement Branch from Entry action in discussion history [already existed]
|
||||||
|
- [x] Task: Implement visual timeline showing take divergence [_render_takes_panel with table view]
|
||||||
|
- [x] Task: Integrate synthesis panel into Takes tab [cc6a651]
|
||||||
|
- [x] Task: Implement take selection for synthesis [cc6a651]
|
||||||
|
- [x] Task: Write tests for take branching and synthesis [cc6a651]
|
||||||
|
- [x] Task: Conductor - User Manual Verification 'Phase 4: Takes Timeline Integration'
|
||||||
|
|
||||||
|
## Phase 5: Final Integration & Cleanup
|
||||||
|
Focus: Ensure all panels work together, remove dead code
|
||||||
|
|
||||||
|
- [ ] Task: Run full test suite to verify no regressions
|
||||||
|
- [ ] Task: Remove dead code from ui_summary_only references
|
||||||
|
- [ ] Task: Update conductor/tracks.md to mark old session_context_snapshots and discussion_takes_branching as archived/replaced
|
||||||
|
- [ ] Task: Conductor - User Manual Verification 'Phase 5: Final Integration & Cleanup'
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# Specification: Discussion Hub Panel Reorganization
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
This track addresses the fragmented implementation of Session Context Snapshots and Discussion Takes & Timeline Branching tracks (2026-03-11). Those tracks were marked complete but the UI panel layout was not properly reorganized.
|
||||||
|
|
||||||
|
**Goal:** Create a coherent Discussion Hub that absorbs Session Hub functionality, establishes Files & Media as project-level file inventory, and properly implements Context Composition and DAW-style Takes branching.
|
||||||
|
|
||||||
|
## 2. Current State Audit (as of 2026-03-22)
|
||||||
|
|
||||||
|
### Already Implemented (DO NOT re-implement)
|
||||||
|
- `ui_summary_only` checkbox in Projects panel
|
||||||
|
- Session Hub as separate window with tabs: Aggregate MD | System Prompt
|
||||||
|
- Context Hub with tabs: Projects | Paths | Context Presets
|
||||||
|
- Context Presets save/load mechanism in project TOML
|
||||||
|
- `_render_synthesis_panel()` method (gui_2.py:2612-2643) - basic synthesis UI
|
||||||
|
- Takes data structure in `project['discussion']['discussions']`
|
||||||
|
- Per-file `Auto-Aggregate` and `Force Full` flags in Files & Media
|
||||||
|
|
||||||
|
### Gaps to Fill (This Track's Scope)
|
||||||
|
1. `ui_summary_only` is redundant with per-file flags - deprecate it
|
||||||
|
2. Context Hub renamed to "Project Settings" (remove Context Presets tab)
|
||||||
|
3. Session Hub merged into Discussion Hub as tabs
|
||||||
|
4. Files & Media stays separate as project-level inventory
|
||||||
|
5. Context Composition tab in Discussion Hub for per-discussion filter
|
||||||
|
6. Context Presets accessible via Context Composition (save/load filters)
|
||||||
|
7. DAW-style Takes timeline properly integrated into Discussion Hub
|
||||||
|
8. Synthesis properly integrated with Take selection
|
||||||
|
|
||||||
|
## 3. Panel Layout Target
|
||||||
|
|
||||||
|
| Panel | Location | Purpose |
|
||||||
|
|-------|----------|---------|
|
||||||
|
| **AI Settings** | Separate dockable | Provider, model, system prompts, tool presets, bias profiles |
|
||||||
|
| **Files & Media** | Separate dockable | Project-level file inventory (addressable files) |
|
||||||
|
| **Project Settings** | Context Hub → rename | Git dir, paths, project list (NO context stuff) |
|
||||||
|
| **Discussion Hub** | Main hub | All discussion-related UI (tabs below) |
|
||||||
|
| **MMA Dashboard** | Separate dockable | Multi-agent orchestration |
|
||||||
|
| **Operations Hub** | Separate dockable | Tool calls, comms history, external tools |
|
||||||
|
| **Diagnostics** | Separate dockable | Telemetry, logs |
|
||||||
|
|
||||||
|
**Discussion Hub Tabs:**
|
||||||
|
1. **Discussion** - Main conversation view (current implementation)
|
||||||
|
2. **Context Composition** - File/screenshot filter + presets (NEW)
|
||||||
|
3. **Snapshot** - Aggregate MD + System Prompt preview (moved from Session Hub)
|
||||||
|
4. **Takes** - DAW-style timeline branching + synthesis (integrated, not separate panel)
|
||||||
|
|
||||||
|
## 4. Functional Requirements
|
||||||
|
|
||||||
|
### 4.1 Deprecate ui_summary_only
|
||||||
|
- Remove `ui_summary_only` checkbox from Projects panel
|
||||||
|
- Per-file flags (`Auto-Aggregate`, `Force Full`) are the intended mechanism
|
||||||
|
- Document migration path for users
|
||||||
|
|
||||||
|
### 4.2 Rename Context Hub → Project Settings
|
||||||
|
- Context Hub tab bar: Projects | Paths
|
||||||
|
- Remove "Context Presets" tab
|
||||||
|
- All context-related functionality moves to Discussion Hub → Context Composition
|
||||||
|
|
||||||
|
### 4.3 Merge Session Hub into Discussion Hub
|
||||||
|
- Session Hub window eliminated
|
||||||
|
- Its content becomes tabs in Discussion Hub:
|
||||||
|
- **Snapshot tab**: Aggregate MD preview, System Prompt preview, "Copy" buttons
|
||||||
|
- These were previously in Session Hub
|
||||||
|
|
||||||
|
### 4.4 Context Composition Tab (NEW)
|
||||||
|
- Shows currently selected files/screenshots for THIS discussion
|
||||||
|
- Per-file flags: Auto-Aggregate, Force Full
|
||||||
|
- **"Save as Preset"** / **"Load Preset"** buttons
|
||||||
|
- Dropdown to select from saved presets
|
||||||
|
- Relationship to Files & Media:
|
||||||
|
- Files & Media = the inventory (project-level)
|
||||||
|
- Context Composition = selected filter for current discussion
|
||||||
|
|
||||||
|
### 4.5 Takes Timeline (DAW-Style)
|
||||||
|
- **New Take**: Start fresh discussion thread
|
||||||
|
- **Branch Take**: Fork from any discussion entry
|
||||||
|
- **Switch Take**: Make a take the active discussion
|
||||||
|
- **Rename/Delete Take**
|
||||||
|
- All takes share the same Files & Media (not duplicated)
|
||||||
|
- Non-destructive branching
|
||||||
|
- Visual timeline showing divergence points
|
||||||
|
|
||||||
|
### 4.6 Synthesis Integration
|
||||||
|
- User selects 2+ takes via checkboxes
|
||||||
|
- Click "Synthesize" button
|
||||||
|
- AI generates "resolved" response considering all selected approaches
|
||||||
|
- Result appears as new take
|
||||||
|
- Accessible from Discussion Hub → Takes tab
|
||||||
|
|
||||||
|
## 5. Data Model Changes
|
||||||
|
|
||||||
|
### 5.1 Discussion State Structure
|
||||||
|
```python
|
||||||
|
# Per discussion in project['discussion']['discussions']
|
||||||
|
{
|
||||||
|
"name": str,
|
||||||
|
"history": [
|
||||||
|
{"role": "user"|"assistant", "content": str, "ts": str, "files_injected": [...]}
|
||||||
|
],
|
||||||
|
"parent_entry": Optional[int], # index of parent message if branched
|
||||||
|
"parent_take": Optional[str], # name of parent take if branched
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Context Preset Format
|
||||||
|
```toml
|
||||||
|
[context_preset.my_filter]
|
||||||
|
files = ["path/to/file_a.py"]
|
||||||
|
auto_aggregate = true
|
||||||
|
force_full = false
|
||||||
|
screenshots = ["path/to/shot1.png"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Non-Functional Requirements
|
||||||
|
- All changes must not break existing tests
|
||||||
|
- New tests required for new functionality
|
||||||
|
- Follow 1-space indentation Python code style
|
||||||
|
- No comments unless explicitly requested
|
||||||
|
|
||||||
|
## 7. Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] `ui_summary_only` removed from Projects panel
|
||||||
|
- [ ] Context Hub renamed to Project Settings
|
||||||
|
- [ ] Session Hub window eliminated
|
||||||
|
- [ ] Discussion Hub has 4 tabs: Discussion, Context Composition, Snapshot, Takes
|
||||||
|
- [ ] Context Composition allows save/load of filter presets
|
||||||
|
- [ ] Takes can be branched from any entry
|
||||||
|
- [ ] Takes timeline shows divergence visually
|
||||||
|
- [ ] Synthesis works with 2+ selected takes
|
||||||
|
- [ ] All existing tests still pass
|
||||||
|
- [ ] New tests cover new functionality
|
||||||
|
|
||||||
|
## 8. Out of Scope
|
||||||
|
- Aggregation improvements (sub-agent summarization, hash-based caching) - separate future track
|
||||||
|
- System prompt exposure (`_SYSTEM_PROMPT` in ai_client.py) - separate future track
|
||||||
|
- Session sophistication (Session as container for multiple discussions) - deferred
|
||||||
@@ -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]
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
# Implementation Plan: Session Context Snapshots & Visibility
|
# Implementation Plan: Session Context Snapshots & Visibility
|
||||||
|
|
||||||
## Phase 1: Backend Support for Context Presets
|
## Phase 1: Backend Support for Context Presets
|
||||||
- [ ] Task: Write failing tests for saving, loading, and listing Context Presets in the project configuration.
|
- [x] Task: Write failing tests for saving, loading, and listing Context Presets in the project configuration. 93a590c
|
||||||
- [ ] Task: Implement Context Preset storage logic (e.g., updating TOML schemas in `project_manager.py`) to manage file/screenshot lists.
|
- [x] Task: Implement Context Preset storage logic (e.g., updating TOML schemas in `project_manager.py`) to manage file/screenshot lists. 93a590c
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Backend Support for Context Presets' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 1: Backend Support for Context Presets' (Protocol in workflow.md) 93a590c
|
||||||
|
|
||||||
## Phase 2: GUI Integration & Persona Assignment
|
## Phase 2: GUI Integration & Persona Assignment
|
||||||
- [ ] Task: Write tests for the Context Hub UI components handling preset saving and loading.
|
- [x] Task: Write tests for the Context Hub UI components handling preset saving and loading. 573f5ee
|
||||||
- [ ] Task: Implement the UI controls in the Context Hub to save current selections as a preset and load existing presets.
|
- [x] Task: Implement the UI controls in the Context Hub to save current selections as a preset and load existing presets. 573f5ee
|
||||||
- [ ] Task: Update the Persona configuration UI (`personas.py` / `gui_2.py`) to allow assigning a named Context Preset to an agent persona.
|
- [x] Task: Update the Persona configuration UI (`personas.py` / `gui_2.py`) to allow assigning a named Context Preset to an agent persona. 791e1b7
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: GUI Integration & Persona Assignment' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 2: GUI Integration & Persona Assignment' (Protocol in workflow.md) 791e1b7
|
||||||
|
|
||||||
## Phase 3: Transparent Context Visibility
|
## Phase 3: Transparent Context Visibility
|
||||||
- [ ] Task: Write tests to ensure the initial aggregate markdown, resolved system prompt, and file injection timestamps are accurately recorded in the session state.
|
- [x] Task: Write tests to ensure the initial aggregate markdown, resolved system prompt, and file injection timestamps are accurately recorded in the session state. 84b6266
|
||||||
- [ ] Task: Implement UI elements in the Session Hub to expose the aggregated markdown and the active system prompt.
|
- [x] Task: Implement UI elements in the Session Hub to expose the aggregated markdown and the active system prompt. 84b6266
|
||||||
- [ ] Task: Enhance the discussion timeline rendering in `gui_2.py` to visually indicate exactly when files and screenshots were injected into the context.
|
- [x] Task: Enhance the discussion timeline rendering in `gui_2.py` to visually indicate exactly when files and screenshots were injected into the context. 84b6266
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Transparent Context Visibility' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 3: Transparent Context Visibility' (Protocol in workflow.md) 84b6266
|
||||||
|
|
||||||
## Phase 4: Agent-Focused Session Filtering
|
## Phase 4: Agent-Focused Session Filtering
|
||||||
- [ ] Task: Write tests for the GUI state filtering logic when focusing on a specific agent's session.
|
- [x] Task: Write tests for the GUI state filtering logic when focusing on a specific agent's session. 038c909
|
||||||
- [ ] Task: Relocate the 'Focus Agent' feature from the Operations Hub to the MMA Dashboard.
|
- [x] Task: Relocate the 'Focus Agent' feature from the Operations Hub to the MMA Dashboard. 038c909
|
||||||
- [ ] Task: Implement the action to filter the Session and Discussion hubs based on the selected agent's context.
|
- [x] Task: Implement the action to filter the Session and Discussion hubs based on the selected agent's context. 038c909
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Agent-Focused Session Filtering' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 4: Agent-Focused Session Filtering' (Protocol in workflow.md) 038c909
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "system_context_exposure",
|
||||||
|
"created": "2026-03-22",
|
||||||
|
"status": "future",
|
||||||
|
"priority": "medium",
|
||||||
|
"affected_files": [
|
||||||
|
"src/ai_client.py",
|
||||||
|
"src/gui_2.py",
|
||||||
|
"src/models.py"
|
||||||
|
],
|
||||||
|
"related_tracks": [
|
||||||
|
"discussion_hub_panel_reorganization (in_progress)",
|
||||||
|
"aggregation_smarter_summaries (future)"
|
||||||
|
],
|
||||||
|
"notes": "Deferred from discussion_hub_panel_reorganization planning. The _SYSTEM_PROMPT in ai_client.py is hidden from users - this exposes it for customization."
|
||||||
|
}
|
||||||
41
conductor/tracks/system_context_exposure_20260322/plan.md
Normal file
41
conductor/tracks/system_context_exposure_20260322/plan.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Implementation Plan: System Context Exposure
|
||||||
|
|
||||||
|
## Phase 1: Backend Changes
|
||||||
|
Focus: Make _SYSTEM_PROMPT configurable
|
||||||
|
|
||||||
|
- [ ] Task: Audit ai_client.py system prompt flow
|
||||||
|
- [ ] Task: Move _SYSTEM_PROMPT to configurable storage
|
||||||
|
- [ ] Task: Implement load/save of base system prompt
|
||||||
|
- [ ] Task: Modify _get_combined_system_prompt() to use config
|
||||||
|
- [ ] Task: Write tests for configurable system prompt
|
||||||
|
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Backend Changes'
|
||||||
|
|
||||||
|
## Phase 2: UI Implementation
|
||||||
|
Focus: Add base prompt editor to AI Settings
|
||||||
|
|
||||||
|
- [ ] Task: Add UI controls to _render_system_prompts_panel
|
||||||
|
- [ ] Task: Implement checkbox for "Use Default Base"
|
||||||
|
- [ ] Task: Implement collapsible base prompt editor
|
||||||
|
- [ ] Task: Add "Reset to Default" button
|
||||||
|
- [ ] Task: Write tests for UI controls
|
||||||
|
- [ ] Task: Conductor - User Manual Verification 'Phase 2: UI Implementation'
|
||||||
|
|
||||||
|
## Phase 3: Persistence & Provider Testing
|
||||||
|
Focus: Ensure persistence and cross-provider compatibility
|
||||||
|
|
||||||
|
- [ ] Task: Verify base prompt persists across app restarts
|
||||||
|
- [ ] Task: Test with Gemini provider
|
||||||
|
- [ ] Task: Test with Anthropic provider
|
||||||
|
- [ ] Task: Test with DeepSeek provider
|
||||||
|
- [ ] Task: Test with Gemini CLI adapter
|
||||||
|
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Persistence & Provider Testing'
|
||||||
|
|
||||||
|
## Phase 4: Safety & Defaults
|
||||||
|
Focus: Ensure users can recover from bad edits
|
||||||
|
|
||||||
|
- [ ] Task: Implement confirmation dialog before saving custom base
|
||||||
|
- [ ] Task: Add validation for empty/invalid prompts
|
||||||
|
- [ ] Task: Document the base prompt purpose in UI
|
||||||
|
- [ ] Task: Add "Show Diff" between default and custom
|
||||||
|
- [ ] Task: Write tests for safety features
|
||||||
|
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Safety & Defaults'
|
||||||
120
conductor/tracks/system_context_exposure_20260322/spec.md
Normal file
120
conductor/tracks/system_context_exposure_20260322/spec.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# Specification: System Context Exposure
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
This track exposes the hidden system prompt from `ai_client.py` to users for customization.
|
||||||
|
|
||||||
|
**Current Problem:**
|
||||||
|
- `_SYSTEM_PROMPT` in `ai_client.py` (lines ~118-143) is hardcoded
|
||||||
|
- It contains foundational instructions: "You are a helpful coding assistant with access to a PowerShell tool..."
|
||||||
|
- Users can only see/appending their custom portion via `_custom_system_prompt`
|
||||||
|
- The base prompt that defines core agent capabilities is invisible
|
||||||
|
|
||||||
|
**Goal:**
|
||||||
|
- Make `_SYSTEM_PROMPT` visible and editable in the UI
|
||||||
|
- Allow users to customize the foundational agent instructions
|
||||||
|
- Maintain sensible defaults while enabling expert customization
|
||||||
|
|
||||||
|
## 2. Current State Audit
|
||||||
|
|
||||||
|
### Hidden System Prompt Location
|
||||||
|
`src/ai_client.py`:
|
||||||
|
```python
|
||||||
|
_SYSTEM_PROMPT: str = (
|
||||||
|
"You are a helpful coding assistant with access to a PowerShell tool (run_powershell) and MCP tools (file access: read_file, list_directory, search_files, get_file_summary, web access: web_search, fetch_url). "
|
||||||
|
"When calling file/directory tools, always use the 'path' parameter for the target path. "
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Related State
|
||||||
|
- `_custom_system_prompt` - user-defined append/injection
|
||||||
|
- `_get_combined_system_prompt()` - merges both
|
||||||
|
- `set_custom_system_prompt()` - setter for user portion
|
||||||
|
|
||||||
|
### UI Current State
|
||||||
|
- AI Settings → System Prompts shows global and project prompts
|
||||||
|
- These are injected as `[USER SYSTEM PROMPT]` after `_SYSTEM_PROMPT`
|
||||||
|
- But `_SYSTEM_PROMPT` itself is never shown
|
||||||
|
|
||||||
|
## 3. Functional Requirements
|
||||||
|
|
||||||
|
### 3.1 Base System Prompt Visibility
|
||||||
|
- Add "Base System Prompt" section in AI Settings
|
||||||
|
- Display current `_SYSTEM_PROMPT` content
|
||||||
|
- Allow editing with syntax highlighting (it's markdown text)
|
||||||
|
|
||||||
|
### 3.2 Default vs Custom Base
|
||||||
|
- Maintain default base prompt as reference
|
||||||
|
- User can reset to default if they mess it up
|
||||||
|
- Show diff between default and custom
|
||||||
|
|
||||||
|
### 3.3 Persistence
|
||||||
|
- Custom base prompt stored in config or project TOML
|
||||||
|
- Loaded on app start
|
||||||
|
- Applied before `_custom_system_prompt` in `_get_combined_system_prompt()`
|
||||||
|
|
||||||
|
### 3.4 Provider Considerations
|
||||||
|
- Some providers handle system prompts differently
|
||||||
|
- Verify behavior across Gemini, Anthropic, DeepSeek
|
||||||
|
- May need provider-specific base prompts
|
||||||
|
|
||||||
|
## 4. Data Model
|
||||||
|
|
||||||
|
### 4.1 Config Storage
|
||||||
|
```toml
|
||||||
|
[ai_settings]
|
||||||
|
base_system_prompt = """..."""
|
||||||
|
use_default_base = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Combined Prompt Order
|
||||||
|
1. `_SYSTEM_PROMPT` (or custom base if enabled)
|
||||||
|
2. `[USER SYSTEM PROMPT]` (from AI Settings global/project)
|
||||||
|
3. Tooling strategy (from bias engine)
|
||||||
|
|
||||||
|
## 5. UI Design
|
||||||
|
|
||||||
|
**Location:** AI Settings panel → System Prompts section
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ System Prompts ──────────────────────────────┐
|
||||||
|
│ ☑ Use Default Base System Prompt │
|
||||||
|
│ │
|
||||||
|
│ Base System Prompt (collapsed by default): │
|
||||||
|
│ ┌──────────────────────────────────────────┐ │
|
||||||
|
│ │ You are a helpful coding assistant... │ │
|
||||||
|
│ └──────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [Show Editor] [Reset to Default] │
|
||||||
|
│ │
|
||||||
|
│ Global System Prompt: │
|
||||||
|
│ ┌──────────────────────────────────────────┐ │
|
||||||
|
│ │ [current global prompt content] │ │
|
||||||
|
│ └──────────────────────────────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
When "Show Editor" clicked:
|
||||||
|
- Expand to full editor for base prompt
|
||||||
|
- Syntax highlighting for markdown
|
||||||
|
- Character count
|
||||||
|
|
||||||
|
## 6. Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] `_SYSTEM_PROMPT` visible in AI Settings
|
||||||
|
- [ ] User can edit base system prompt
|
||||||
|
- [ ] Changes persist across app restarts
|
||||||
|
- [ ] "Reset to Default" restores original
|
||||||
|
- [ ] Provider APIs receive modified prompt correctly
|
||||||
|
- [ ] No regression in agent behavior with defaults
|
||||||
|
|
||||||
|
## 7. Out of Scope
|
||||||
|
- Changes to actual agent behavior logic
|
||||||
|
- Changes to tool definitions or availability
|
||||||
|
- Changes to aggregation or context handling
|
||||||
|
|
||||||
|
## 8. Dependencies
|
||||||
|
- `ai_client.py` - `_SYSTEM_PROMPT` and `_get_combined_system_prompt()`
|
||||||
|
- `gui_2.py` - AI Settings panel rendering
|
||||||
|
- `models.py` - Config structures
|
||||||
28
config.toml
28
config.toml
@@ -5,25 +5,20 @@ 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 = [
|
||||||
"C:/projects/gencpp/gencpp_sloppy.toml",
|
"C:/projects/gencpp/.ai/gencpp_sloppy.toml",
|
||||||
"C:\\projects\\manual_slop\\tests\\artifacts\\temp_livecontextsim.toml",
|
|
||||||
"C:\\projects\\manual_slop\\tests\\artifacts\\temp_liveaisettingssim.toml",
|
|
||||||
"C:\\projects\\manual_slop\\tests\\artifacts\\temp_livetoolssim.toml",
|
|
||||||
"C:\\projects\\manual_slop\\tests\\artifacts\\temp_liveexecutionsim.toml",
|
|
||||||
"C:\\projects\\manual_slop\\tests\\artifacts\\temp_project.toml",
|
|
||||||
]
|
]
|
||||||
active = "C:/projects/gencpp/gencpp_sloppy.toml"
|
active = "C:/projects/gencpp/.ai/gencpp_sloppy.toml"
|
||||||
|
|
||||||
[gui]
|
[gui]
|
||||||
separate_message_panel = false
|
separate_message_panel = false
|
||||||
separate_response_panel = false
|
separate_response_panel = false
|
||||||
separate_tool_calls_panel = false
|
separate_tool_calls_panel = false
|
||||||
bg_shader_enabled = true
|
bg_shader_enabled = false
|
||||||
crt_filter_enabled = false
|
crt_filter_enabled = false
|
||||||
separate_task_dag = false
|
separate_task_dag = false
|
||||||
separate_usage_analytics = false
|
separate_usage_analytics = false
|
||||||
@@ -34,12 +29,12 @@ separate_tier4 = false
|
|||||||
separate_external_tools = false
|
separate_external_tools = false
|
||||||
|
|
||||||
[gui.show_windows]
|
[gui.show_windows]
|
||||||
"Context Hub" = true
|
"Project Settings" = true
|
||||||
"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
|
||||||
@@ -53,16 +48,17 @@ separate_external_tools = false
|
|||||||
Message = false
|
Message = false
|
||||||
Response = false
|
Response = false
|
||||||
"Tool Calls" = false
|
"Tool Calls" = false
|
||||||
Theme = true
|
Theme = false
|
||||||
"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 = "Nord 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
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ ViewportPos=43,95
|
|||||||
ViewportId=0x78C57832
|
ViewportId=0x78C57832
|
||||||
Size=897,649
|
Size=897,649
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000001,0
|
DockId=0x00000005,0
|
||||||
|
|
||||||
[Window][Files]
|
[Window][Files]
|
||||||
ViewportPos=3125,170
|
ViewportPos=3125,170
|
||||||
@@ -33,7 +33,7 @@ DockId=0x0000000A,0
|
|||||||
Pos=0,17
|
Pos=0,17
|
||||||
Size=1680,730
|
Size=1680,730
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000001,0
|
DockId=0x00000005,0
|
||||||
|
|
||||||
[Window][Provider]
|
[Window][Provider]
|
||||||
ViewportPos=43,95
|
ViewportPos=43,95
|
||||||
@@ -41,23 +41,23 @@ ViewportId=0x78C57832
|
|||||||
Pos=0,651
|
Pos=0,651
|
||||||
Size=897,468
|
Size=897,468
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000001,0
|
DockId=0x00000005,0
|
||||||
|
|
||||||
[Window][Message]
|
[Window][Message]
|
||||||
Pos=661,1426
|
Pos=711,694
|
||||||
Size=716,455
|
Size=716,455
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Response]
|
[Window][Response]
|
||||||
Pos=2258,1377
|
Pos=245,1014
|
||||||
Size=1102,575
|
Size=1492,948
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Tool Calls]
|
[Window][Tool Calls]
|
||||||
Pos=520,1144
|
Pos=1028,1668
|
||||||
Size=663,232
|
Size=1397,340
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000006,0
|
DockId=0x0000000E,0
|
||||||
|
|
||||||
[Window][Comms History]
|
[Window][Comms History]
|
||||||
ViewportPos=43,95
|
ViewportPos=43,95
|
||||||
@@ -74,10 +74,10 @@ Collapsed=0
|
|||||||
DockId=0xAFC85805,2
|
DockId=0xAFC85805,2
|
||||||
|
|
||||||
[Window][Theme]
|
[Window][Theme]
|
||||||
Pos=0,1423
|
Pos=0,975
|
||||||
Size=579,737
|
Size=1010,730
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,2
|
DockId=0x00000007,0
|
||||||
|
|
||||||
[Window][Text Viewer - Entry #7]
|
[Window][Text Viewer - Entry #7]
|
||||||
Pos=379,324
|
Pos=379,324
|
||||||
@@ -85,16 +85,15 @@ Size=900,700
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Diagnostics]
|
[Window][Diagnostics]
|
||||||
Pos=1649,24
|
Pos=1945,734
|
||||||
Size=580,1284
|
Size=1211,713
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000010,2
|
|
||||||
|
|
||||||
[Window][Context Hub]
|
[Window][Context Hub]
|
||||||
Pos=0,1423
|
Pos=0,975
|
||||||
Size=579,737
|
Size=1010,730
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,1
|
DockId=0x00000007,0
|
||||||
|
|
||||||
[Window][AI Settings Hub]
|
[Window][AI Settings Hub]
|
||||||
Pos=406,17
|
Pos=406,17
|
||||||
@@ -103,28 +102,28 @@ Collapsed=0
|
|||||||
DockId=0x0000000D,0
|
DockId=0x0000000D,0
|
||||||
|
|
||||||
[Window][Discussion Hub]
|
[Window][Discussion Hub]
|
||||||
Pos=2230,26
|
Pos=1126,24
|
||||||
Size=1610,2134
|
Size=1638,1608
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000013,0
|
DockId=0x00000006,0
|
||||||
|
|
||||||
[Window][Operations Hub]
|
[Window][Operations Hub]
|
||||||
Pos=581,26
|
Pos=0,24
|
||||||
Size=1647,2134
|
Size=1124,1608
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000005,0
|
DockId=0x00000005,2
|
||||||
|
|
||||||
[Window][Files & Media]
|
[Window][Files & Media]
|
||||||
Pos=0,1423
|
Pos=1126,24
|
||||||
Size=579,737
|
Size=1638,1608
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000002,0
|
DockId=0x00000006,1
|
||||||
|
|
||||||
[Window][AI Settings]
|
[Window][AI Settings]
|
||||||
Pos=0,26
|
Pos=0,24
|
||||||
Size=579,1395
|
Size=1124,1608
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000001,0
|
DockId=0x00000005,0
|
||||||
|
|
||||||
[Window][Approve Tool Execution]
|
[Window][Approve Tool Execution]
|
||||||
Pos=3,524
|
Pos=3,524
|
||||||
@@ -135,13 +134,13 @@ Collapsed=0
|
|||||||
Pos=3360,26
|
Pos=3360,26
|
||||||
Size=480,2134
|
Size=480,2134
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000010,0
|
DockId=0x00000004,0
|
||||||
|
|
||||||
[Window][Log Management]
|
[Window][Log Management]
|
||||||
Pos=3360,26
|
Pos=3360,26
|
||||||
Size=480,2134
|
Size=480,2134
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000010,0
|
DockId=0x00000004,0
|
||||||
|
|
||||||
[Window][Track Proposal]
|
[Window][Track Proposal]
|
||||||
Pos=709,326
|
Pos=709,326
|
||||||
@@ -175,8 +174,8 @@ Size=381,329
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Last Script Output]
|
[Window][Last Script Output]
|
||||||
Pos=927,1365
|
Pos=1076,794
|
||||||
Size=800,562
|
Size=1085,1154
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - Log Entry #1 (request)]
|
[Window][Text Viewer - Log Entry #1 (request)]
|
||||||
@@ -190,7 +189,7 @@ Size=1005,366
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - Entry #11]
|
[Window][Text Viewer - Entry #11]
|
||||||
Pos=60,60
|
Pos=1010,564
|
||||||
Size=1529,925
|
Size=1529,925
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
@@ -225,8 +224,8 @@ Size=900,700
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - system]
|
[Window][Text Viewer - system]
|
||||||
Pos=377,705
|
Pos=901,1502
|
||||||
Size=900,340
|
Size=876,536
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - Entry #15]
|
[Window][Text Viewer - Entry #15]
|
||||||
@@ -240,8 +239,8 @@ Size=900,700
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - tool_calls]
|
[Window][Text Viewer - tool_calls]
|
||||||
Pos=60,60
|
Pos=1106,942
|
||||||
Size=900,700
|
Size=831,482
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - Tool Script #1]
|
[Window][Text Viewer - Tool Script #1]
|
||||||
@@ -285,7 +284,7 @@ Size=900,700
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - Tool Call #1 Details]
|
[Window][Text Viewer - Tool Call #1 Details]
|
||||||
Pos=165,1081
|
Pos=963,716
|
||||||
Size=727,725
|
Size=727,725
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
@@ -330,8 +329,8 @@ Size=967,499
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Usage Analytics]
|
[Window][Usage Analytics]
|
||||||
Pos=1739,1107
|
Pos=2678,26
|
||||||
Size=586,269
|
Size=1162,2134
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x0000000F,0
|
DockId=0x0000000F,0
|
||||||
|
|
||||||
@@ -366,7 +365,7 @@ Size=900,700
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - Entry #4]
|
[Window][Text Viewer - Entry #4]
|
||||||
Pos=1247,1182
|
Pos=1165,782
|
||||||
Size=900,700
|
Size=900,700
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
@@ -376,15 +375,42 @@ Size=1593,1240
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Text Viewer - Entry #5]
|
[Window][Text Viewer - Entry #5]
|
||||||
Pos=60,60
|
Pos=989,778
|
||||||
Size=900,700
|
Size=1366,1032
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Shader Editor]
|
[Window][Shader Editor]
|
||||||
Pos=457,710
|
Pos=457,710
|
||||||
Size=493,252
|
Size=573,280
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][Text Viewer - list_directory]
|
||||||
|
Pos=1376,796
|
||||||
|
Size=882,656
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][Text Viewer - Last Output]
|
||||||
|
Pos=60,60
|
||||||
|
Size=900,700
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][Text Viewer - Entry #2]
|
||||||
|
Pos=1518,488
|
||||||
|
Size=900,700
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][Session Hub]
|
||||||
|
Pos=1163,24
|
||||||
|
Size=1234,1542
|
||||||
|
Collapsed=0
|
||||||
|
DockId=0x00000006,1
|
||||||
|
|
||||||
|
[Window][Project Settings]
|
||||||
|
Pos=0,24
|
||||||
|
Size=1124,1608
|
||||||
|
Collapsed=0
|
||||||
|
DockId=0x00000005,1
|
||||||
|
|
||||||
[Table][0xFB6E3870,4]
|
[Table][0xFB6E3870,4]
|
||||||
RefScale=13
|
RefScale=13
|
||||||
Column 0 Width=80
|
Column 0 Width=80
|
||||||
@@ -418,9 +444,9 @@ Column 4 Weight=1.0000
|
|||||||
[Table][0x2A6000B6,4]
|
[Table][0x2A6000B6,4]
|
||||||
RefScale=16
|
RefScale=16
|
||||||
Column 0 Width=48
|
Column 0 Width=48
|
||||||
Column 1 Width=68
|
Column 1 Width=67
|
||||||
Column 2 Weight=1.0000
|
Column 2 Weight=1.0000
|
||||||
Column 3 Width=120
|
Column 3 Width=243
|
||||||
|
|
||||||
[Table][0x8BCC69C7,6]
|
[Table][0x8BCC69C7,6]
|
||||||
RefScale=13
|
RefScale=13
|
||||||
@@ -432,17 +458,17 @@ Column 4 Weight=1.0000
|
|||||||
Column 5 Width=50
|
Column 5 Width=50
|
||||||
|
|
||||||
[Table][0x3751446B,4]
|
[Table][0x3751446B,4]
|
||||||
RefScale=16
|
RefScale=18
|
||||||
Column 0 Width=48
|
Column 0 Width=54
|
||||||
Column 1 Width=72
|
Column 1 Width=81
|
||||||
Column 2 Weight=1.0000
|
Column 2 Weight=1.0000
|
||||||
Column 3 Width=120
|
Column 3 Width=135
|
||||||
|
|
||||||
[Table][0x2C515046,4]
|
[Table][0x2C515046,4]
|
||||||
RefScale=16
|
RefScale=16
|
||||||
Column 0 Width=48
|
Column 0 Width=48
|
||||||
Column 1 Weight=1.0000
|
Column 1 Weight=1.0000
|
||||||
Column 2 Width=118
|
Column 2 Width=166
|
||||||
Column 3 Width=48
|
Column 3 Width=48
|
||||||
|
|
||||||
[Table][0xD99F45C5,4]
|
[Table][0xD99F45C5,4]
|
||||||
@@ -465,7 +491,7 @@ Column 2 Weight=1.0000
|
|||||||
|
|
||||||
[Table][0xA02D8C87,3]
|
[Table][0xA02D8C87,3]
|
||||||
RefScale=16
|
RefScale=16
|
||||||
Column 0 Width=180
|
Column 0 Width=179
|
||||||
Column 1 Width=120
|
Column 1 Width=120
|
||||||
Column 2 Weight=1.0000
|
Column 2 Weight=1.0000
|
||||||
|
|
||||||
@@ -480,13 +506,13 @@ Column 0 Width=150
|
|||||||
Column 1 Weight=1.0000
|
Column 1 Weight=1.0000
|
||||||
|
|
||||||
[Table][0x8D8494AB,2]
|
[Table][0x8D8494AB,2]
|
||||||
RefScale=16
|
RefScale=18
|
||||||
Column 0 Width=132
|
Column 0 Width=148
|
||||||
Column 1 Weight=1.0000
|
Column 1 Weight=1.0000
|
||||||
|
|
||||||
[Table][0x2C261E6E,2]
|
[Table][0x2C261E6E,2]
|
||||||
RefScale=16
|
RefScale=18
|
||||||
Column 0 Width=99
|
Column 0 Width=111
|
||||||
Column 1 Weight=1.0000
|
Column 1 Weight=1.0000
|
||||||
|
|
||||||
[Table][0x9CB1E6FD,2]
|
[Table][0x9CB1E6FD,2]
|
||||||
@@ -498,23 +524,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=3840,2134 Split=X
|
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,24 Size=2764,1608 Split=X
|
||||||
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=3358,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=579,858 Split=Y Selected=0x8CA2375C
|
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=1512,858 Split=X Selected=0x8CA2375C
|
||||||
DockNode ID=0x00000001 Parent=0x00000007 SizeRef=824,525 CentralNode=1 Selected=0x7BD57D6A
|
DockNode ID=0x00000005 Parent=0x00000007 SizeRef=1226,1681 CentralNode=1 Selected=0x7BD57D6A
|
||||||
DockNode ID=0x00000002 Parent=0x00000007 SizeRef=824,737 Selected=0x8CA2375C
|
DockNode ID=0x00000006 Parent=0x00000007 SizeRef=1638,1681 Selected=0x6F2B5B04
|
||||||
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=3259,858 Split=X Selected=0x418C7449
|
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1777,858 Selected=0x418C7449
|
||||||
DockNode ID=0x00000012 Parent=0x0000000E SizeRef=1647,402 Split=Y Selected=0x418C7449
|
|
||||||
DockNode ID=0x00000005 Parent=0x00000012 SizeRef=876,1749 Selected=0x418C7449
|
|
||||||
DockNode ID=0x00000006 Parent=0x00000012 SizeRef=876,362 Selected=0x1D56B311
|
|
||||||
DockNode ID=0x00000013 Parent=0x0000000E SizeRef=1610,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=480,1183 Split=Y Selected=0x3AEC3498
|
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=1162,1183 Split=X Selected=0x3AEC3498
|
||||||
DockNode ID=0x00000010 Parent=0x00000004 SizeRef=1199,1689 Selected=0x2C0206CE
|
DockNode ID=0x0000000C Parent=0x00000004 SizeRef=916,380 Selected=0x655BC6E9
|
||||||
DockNode ID=0x00000011 Parent=0x00000004 SizeRef=1199,420 Split=X Selected=0xDEB547B6
|
DockNode ID=0x0000000F Parent=0x00000004 SizeRef=281,380 Selected=0xDEB547B6
|
||||||
DockNode ID=0x0000000C Parent=0x00000011 SizeRef=916,380 Selected=0x655BC6E9
|
|
||||||
DockNode ID=0x0000000F Parent=0x00000011 SizeRef=281,380 Selected=0xDEB547B6
|
|
||||||
|
|
||||||
;;;<<<Layout_655921752_Default>>>;;;
|
;;;<<<Layout_655921752_Default>>>;;;
|
||||||
;;;<<<HelloImGui_Misc>>>;;;
|
;;;<<<HelloImGui_Misc>>>;;;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -71,5 +71,6 @@
|
|||||||
"logs/**",
|
"logs/**",
|
||||||
"*.log"
|
"*.log"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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-12T20:34:43"
|
last_updated = "2026-03-22T12:59:02"
|
||||||
history = []
|
history = []
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ class HookHandler(BaseHTTPRequestHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
files = _get_app_attr(app, "files", [])
|
files = _get_app_attr(app, "files", [])
|
||||||
screenshots = _get_app_attr(app, "screenshots", [])
|
screenshots = _get_app_attr(app, "screenshots", [])
|
||||||
self.wfile.write(json.dumps({"files": files, "screenshots": screenshots}).encode("utf-8"))
|
self.wfile.write(json.dumps({"files": _serialize_for_api(files), "screenshots": _serialize_for_api(screenshots)}).encode("utf-8"))
|
||||||
elif self.path == "/api/metrics/financial":
|
elif self.path == "/api/metrics/financial":
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-Type", "application/json")
|
self.send_header("Content-Type", "application/json")
|
||||||
|
|||||||
@@ -230,7 +230,6 @@ class AppController:
|
|||||||
self.ui_project_system_prompt: str = ""
|
self.ui_project_system_prompt: str = ""
|
||||||
self.ui_gemini_cli_path: str = "gemini"
|
self.ui_gemini_cli_path: str = "gemini"
|
||||||
self.ui_word_wrap: bool = True
|
self.ui_word_wrap: bool = True
|
||||||
self.ui_summary_only: bool = False
|
|
||||||
self.ui_auto_add_history: bool = False
|
self.ui_auto_add_history: bool = False
|
||||||
self.ui_active_tool_preset: str | None = None
|
self.ui_active_tool_preset: str | None = None
|
||||||
self.ui_global_system_prompt: str = ""
|
self.ui_global_system_prompt: str = ""
|
||||||
@@ -243,6 +242,8 @@ class AppController:
|
|||||||
self.ai_status: str = 'idle'
|
self.ai_status: str = 'idle'
|
||||||
self.ai_response: str = ''
|
self.ai_response: str = ''
|
||||||
self.last_md: str = ''
|
self.last_md: str = ''
|
||||||
|
self.last_aggregate_markdown: str = ''
|
||||||
|
self.last_resolved_system_prompt: str = ''
|
||||||
self.last_md_path: Optional[Path] = None
|
self.last_md_path: Optional[Path] = None
|
||||||
self.last_file_items: List[Any] = []
|
self.last_file_items: List[Any] = []
|
||||||
self.send_thread: Optional[threading.Thread] = None
|
self.send_thread: Optional[threading.Thread] = None
|
||||||
@@ -910,7 +911,6 @@ class AppController:
|
|||||||
self.ui_gemini_cli_path = self.project.get("gemini_cli", {}).get("binary_path", "gemini")
|
self.ui_gemini_cli_path = self.project.get("gemini_cli", {}).get("binary_path", "gemini")
|
||||||
self._update_gcli_adapter(self.ui_gemini_cli_path)
|
self._update_gcli_adapter(self.ui_gemini_cli_path)
|
||||||
self.ui_word_wrap = proj_meta.get("word_wrap", True)
|
self.ui_word_wrap = proj_meta.get("word_wrap", True)
|
||||||
self.ui_summary_only = proj_meta.get("summary_only", False)
|
|
||||||
self.ui_auto_add_history = disc_sec.get("auto_add", False)
|
self.ui_auto_add_history = disc_sec.get("auto_add", False)
|
||||||
self.ui_global_system_prompt = self.config.get("ai", {}).get("system_prompt", "")
|
self.ui_global_system_prompt = self.config.get("ai", {}).get("system_prompt", "")
|
||||||
|
|
||||||
@@ -950,7 +950,7 @@ class AppController:
|
|||||||
bg_shader.get_bg().enabled = gui_cfg.get("bg_shader_enabled", False)
|
bg_shader.get_bg().enabled = gui_cfg.get("bg_shader_enabled", False)
|
||||||
|
|
||||||
_default_windows = {
|
_default_windows = {
|
||||||
"Context Hub": True,
|
"Project Settings": True,
|
||||||
"Files & Media": True,
|
"Files & Media": True,
|
||||||
"AI Settings": True,
|
"AI Settings": True,
|
||||||
"MMA Dashboard": True,
|
"MMA Dashboard": True,
|
||||||
@@ -2004,7 +2004,6 @@ class AppController:
|
|||||||
self.ui_auto_scroll_comms = proj.get("project", {}).get("auto_scroll_comms", True)
|
self.ui_auto_scroll_comms = proj.get("project", {}).get("auto_scroll_comms", True)
|
||||||
self.ui_auto_scroll_tool_calls = proj.get("project", {}).get("auto_scroll_tool_calls", True)
|
self.ui_auto_scroll_tool_calls = proj.get("project", {}).get("auto_scroll_tool_calls", True)
|
||||||
self.ui_word_wrap = proj.get("project", {}).get("word_wrap", True)
|
self.ui_word_wrap = proj.get("project", {}).get("word_wrap", True)
|
||||||
self.ui_summary_only = proj.get("project", {}).get("summary_only", False)
|
|
||||||
agent_tools_cfg = proj.get("agent", {}).get("tools", {})
|
agent_tools_cfg = proj.get("agent", {}).get("tools", {})
|
||||||
self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in models.AGENT_TOOL_NAMES}
|
self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in models.AGENT_TOOL_NAMES}
|
||||||
# MMA Tracks
|
# MMA Tracks
|
||||||
@@ -2189,6 +2188,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", {})
|
||||||
@@ -2442,7 +2455,6 @@ class AppController:
|
|||||||
proj["project"]["main_context"] = self.ui_project_main_context
|
proj["project"]["main_context"] = self.ui_project_main_context
|
||||||
proj["project"]["active_preset"] = self.ui_project_preset_name
|
proj["project"]["active_preset"] = self.ui_project_preset_name
|
||||||
proj["project"]["word_wrap"] = self.ui_word_wrap
|
proj["project"]["word_wrap"] = self.ui_word_wrap
|
||||||
proj["project"]["summary_only"] = self.ui_summary_only
|
|
||||||
proj["project"]["auto_scroll_comms"] = self.ui_auto_scroll_comms
|
proj["project"]["auto_scroll_comms"] = self.ui_auto_scroll_comms
|
||||||
proj["project"]["auto_scroll_tool_calls"] = self.ui_auto_scroll_tool_calls
|
proj["project"]["auto_scroll_tool_calls"] = self.ui_auto_scroll_tool_calls
|
||||||
proj.setdefault("gemini_cli", {})["binary_path"] = self.ui_gemini_cli_path
|
proj.setdefault("gemini_cli", {})["binary_path"] = self.ui_gemini_cli_path
|
||||||
@@ -2516,6 +2528,11 @@ class AppController:
|
|||||||
# Build discussion history text separately
|
# Build discussion history text separately
|
||||||
history = flat.get("discussion", {}).get("history", [])
|
history = flat.get("discussion", {}).get("history", [])
|
||||||
discussion_text = aggregate.build_discussion_text(history)
|
discussion_text = aggregate.build_discussion_text(history)
|
||||||
|
|
||||||
|
csp = filter(bool, [self.ui_global_system_prompt.strip(), self.ui_project_system_prompt.strip()])
|
||||||
|
self.last_resolved_system_prompt = "\n\n".join(csp)
|
||||||
|
self.last_aggregate_markdown = full_md
|
||||||
|
|
||||||
return full_md, path, file_items, stable_md, discussion_text
|
return full_md, path, file_items, stable_md, discussion_text
|
||||||
|
|
||||||
def _cb_plan_epic(self) -> None:
|
def _cb_plan_epic(self) -> None:
|
||||||
|
|||||||
@@ -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]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
536
src/gui_2.py
536
src/gui_2.py
@@ -26,6 +26,7 @@ from src import log_pruner
|
|||||||
from src import models
|
from src import models
|
||||||
from src import app_controller
|
from src import app_controller
|
||||||
from src import mcp_client
|
from src import mcp_client
|
||||||
|
from src import aggregate
|
||||||
from src import markdown_helper
|
from src import markdown_helper
|
||||||
from src import bg_shader
|
from src import bg_shader
|
||||||
from src import thinking_parser
|
from src import thinking_parser
|
||||||
@@ -108,6 +109,16 @@ class App:
|
|||||||
self.show_windows.setdefault("Diagnostics", False)
|
self.show_windows.setdefault("Diagnostics", False)
|
||||||
self.controller.start_services(self)
|
self.controller.start_services(self)
|
||||||
self.controller._predefined_callbacks['_render_text_viewer'] = self._render_text_viewer
|
self.controller._predefined_callbacks['_render_text_viewer'] = self._render_text_viewer
|
||||||
|
self.controller._predefined_callbacks['save_context_preset'] = self.save_context_preset
|
||||||
|
self.controller._predefined_callbacks['load_context_preset'] = self.load_context_preset
|
||||||
|
self.controller._predefined_callbacks['set_ui_file_paths'] = lambda p: setattr(self, 'ui_file_paths', p)
|
||||||
|
self.controller._predefined_callbacks['set_ui_screenshot_paths'] = lambda p: setattr(self, 'ui_screenshot_paths', p)
|
||||||
|
def simulate_save_preset(name: str):
|
||||||
|
from src import models
|
||||||
|
self.files = [models.FileItem(path='test.py')]
|
||||||
|
self.screenshots = ['test.png']
|
||||||
|
self.save_context_preset(name)
|
||||||
|
self.controller._predefined_callbacks['simulate_save_preset'] = simulate_save_preset
|
||||||
self.show_preset_manager_window = False
|
self.show_preset_manager_window = False
|
||||||
self.show_tool_preset_manager_window = False
|
self.show_tool_preset_manager_window = False
|
||||||
self.show_persona_editor_window = False
|
self.show_persona_editor_window = False
|
||||||
@@ -119,6 +130,7 @@ class App:
|
|||||||
self._text_viewer_editor: Optional[ced.TextEditor] = None
|
self._text_viewer_editor: Optional[ced.TextEditor] = None
|
||||||
self.ui_active_tool_preset = ""
|
self.ui_active_tool_preset = ""
|
||||||
self.ui_active_bias_profile = ""
|
self.ui_active_bias_profile = ""
|
||||||
|
self.ui_active_context_preset = ""
|
||||||
self.ui_active_persona = ""
|
self.ui_active_persona = ""
|
||||||
self._editing_persona_name = ""
|
self._editing_persona_name = ""
|
||||||
self._editing_persona_description = ""
|
self._editing_persona_description = ""
|
||||||
@@ -130,6 +142,7 @@ class App:
|
|||||||
self._editing_persona_max_tokens = 4096
|
self._editing_persona_max_tokens = 4096
|
||||||
self._editing_persona_tool_preset_id = ""
|
self._editing_persona_tool_preset_id = ""
|
||||||
self._editing_persona_bias_profile_id = ""
|
self._editing_persona_bias_profile_id = ""
|
||||||
|
self._editing_persona_context_preset_id = ""
|
||||||
self._editing_persona_preferred_models_list: list[dict] = []
|
self._editing_persona_preferred_models_list: list[dict] = []
|
||||||
self._editing_persona_scope = "project"
|
self._editing_persona_scope = "project"
|
||||||
self._editing_persona_is_new = True
|
self._editing_persona_is_new = True
|
||||||
@@ -221,8 +234,9 @@ class App:
|
|||||||
self.ui_tool_filter_category = "All"
|
self.ui_tool_filter_category = "All"
|
||||||
self.ui_discussion_split_h = 300.0
|
self.ui_discussion_split_h = 300.0
|
||||||
self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8}
|
self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8}
|
||||||
|
self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8}
|
||||||
def _handle_approve_tool(self, user_data=None) -> None:
|
self.ui_new_context_preset_name = ""
|
||||||
|
self._focus_md_cache: dict[str, str] = {}
|
||||||
"""UI-level wrapper for approving a pending tool execution ask."""
|
"""UI-level wrapper for approving a pending tool execution ask."""
|
||||||
self._handle_approve_ask()
|
self._handle_approve_ask()
|
||||||
|
|
||||||
@@ -280,6 +294,54 @@ class App:
|
|||||||
pass
|
pass
|
||||||
self.controller.shutdown()
|
self.controller.shutdown()
|
||||||
|
|
||||||
|
def save_context_preset(self, name: str) -> None:
|
||||||
|
sys.stderr.write(f"[DEBUG] save_context_preset called with: {name}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
if 'context_presets' not in self.controller.project:
|
||||||
|
self.controller.project['context_presets'] = {}
|
||||||
|
self.controller.project['context_presets'][name] = {
|
||||||
|
'files': [f.to_dict() if hasattr(f, 'to_dict') else {'path': str(f)} for f in self.files],
|
||||||
|
'screenshots': list(self.screenshots)
|
||||||
|
}
|
||||||
|
self.controller._save_active_project()
|
||||||
|
sys.stderr.write(f"[DEBUG] save_context_preset finished. Project keys: {list(self.controller.project.keys())}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
def load_context_preset(self, name: str) -> None:
|
||||||
|
presets = self.controller.project.get('context_presets', {})
|
||||||
|
if name in presets:
|
||||||
|
preset = presets[name]
|
||||||
|
self.files = [models.FileItem.from_dict(f) if isinstance(f, dict) else models.FileItem(path=str(f)) for f in preset.get('files', [])]
|
||||||
|
self.screenshots = list(preset.get('screenshots', []))
|
||||||
|
|
||||||
|
def delete_context_preset(self, name: str) -> None:
|
||||||
|
if 'context_presets' in self.controller.project:
|
||||||
|
self.controller.project['context_presets'].pop(name, None)
|
||||||
|
self.controller._save_active_project()
|
||||||
|
@property
|
||||||
|
def ui_file_paths(self) -> list[str]:
|
||||||
|
return [f.path if hasattr(f, 'path') else str(f) for f in self.files]
|
||||||
|
|
||||||
|
@ui_file_paths.setter
|
||||||
|
def ui_file_paths(self, paths: list[str]) -> None:
|
||||||
|
old_files = {f.path: f for f in self.files if hasattr(f, 'path')}
|
||||||
|
new_files = []
|
||||||
|
now = time.time()
|
||||||
|
for p in paths:
|
||||||
|
if p in old_files:
|
||||||
|
new_files.append(old_files[p])
|
||||||
|
else:
|
||||||
|
new_files.append(models.FileItem(path=p, injected_at=now))
|
||||||
|
self.files = new_files
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ui_screenshot_paths(self) -> list[str]:
|
||||||
|
return self.screenshots
|
||||||
|
|
||||||
|
@ui_screenshot_paths.setter
|
||||||
|
def ui_screenshot_paths(self, paths: list[str]) -> None:
|
||||||
|
self.screenshots = paths
|
||||||
|
|
||||||
def _test_callback_func_write_to_file(self, data: str) -> None:
|
def _test_callback_func_write_to_file(self, data: str) -> None:
|
||||||
"""A dummy function that a custom_callback would execute for testing."""
|
"""A dummy function that a custom_callback would execute for testing."""
|
||||||
# Ensure the directory exists if running from a different cwd
|
# Ensure the directory exists if running from a different cwd
|
||||||
@@ -576,9 +638,9 @@ class App:
|
|||||||
self._tool_log_cache = log_raw
|
self._tool_log_cache = log_raw
|
||||||
self._tool_log_dirty = False
|
self._tool_log_dirty = False
|
||||||
|
|
||||||
if self.show_windows.get("Context Hub", False):
|
if self.show_windows.get("Project Settings", False):
|
||||||
exp, opened = imgui.begin("Context Hub", self.show_windows["Context Hub"])
|
exp, opened = imgui.begin("Project Settings", self.show_windows["Project Settings"])
|
||||||
self.show_windows["Context Hub"] = bool(opened)
|
self.show_windows["Project Settings"] = bool(opened)
|
||||||
if exp:
|
if exp:
|
||||||
if imgui.begin_tab_bar('context_hub_tabs'):
|
if imgui.begin_tab_bar('context_hub_tabs'):
|
||||||
if imgui.begin_tab_item('Projects')[0]:
|
if imgui.begin_tab_item('Projects')[0]:
|
||||||
@@ -661,69 +723,24 @@ class App:
|
|||||||
exp, opened = imgui.begin("Discussion Hub", self.show_windows["Discussion Hub"])
|
exp, opened = imgui.begin("Discussion Hub", self.show_windows["Discussion Hub"])
|
||||||
self.show_windows["Discussion Hub"] = bool(opened)
|
self.show_windows["Discussion Hub"] = bool(opened)
|
||||||
if exp:
|
if exp:
|
||||||
# Top part for the history
|
if imgui.begin_tab_bar("discussion_hub_tabs"):
|
||||||
imgui.begin_child("HistoryChild", size=(0, -self.ui_discussion_split_h))
|
if imgui.begin_tab_item("Discussion")[0]:
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_discussion_panel")
|
self._render_discussion_tab()
|
||||||
self._render_discussion_panel()
|
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_discussion_panel")
|
|
||||||
imgui.end_child()
|
|
||||||
# Splitter
|
|
||||||
imgui.button("###discussion_splitter", imgui.ImVec2(-1, 4))
|
|
||||||
if imgui.is_item_active():
|
|
||||||
self.ui_discussion_split_h = max(150.0, min(imgui.get_window_height() - 150.0, self.ui_discussion_split_h - imgui.get_io().mouse_delta.y))
|
|
||||||
# Bottom part with tabs for message and response
|
|
||||||
# Detach controls
|
|
||||||
imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4))
|
|
||||||
ch1, self.ui_separate_message_panel = imgui.checkbox("Pop Out Message", self.ui_separate_message_panel)
|
|
||||||
imgui.same_line()
|
|
||||||
ch2, self.ui_separate_response_panel = imgui.checkbox("Pop Out Response", self.ui_separate_response_panel)
|
|
||||||
if ch1: self.show_windows["Message"] = self.ui_separate_message_panel
|
|
||||||
if ch2: self.show_windows["Response"] = self.ui_separate_response_panel
|
|
||||||
imgui.pop_style_var()
|
|
||||||
|
|
||||||
show_message_tab = not self.ui_separate_message_panel
|
|
||||||
show_response_tab = not self.ui_separate_response_panel
|
|
||||||
|
|
||||||
if show_message_tab or show_response_tab:
|
|
||||||
if imgui.begin_tab_bar("discussion_tabs"):
|
|
||||||
# Task: Auto-focus Response tab when response received
|
|
||||||
tab_flags = imgui.TabItemFlags_.none
|
|
||||||
if self._autofocus_response_tab:
|
|
||||||
tab_flags = imgui.TabItemFlags_.set_selected
|
|
||||||
self._autofocus_response_tab = False
|
|
||||||
self.controller._autofocus_response_tab = False
|
|
||||||
|
|
||||||
if show_message_tab:
|
|
||||||
if imgui.begin_tab_item("Message", None)[0]:
|
|
||||||
self._render_message_panel()
|
|
||||||
imgui.end_tab_item()
|
imgui.end_tab_item()
|
||||||
if show_response_tab:
|
if imgui.begin_tab_item("Context Composition")[0]:
|
||||||
if imgui.begin_tab_item("Response", None, tab_flags)[0]:
|
self._render_context_composition_panel()
|
||||||
self._render_response_panel()
|
imgui.end_tab_item()
|
||||||
|
if imgui.begin_tab_item("Snapshot")[0]:
|
||||||
|
self._render_snapshot_tab()
|
||||||
|
imgui.end_tab_item()
|
||||||
|
if imgui.begin_tab_item("Takes")[0]:
|
||||||
|
self._render_takes_panel()
|
||||||
imgui.end_tab_item()
|
imgui.end_tab_item()
|
||||||
imgui.end_tab_bar()
|
imgui.end_tab_bar()
|
||||||
else:
|
|
||||||
imgui.text_disabled("Message & Response panels are detached.")
|
|
||||||
|
|
||||||
imgui.end()
|
imgui.end()
|
||||||
if self.show_windows.get("Operations Hub", False):
|
if self.show_windows.get("Operations Hub", False):
|
||||||
exp, opened = imgui.begin("Operations Hub", self.show_windows["Operations Hub"])
|
exp, opened = imgui.begin("Operations Hub", self.show_windows["Operations Hub"])
|
||||||
self.show_windows["Operations Hub"] = bool(opened)
|
self.show_windows["Operations Hub"] = bool(opened)
|
||||||
if exp:
|
|
||||||
imgui.text("Focus Agent:")
|
|
||||||
imgui.same_line()
|
|
||||||
focus_label = self.ui_focus_agent or "All"
|
|
||||||
if imgui.begin_combo("##focus_agent", focus_label, imgui.ComboFlags_.width_fit_preview):
|
|
||||||
if imgui.selectable("All", self.ui_focus_agent is None)[0]:
|
|
||||||
self.ui_focus_agent = None
|
|
||||||
for tier in ["Tier 2", "Tier 3", "Tier 4"]:
|
|
||||||
if imgui.selectable(tier, self.ui_focus_agent == tier)[0]:
|
|
||||||
self.ui_focus_agent = tier
|
|
||||||
imgui.end_combo()
|
|
||||||
imgui.same_line()
|
|
||||||
if self.ui_focus_agent:
|
|
||||||
if imgui.button("x##clear_focus"):
|
|
||||||
self.ui_focus_agent = None
|
|
||||||
if exp:
|
if exp:
|
||||||
imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4))
|
imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4))
|
||||||
ch1, self.ui_separate_tool_calls_panel = imgui.checkbox("Pop Out Tool Calls", self.ui_separate_tool_calls_panel)
|
ch1, self.ui_separate_tool_calls_panel = imgui.checkbox("Pop Out Tool Calls", self.ui_separate_tool_calls_panel)
|
||||||
@@ -1420,6 +1437,7 @@ class App:
|
|||||||
if imgui.button("New Persona", imgui.ImVec2(-1, 0)):
|
if imgui.button("New Persona", imgui.ImVec2(-1, 0)):
|
||||||
self._editing_persona_name = ""; self._editing_persona_system_prompt = ""
|
self._editing_persona_name = ""; self._editing_persona_system_prompt = ""
|
||||||
self._editing_persona_tool_preset_id = ""; self._editing_persona_bias_profile_id = ""
|
self._editing_persona_tool_preset_id = ""; self._editing_persona_bias_profile_id = ""
|
||||||
|
self._editing_persona_context_preset_id = ""
|
||||||
self._editing_persona_preferred_models_list = [{"provider": self.current_provider, "model": self.current_model, "temperature": 0.7, "top_p": 1.0, "max_output_tokens": 4096, "history_trunc_limit": 900000}]
|
self._editing_persona_preferred_models_list = [{"provider": self.current_provider, "model": self.current_model, "temperature": 0.7, "top_p": 1.0, "max_output_tokens": 4096, "history_trunc_limit": 900000}]
|
||||||
self._editing_persona_scope = "project"; self._editing_persona_is_new = True
|
self._editing_persona_scope = "project"; self._editing_persona_is_new = True
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
@@ -1428,6 +1446,7 @@ class App:
|
|||||||
if name and imgui.selectable(f"{name}##p_list", name == self._editing_persona_name and not getattr(self, '_editing_persona_is_new', False))[0]:
|
if name and imgui.selectable(f"{name}##p_list", name == self._editing_persona_name and not getattr(self, '_editing_persona_is_new', False))[0]:
|
||||||
p = personas[name]; self._editing_persona_name = p.name; self._editing_persona_system_prompt = p.system_prompt or ""
|
p = personas[name]; self._editing_persona_name = p.name; self._editing_persona_system_prompt = p.system_prompt or ""
|
||||||
self._editing_persona_tool_preset_id = p.tool_preset or ""; self._editing_persona_bias_profile_id = p.bias_profile or ""
|
self._editing_persona_tool_preset_id = p.tool_preset or ""; self._editing_persona_bias_profile_id = p.bias_profile or ""
|
||||||
|
self._editing_persona_context_preset_id = getattr(p, 'context_preset', '') or ""
|
||||||
import copy; self._editing_persona_preferred_models_list = copy.deepcopy(p.preferred_models) if p.preferred_models else []
|
import copy; self._editing_persona_preferred_models_list = copy.deepcopy(p.preferred_models) if p.preferred_models else []
|
||||||
self._editing_persona_scope = self.controller.persona_manager.get_persona_scope(p.name); self._editing_persona_is_new = False
|
self._editing_persona_scope = self.controller.persona_manager.get_persona_scope(p.name); self._editing_persona_is_new = False
|
||||||
imgui.end_child()
|
imgui.end_child()
|
||||||
@@ -1513,6 +1532,10 @@ class App:
|
|||||||
imgui.table_next_column(); imgui.text("Bias Profile:"); bn = ["None"] + sorted(self.controller.bias_profiles.keys())
|
imgui.table_next_column(); imgui.text("Bias Profile:"); bn = ["None"] + sorted(self.controller.bias_profiles.keys())
|
||||||
b_idx = bn.index(self._editing_persona_bias_profile_id) if getattr(self, '_editing_persona_bias_profile_id', '') in bn else 0
|
b_idx = bn.index(self._editing_persona_bias_profile_id) if getattr(self, '_editing_persona_bias_profile_id', '') in bn else 0
|
||||||
imgui.set_next_item_width(-1); _, b_idx = imgui.combo("##pbp", b_idx, bn); self._editing_persona_bias_profile_id = bn[b_idx] if b_idx > 0 else ""
|
imgui.set_next_item_width(-1); _, b_idx = imgui.combo("##pbp", b_idx, bn); self._editing_persona_bias_profile_id = bn[b_idx] if b_idx > 0 else ""
|
||||||
|
imgui.table_next_row()
|
||||||
|
imgui.table_next_column(); imgui.text("Context Preset:"); cn = ["None"] + sorted(self.controller.project.get("context_presets", {}).keys())
|
||||||
|
c_idx = cn.index(self._editing_persona_context_preset_id) if getattr(self, '_editing_persona_context_preset_id', '') in cn else 0
|
||||||
|
imgui.set_next_item_width(-1); _, c_idx = imgui.combo("##pcp", c_idx, cn); self._editing_persona_context_preset_id = cn[c_idx] if c_idx > 0 else ""
|
||||||
imgui.end_table()
|
imgui.end_table()
|
||||||
|
|
||||||
if imgui.button("Manage Tools & Biases", imgui.ImVec2(-1, 0)): self.show_tool_preset_manager_window = True
|
if imgui.button("Manage Tools & Biases", imgui.ImVec2(-1, 0)): self.show_tool_preset_manager_window = True
|
||||||
@@ -1540,7 +1563,7 @@ class App:
|
|||||||
if imgui.button("Save##pers", imgui.ImVec2(100, 0)):
|
if imgui.button("Save##pers", imgui.ImVec2(100, 0)):
|
||||||
if self._editing_persona_name.strip():
|
if self._editing_persona_name.strip():
|
||||||
try:
|
try:
|
||||||
import copy; persona = models.Persona(name=self._editing_persona_name.strip(), system_prompt=self._editing_persona_system_prompt, tool_preset=self._editing_persona_tool_preset_id or None, bias_profile=self._editing_persona_bias_profile_id or None, preferred_models=copy.deepcopy(self._editing_persona_preferred_models_list))
|
import copy; persona = models.Persona(name=self._editing_persona_name.strip(), system_prompt=self._editing_persona_system_prompt, tool_preset=self._editing_persona_tool_preset_id or None, bias_profile=self._editing_persona_bias_profile_id or None, context_preset=self._editing_persona_context_preset_id or None, preferred_models=copy.deepcopy(self._editing_persona_preferred_models_list))
|
||||||
self.controller._cb_save_persona(persona, getattr(self, '_editing_persona_scope', 'project')); self.ai_status = f"Saved: {persona.name}"
|
self.controller._cb_save_persona(persona, getattr(self, '_editing_persona_scope', 'project')); self.ai_status = f"Saved: {persona.name}"
|
||||||
except Exception as e: self.ai_status = f"Error: {e}"
|
except Exception as e: self.ai_status = f"Error: {e}"
|
||||||
else: self.ai_status = "Name required"
|
else: self.ai_status = "Name required"
|
||||||
@@ -1643,7 +1666,6 @@ class App:
|
|||||||
models.save_config(self.config)
|
models.save_config(self.config)
|
||||||
self.ai_status = "config saved"
|
self.ai_status = "config saved"
|
||||||
ch, self.ui_word_wrap = imgui.checkbox("Word-Wrap (Read-only panels)", self.ui_word_wrap)
|
ch, self.ui_word_wrap = imgui.checkbox("Word-Wrap (Read-only panels)", self.ui_word_wrap)
|
||||||
ch, self.ui_summary_only = imgui.checkbox("Summary Only (send file structure, not full content)", self.ui_summary_only)
|
|
||||||
ch, self.ui_auto_scroll_comms = imgui.checkbox("Auto-scroll Comms History", self.ui_auto_scroll_comms)
|
ch, self.ui_auto_scroll_comms = imgui.checkbox("Auto-scroll Comms History", self.ui_auto_scroll_comms)
|
||||||
ch, self.ui_auto_scroll_tool_calls = imgui.checkbox("Auto-scroll Tool History", self.ui_auto_scroll_tool_calls)
|
ch, self.ui_auto_scroll_tool_calls = imgui.checkbox("Auto-scroll Tool History", self.ui_auto_scroll_tool_calls)
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_projects_panel")
|
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_projects_panel")
|
||||||
@@ -1698,6 +1720,30 @@ class App:
|
|||||||
self.ai_status = "paths reset to defaults"
|
self.ai_status = "paths reset to defaults"
|
||||||
|
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_paths_panel")
|
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_paths_panel")
|
||||||
|
|
||||||
|
def _render_context_presets_panel(self) -> None:
|
||||||
|
imgui.text_colored(C_IN, "Context Presets")
|
||||||
|
imgui.separator()
|
||||||
|
changed, new_name = imgui.input_text("Preset Name##new_ctx", self.ui_new_context_preset_name)
|
||||||
|
if changed: self.ui_new_context_preset_name = new_name
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button("Save Current"):
|
||||||
|
if self.ui_new_context_preset_name.strip():
|
||||||
|
self.save_context_preset(self.ui_new_context_preset_name.strip())
|
||||||
|
|
||||||
|
imgui.separator()
|
||||||
|
presets = self.controller.project.get('context_presets', {})
|
||||||
|
for name in sorted(presets.keys()):
|
||||||
|
preset = presets[name]
|
||||||
|
n_files = len(preset.get('files', []))
|
||||||
|
n_shots = len(preset.get('screenshots', []))
|
||||||
|
imgui.text(f"{name} ({n_files} files, {n_shots} shots)")
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button(f"Load##{name}"):
|
||||||
|
self.load_context_preset(name)
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button(f"Delete##{name}"):
|
||||||
|
self.delete_context_preset(name)
|
||||||
def _render_track_proposal_modal(self) -> None:
|
def _render_track_proposal_modal(self) -> None:
|
||||||
if self._show_track_proposal_modal:
|
if self._show_track_proposal_modal:
|
||||||
imgui.open_popup("Track Proposal")
|
imgui.open_popup("Track Proposal")
|
||||||
@@ -2002,6 +2048,188 @@ class App:
|
|||||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_diagnostics_panel")
|
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_diagnostics_panel")
|
||||||
imgui.end()
|
imgui.end()
|
||||||
|
|
||||||
|
def _render_discussion_tab(self) -> None:
|
||||||
|
imgui.begin_child("HistoryChild", size=(0, -self.ui_discussion_split_h))
|
||||||
|
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_discussion_panel")
|
||||||
|
self._render_discussion_panel()
|
||||||
|
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_discussion_panel")
|
||||||
|
imgui.end_child()
|
||||||
|
imgui.button("###discussion_splitter", imgui.ImVec2(-1, 4))
|
||||||
|
if imgui.is_item_active():
|
||||||
|
self.ui_discussion_split_h = max(150.0, min(imgui.get_window_height() - 150.0, self.ui_discussion_split_h - imgui.get_io().mouse_delta.y))
|
||||||
|
imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4))
|
||||||
|
ch1, self.ui_separate_message_panel = imgui.checkbox("Pop Out Message", self.ui_separate_message_panel)
|
||||||
|
imgui.same_line()
|
||||||
|
ch2, self.ui_separate_response_panel = imgui.checkbox("Pop Out Response", self.ui_separate_response_panel)
|
||||||
|
if ch1: self.show_windows["Message"] = self.ui_separate_message_panel
|
||||||
|
if ch2: self.show_windows["Response"] = self.ui_separate_response_panel
|
||||||
|
imgui.pop_style_var()
|
||||||
|
show_message_tab = not self.ui_separate_message_panel
|
||||||
|
show_response_tab = not self.ui_separate_response_panel
|
||||||
|
if show_message_tab or show_response_tab:
|
||||||
|
if imgui.begin_tab_bar("discussion_tabs"):
|
||||||
|
tab_flags = imgui.TabItemFlags_.none
|
||||||
|
if self._autofocus_response_tab:
|
||||||
|
tab_flags = imgui.TabItemFlags_.set_selected
|
||||||
|
self._autofocus_response_tab = False
|
||||||
|
self.controller._autofocus_response_tab = False
|
||||||
|
if show_message_tab:
|
||||||
|
if imgui.begin_tab_item("Message", None)[0]:
|
||||||
|
self._render_message_panel()
|
||||||
|
imgui.end_tab_item()
|
||||||
|
if show_response_tab:
|
||||||
|
if imgui.begin_tab_item("Response", None, tab_flags)[0]:
|
||||||
|
self._render_response_panel()
|
||||||
|
imgui.end_tab_item()
|
||||||
|
imgui.end_tab_bar()
|
||||||
|
else:
|
||||||
|
imgui.text_disabled("Message & Response panels are detached.")
|
||||||
|
|
||||||
|
def _render_context_composition_panel(self) -> None:
|
||||||
|
imgui.text("Context Composition")
|
||||||
|
imgui.separator()
|
||||||
|
if imgui.begin_table("ctx_comp_table", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders):
|
||||||
|
imgui.table_setup_column("File", imgui.TableColumnFlags_.width_stretch)
|
||||||
|
imgui.table_setup_column("Flags", imgui.TableColumnFlags_.width_fixed, 120)
|
||||||
|
imgui.table_headers_row()
|
||||||
|
for i, f_item in enumerate(self.files):
|
||||||
|
imgui.table_next_row()
|
||||||
|
imgui.table_set_column_index(0)
|
||||||
|
imgui.text(f_item.path if hasattr(f_item, "path") else str(f_item))
|
||||||
|
imgui.table_set_column_index(1)
|
||||||
|
if hasattr(f_item, "auto_aggregate"):
|
||||||
|
changed_agg, f_item.auto_aggregate = imgui.checkbox(f"Agg##cc{i}", f_item.auto_aggregate)
|
||||||
|
imgui.same_line()
|
||||||
|
changed_full, f_item.force_full = imgui.checkbox(f"Full##cc{i}", f_item.force_full)
|
||||||
|
imgui.end_table()
|
||||||
|
imgui.separator()
|
||||||
|
imgui.text("Screenshots")
|
||||||
|
for i, s in enumerate(self.screenshots):
|
||||||
|
imgui.text(s)
|
||||||
|
imgui.separator()
|
||||||
|
imgui.text("Presets")
|
||||||
|
presets = self.controller.project.get('context_presets', {})
|
||||||
|
preset_names = [""] + sorted(presets.keys())
|
||||||
|
active = getattr(self, "ui_active_context_preset", "")
|
||||||
|
if active not in preset_names:
|
||||||
|
active = ""
|
||||||
|
try:
|
||||||
|
idx = preset_names.index(active)
|
||||||
|
except ValueError:
|
||||||
|
idx = 0
|
||||||
|
ch, new_idx = imgui.combo("##ctx_preset", idx, preset_names)
|
||||||
|
if ch:
|
||||||
|
self.ui_active_context_preset = preset_names[new_idx]
|
||||||
|
if preset_names[new_idx]:
|
||||||
|
self.load_context_preset(preset_names[new_idx])
|
||||||
|
imgui.same_line()
|
||||||
|
changed, new_name = imgui.input_text("##new_preset", getattr(self, "ui_new_context_preset_name", ""))
|
||||||
|
if changed:
|
||||||
|
self.ui_new_context_preset_name = new_name
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button("Save##ctx"):
|
||||||
|
if getattr(self, "ui_new_context_preset_name", "").strip():
|
||||||
|
self.save_context_preset(self.ui_new_context_preset_name.strip())
|
||||||
|
self.ui_new_context_preset_name = ""
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button("Delete##ctx"):
|
||||||
|
if getattr(self, "ui_active_context_preset", ""):
|
||||||
|
self.delete_context_preset(self.ui_active_context_preset)
|
||||||
|
self.ui_active_context_preset = ""
|
||||||
|
|
||||||
|
def _render_snapshot_tab(self) -> None:
|
||||||
|
if imgui.begin_tab_bar("snapshot_tabs"):
|
||||||
|
if imgui.begin_tab_item("Aggregate MD")[0]:
|
||||||
|
display_md = self.last_aggregate_markdown
|
||||||
|
if self.ui_focus_agent:
|
||||||
|
tier_usage = self.mma_tier_usage.get(self.ui_focus_agent)
|
||||||
|
if tier_usage:
|
||||||
|
persona_name = tier_usage.get("persona")
|
||||||
|
if persona_name:
|
||||||
|
persona = self.controller.personas.get(persona_name)
|
||||||
|
if persona and persona.context_preset:
|
||||||
|
cp_name = persona.context_preset
|
||||||
|
if cp_name in self._focus_md_cache:
|
||||||
|
display_md = self._focus_md_cache[cp_name]
|
||||||
|
else:
|
||||||
|
flat = src.project_manager.flat_config(self.controller.project, self.active_discussion)
|
||||||
|
cp = self.controller.project.get('context_presets', {}).get(cp_name)
|
||||||
|
if cp:
|
||||||
|
flat["files"]["paths"] = cp.get("files", [])
|
||||||
|
flat["screenshots"]["paths"] = cp.get("screenshots", [])
|
||||||
|
full_md, _, _ = src.aggregate.run(flat)
|
||||||
|
self._focus_md_cache[cp_name] = full_md
|
||||||
|
display_md = full_md
|
||||||
|
if imgui.button("Copy"):
|
||||||
|
imgui.set_clipboard_text(display_md)
|
||||||
|
imgui.begin_child("last_agg_md", imgui.ImVec2(0, 0), True)
|
||||||
|
markdown_helper.render(display_md, context_id="snapshot_agg")
|
||||||
|
imgui.end_child()
|
||||||
|
imgui.end_tab_item()
|
||||||
|
if imgui.begin_tab_item("System Prompt")[0]:
|
||||||
|
if imgui.button("Copy"):
|
||||||
|
imgui.set_clipboard_text(self.last_resolved_system_prompt)
|
||||||
|
imgui.begin_child("last_sys_prompt", imgui.ImVec2(0, 0), True)
|
||||||
|
markdown_helper.render(self.last_resolved_system_prompt, context_id="snapshot_sys")
|
||||||
|
imgui.end_child()
|
||||||
|
imgui.end_tab_item()
|
||||||
|
imgui.end_tab_bar()
|
||||||
|
|
||||||
|
def _render_takes_panel(self) -> None:
|
||||||
|
imgui.text("Takes & Synthesis")
|
||||||
|
imgui.separator()
|
||||||
|
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 = ""
|
||||||
|
if imgui.begin_table("takes_table", 3, imgui.TableFlags_.resizable | imgui.TableFlags_.borders):
|
||||||
|
imgui.table_setup_column("Name", imgui.TableColumnFlags_.width_stretch)
|
||||||
|
imgui.table_setup_column("Entries", imgui.TableColumnFlags_.width_fixed, 80)
|
||||||
|
imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 150)
|
||||||
|
imgui.table_headers_row()
|
||||||
|
for name, disc in discussions.items():
|
||||||
|
imgui.table_next_row()
|
||||||
|
imgui.table_set_column_index(0)
|
||||||
|
is_active = name == self.active_discussion
|
||||||
|
if is_active:
|
||||||
|
imgui.text_colored(C_IN, name)
|
||||||
|
else:
|
||||||
|
imgui.text(name)
|
||||||
|
imgui.table_set_column_index(1)
|
||||||
|
history = disc.get('history', [])
|
||||||
|
imgui.text(f"{len(history)}")
|
||||||
|
imgui.table_set_column_index(2)
|
||||||
|
if imgui.button(f"Switch##{name}"):
|
||||||
|
self._switch_discussion(name)
|
||||||
|
imgui.same_line()
|
||||||
|
if name != "main" and imgui.button(f"Delete##{name}"):
|
||||||
|
del discussions[name]
|
||||||
|
imgui.end_table()
|
||||||
|
imgui.separator()
|
||||||
|
imgui.text("Synthesis")
|
||||||
|
imgui.text("Select takes to synthesize:")
|
||||||
|
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
|
||||||
|
takes_dict = {name: discussions.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:
|
||||||
|
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_markdown_test(self) -> None:
|
def _render_markdown_test(self) -> None:
|
||||||
imgui.text("Markdown Test Panel")
|
imgui.text("Markdown Test Panel")
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
@@ -2135,12 +2363,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
|
||||||
@@ -2179,17 +2405,65 @@ def hello():
|
|||||||
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()
|
||||||
|
|
||||||
|
# Sync variables in case combo selection changed self.active_discussion
|
||||||
|
active_base = self.active_discussion.split("_take_")[0]
|
||||||
|
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)
|
||||||
@@ -2204,10 +2478,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))
|
||||||
@@ -2220,9 +2497,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"):
|
||||||
@@ -2235,6 +2514,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"):
|
||||||
@@ -2254,6 +2534,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:")
|
||||||
@@ -2266,15 +2547,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()
|
||||||
@@ -2283,14 +2568,27 @@ 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
|
||||||
|
display_entries = self.disc_entries
|
||||||
|
if self.ui_focus_agent:
|
||||||
|
tier_usage = self.mma_tier_usage.get(self.ui_focus_agent)
|
||||||
|
if tier_usage:
|
||||||
|
persona_name = tier_usage.get("persona")
|
||||||
|
if persona_name:
|
||||||
|
# Show User messages and the focused agent's responses
|
||||||
|
display_entries = [e for e in self.disc_entries if e.get("role") == persona_name or e.get("role") == "User"]
|
||||||
|
|
||||||
clipper = imgui.ListClipper()
|
clipper = imgui.ListClipper()
|
||||||
clipper.begin(len(self.disc_entries))
|
clipper.begin(len(display_entries))
|
||||||
while clipper.step():
|
while clipper.step():
|
||||||
for i in range(clipper.display_start, clipper.display_end):
|
for i in range(clipper.display_start, clipper.display_end):
|
||||||
entry = self.disc_entries[i]
|
entry = display_entries[i]
|
||||||
imgui.push_id(str(i))
|
# Use the index in the original list for ID if possible, but here i is index in display_entries
|
||||||
|
imgui.push_id(f"disc_{i}")
|
||||||
collapsed = entry.get("collapsed", False)
|
collapsed = entry.get("collapsed", False)
|
||||||
read_mode = entry.get("read_mode", False)
|
read_mode = entry.get("read_mode", False)
|
||||||
if imgui.button("+" if collapsed else "-"):
|
if imgui.button("+" if collapsed else "-"):
|
||||||
@@ -2304,14 +2602,33 @@ 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()
|
||||||
imgui.text_colored(vec4(120, 120, 100), str(ts_str))
|
imgui.text_colored(vec4(120, 120, 100), str(ts_str))
|
||||||
|
# Visual indicator for file injections
|
||||||
|
e_dt = project_manager.parse_ts(ts_str)
|
||||||
|
if e_dt:
|
||||||
|
e_unix = e_dt.timestamp()
|
||||||
|
next_unix = float('inf')
|
||||||
|
if i + 1 < len(self.disc_entries):
|
||||||
|
n_ts = self.disc_entries[i+1].get("ts", "")
|
||||||
|
n_dt = project_manager.parse_ts(n_ts)
|
||||||
|
if n_dt: next_unix = n_dt.timestamp()
|
||||||
|
injected_here = [f for f in self.files if hasattr(f, 'injected_at') and f.injected_at and e_unix <= f.injected_at < next_unix]
|
||||||
|
if injected_here:
|
||||||
|
imgui.same_line()
|
||||||
|
imgui.text_colored(vec4(100, 255, 100), f"[{len(injected_here)}+]")
|
||||||
|
if imgui.is_item_hovered():
|
||||||
|
tooltip = "Files injected at this point:\n" + "\n".join([f.path for f in injected_here])
|
||||||
|
imgui.set_tooltip(tooltip)
|
||||||
|
|
||||||
if collapsed:
|
if collapsed:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("Ins"):
|
if imgui.button("Ins"):
|
||||||
@@ -2322,10 +2639,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:
|
||||||
@@ -2382,10 +2702,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")
|
||||||
@@ -2397,6 +2753,8 @@ def hello():
|
|||||||
if imgui.selectable("None", not self.ui_active_persona)[0]:
|
if imgui.selectable("None", not self.ui_active_persona)[0]:
|
||||||
self.ui_active_persona = ""
|
self.ui_active_persona = ""
|
||||||
for pname in sorted(personas.keys()):
|
for pname in sorted(personas.keys()):
|
||||||
|
if not pname:
|
||||||
|
continue
|
||||||
if imgui.selectable(pname, pname == self.ui_active_persona)[0]:
|
if imgui.selectable(pname, pname == self.ui_active_persona)[0]:
|
||||||
self.ui_active_persona = pname
|
self.ui_active_persona = pname
|
||||||
if pname in personas:
|
if pname in personas:
|
||||||
@@ -2405,6 +2763,7 @@ def hello():
|
|||||||
self._editing_persona_system_prompt = persona.system_prompt or ""
|
self._editing_persona_system_prompt = persona.system_prompt or ""
|
||||||
self._editing_persona_tool_preset_id = persona.tool_preset or ""
|
self._editing_persona_tool_preset_id = persona.tool_preset or ""
|
||||||
self._editing_persona_bias_profile_id = persona.bias_profile or ""
|
self._editing_persona_bias_profile_id = persona.bias_profile or ""
|
||||||
|
self._editing_persona_context_preset_id = getattr(persona, 'context_preset', '') or ""
|
||||||
import copy
|
import copy
|
||||||
self._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else []
|
self._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else []
|
||||||
self._editing_persona_is_new = False
|
self._editing_persona_is_new = False
|
||||||
@@ -2433,6 +2792,9 @@ def hello():
|
|||||||
if persona.bias_profile:
|
if persona.bias_profile:
|
||||||
self.ui_active_bias_profile = persona.bias_profile
|
self.ui_active_bias_profile = persona.bias_profile
|
||||||
ai_client.set_bias_profile(persona.bias_profile)
|
ai_client.set_bias_profile(persona.bias_profile)
|
||||||
|
if getattr(persona, 'context_preset', None):
|
||||||
|
self.ui_active_context_preset = persona.context_preset
|
||||||
|
self.load_context_preset(persona.context_preset)
|
||||||
imgui.end_combo()
|
imgui.end_combo()
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("Manage Personas"):
|
if imgui.button("Manage Personas"):
|
||||||
@@ -3296,6 +3658,24 @@ def hello():
|
|||||||
|
|
||||||
def _render_mma_dashboard(self) -> None:
|
def _render_mma_dashboard(self) -> None:
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_mma_dashboard")
|
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_mma_dashboard")
|
||||||
|
|
||||||
|
# Focus Agent dropdown
|
||||||
|
imgui.text("Focus Agent:")
|
||||||
|
imgui.same_line()
|
||||||
|
focus_label = self.ui_focus_agent or "All"
|
||||||
|
if imgui.begin_combo("##focus_agent", focus_label, imgui.ComboFlags_.width_fit_preview):
|
||||||
|
if imgui.selectable("All", self.ui_focus_agent is None)[0]:
|
||||||
|
self.ui_focus_agent = None
|
||||||
|
for tier in ["Tier 2", "Tier 3", "Tier 4"]:
|
||||||
|
if imgui.selectable(tier, self.ui_focus_agent == tier)[0]:
|
||||||
|
self.ui_focus_agent = tier
|
||||||
|
imgui.end_combo()
|
||||||
|
imgui.same_line()
|
||||||
|
if self.ui_focus_agent:
|
||||||
|
if imgui.button("x##clear_focus"):
|
||||||
|
self.ui_focus_agent = None
|
||||||
|
imgui.separator()
|
||||||
|
|
||||||
is_nerv = theme.is_nerv_active()
|
is_nerv = theme.is_nerv_active()
|
||||||
if self.is_viewing_prior_session:
|
if self.is_viewing_prior_session:
|
||||||
c = vec4(255, 200, 100)
|
c = vec4(255, 200, 100)
|
||||||
@@ -3942,6 +4322,8 @@ def hello():
|
|||||||
from src import ai_client
|
from src import ai_client
|
||||||
ai_client.set_bias_profile(None)
|
ai_client.set_bias_profile(None)
|
||||||
for bname in sorted(self.controller.bias_profiles.keys()):
|
for bname in sorted(self.controller.bias_profiles.keys()):
|
||||||
|
if not bname:
|
||||||
|
continue
|
||||||
if imgui.selectable(bname, bname == getattr(self, 'ui_active_bias_profile', ""))[0]:
|
if imgui.selectable(bname, bname == getattr(self, 'ui_active_bias_profile', ""))[0]:
|
||||||
self.ui_active_bias_profile = bname
|
self.ui_active_bias_profile = bname
|
||||||
from src import ai_client
|
from src import ai_client
|
||||||
|
|||||||
@@ -357,12 +357,14 @@ class FileItem:
|
|||||||
path: str
|
path: str
|
||||||
auto_aggregate: bool = True
|
auto_aggregate: bool = True
|
||||||
force_full: bool = False
|
force_full: bool = False
|
||||||
|
injected_at: Optional[float] = None
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"path": self.path,
|
"path": self.path,
|
||||||
"auto_aggregate": self.auto_aggregate,
|
"auto_aggregate": self.auto_aggregate,
|
||||||
"force_full": self.force_full,
|
"force_full": self.force_full,
|
||||||
|
"injected_at": self.injected_at,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -371,6 +373,7 @@ class FileItem:
|
|||||||
path=data["path"],
|
path=data["path"],
|
||||||
auto_aggregate=data.get("auto_aggregate", True),
|
auto_aggregate=data.get("auto_aggregate", True),
|
||||||
force_full=data.get("force_full", False),
|
force_full=data.get("force_full", False),
|
||||||
|
injected_at=data.get("injected_at"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -466,6 +469,7 @@ class Persona:
|
|||||||
system_prompt: str = ''
|
system_prompt: str = ''
|
||||||
tool_preset: Optional[str] = None
|
tool_preset: Optional[str] = None
|
||||||
bias_profile: Optional[str] = None
|
bias_profile: Optional[str] = None
|
||||||
|
context_preset: Optional[str] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def provider(self) -> Optional[str]:
|
def provider(self) -> Optional[str]:
|
||||||
@@ -508,6 +512,8 @@ class Persona:
|
|||||||
res["tool_preset"] = self.tool_preset
|
res["tool_preset"] = self.tool_preset
|
||||||
if self.bias_profile is not None:
|
if self.bias_profile is not None:
|
||||||
res["bias_profile"] = self.bias_profile
|
res["bias_profile"] = self.bias_profile
|
||||||
|
if self.context_preset is not None:
|
||||||
|
res["context_preset"] = self.context_preset
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -541,8 +547,8 @@ class Persona:
|
|||||||
system_prompt=data.get("system_prompt", ""),
|
system_prompt=data.get("system_prompt", ""),
|
||||||
tool_preset=data.get("tool_preset"),
|
tool_preset=data.get("tool_preset"),
|
||||||
bias_profile=data.get("bias_profile"),
|
bias_profile=data.get("bias_profile"),
|
||||||
|
context_preset=data.get("context_preset"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MCPServerConfig:
|
class MCPServerConfig:
|
||||||
name: str
|
name: str
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ def default_project(name: str = "unnamed") -> dict[str, Any]:
|
|||||||
"output": {"output_dir": "./md_gen"},
|
"output": {"output_dir": "./md_gen"},
|
||||||
"files": {"base_dir": ".", "paths": [], "tier_assignments": {}},
|
"files": {"base_dir": ".", "paths": [], "tier_assignments": {}},
|
||||||
"screenshots": {"base_dir": ".", "paths": []},
|
"screenshots": {"base_dir": ".", "paths": []},
|
||||||
|
"context_presets": {},
|
||||||
"gemini_cli": {"binary_path": "gemini"},
|
"gemini_cli": {"binary_path": "gemini"},
|
||||||
"deepseek": {"reasoning_effort": "medium"},
|
"deepseek": {"reasoning_effort": "medium"},
|
||||||
"agent": {
|
"agent": {
|
||||||
@@ -243,11 +244,33 @@ def flat_config(proj: dict[str, Any], disc_name: Optional[str] = None, track_id:
|
|||||||
"output": proj.get("output", {}),
|
"output": proj.get("output", {}),
|
||||||
"files": proj.get("files", {}),
|
"files": proj.get("files", {}),
|
||||||
"screenshots": proj.get("screenshots", {}),
|
"screenshots": proj.get("screenshots", {}),
|
||||||
|
"context_presets": proj.get("context_presets", {}),
|
||||||
"discussion": {
|
"discussion": {
|
||||||
"roles": disc_sec.get("roles", []),
|
"roles": disc_sec.get("roles", []),
|
||||||
"history": history,
|
"history": history,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
# ── context presets ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def save_context_preset(project_dict: dict, preset_name: str, files: list[str], screenshots: list[str]) -> None:
|
||||||
|
"""Save a named context preset (files + screenshots) into the project dict."""
|
||||||
|
if "context_presets" not in project_dict:
|
||||||
|
project_dict["context_presets"] = {}
|
||||||
|
project_dict["context_presets"][preset_name] = {
|
||||||
|
"files": files,
|
||||||
|
"screenshots": screenshots
|
||||||
|
}
|
||||||
|
|
||||||
|
def load_context_preset(project_dict: dict, preset_name: str) -> dict:
|
||||||
|
"""Return the files and screenshots for a named preset."""
|
||||||
|
if "context_presets" not in project_dict or preset_name not in project_dict["context_presets"]:
|
||||||
|
raise KeyError(f"Preset '{preset_name}' not found in project context_presets.")
|
||||||
|
return project_dict["context_presets"][preset_name]
|
||||||
|
|
||||||
|
def delete_context_preset(project_dict: dict, preset_name: str) -> None:
|
||||||
|
"""Remove a named preset if it exists."""
|
||||||
|
if "context_presets" in project_dict:
|
||||||
|
project_dict["context_presets"].pop(preset_name, None)
|
||||||
# ── track state persistence ─────────────────────────────────────────────────
|
# ── track state persistence ─────────────────────────────────────────────────
|
||||||
|
|
||||||
def save_track_state(track_id: str, state: 'TrackState', base_dir: Union[str, Path] = ".") -> None:
|
def save_track_state(track_id: str, state: 'TrackState', base_dir: Union[str, Path] = ".") -> None:
|
||||||
@@ -401,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
|
||||||
42
tests/test_context_composition_panel.py
Normal file
42
tests/test_context_composition_panel.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import pytest
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
|
def test_context_composition_panel_replaces_placeholder():
|
||||||
|
import src.gui_2 as gui_2
|
||||||
|
|
||||||
|
source = inspect.getsource(gui_2.App._gui_func)
|
||||||
|
assert "_render_context_composition_placeholder" not in source, (
|
||||||
|
"Placeholder should be replaced"
|
||||||
|
)
|
||||||
|
assert "_render_context_composition_panel" in source, (
|
||||||
|
"Should have _render_context_composition_panel"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_context_composition_has_save_load_buttons():
|
||||||
|
import src.gui_2 as gui_2
|
||||||
|
|
||||||
|
source = inspect.getsource(gui_2.App._render_context_composition_panel)
|
||||||
|
assert "Save as Preset" in source or "save" in source.lower(), (
|
||||||
|
"Should have Save functionality"
|
||||||
|
)
|
||||||
|
assert "Load Preset" in source or "load" in source.lower(), (
|
||||||
|
"Should have Load functionality"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_context_composition_shows_files():
|
||||||
|
import src.gui_2 as gui_2
|
||||||
|
|
||||||
|
source = inspect.getsource(gui_2.App._render_context_composition_panel)
|
||||||
|
assert "files" in source.lower() or "Files" in source, "Should show files"
|
||||||
|
|
||||||
|
|
||||||
|
def test_context_composition_has_preset_list():
|
||||||
|
import src.gui_2 as gui_2
|
||||||
|
|
||||||
|
source = inspect.getsource(gui_2.App._render_context_composition_panel)
|
||||||
|
assert "context_presets" in source or "preset" in source.lower(), (
|
||||||
|
"Should reference presets"
|
||||||
|
)
|
||||||
59
tests/test_context_presets.py
Normal file
59
tests/test_context_presets.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import pytest
|
||||||
|
from src.project_manager import (
|
||||||
|
save_context_preset,
|
||||||
|
load_context_preset,
|
||||||
|
delete_context_preset
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_save_context_preset():
|
||||||
|
project_dict = {}
|
||||||
|
preset_name = "test_preset"
|
||||||
|
files = ["file1.py", "file2.py"]
|
||||||
|
screenshots = ["screenshot1.png"]
|
||||||
|
|
||||||
|
save_context_preset(project_dict, preset_name, files, screenshots)
|
||||||
|
|
||||||
|
assert "context_presets" in project_dict
|
||||||
|
assert preset_name in project_dict["context_presets"]
|
||||||
|
assert project_dict["context_presets"][preset_name]["files"] == files
|
||||||
|
assert project_dict["context_presets"][preset_name]["screenshots"] == screenshots
|
||||||
|
|
||||||
|
def test_load_context_preset():
|
||||||
|
project_dict = {
|
||||||
|
"context_presets": {
|
||||||
|
"test_preset": {
|
||||||
|
"files": ["file1.py"],
|
||||||
|
"screenshots": ["screenshot1.png"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preset = load_context_preset(project_dict, "test_preset")
|
||||||
|
|
||||||
|
assert preset["files"] == ["file1.py"]
|
||||||
|
assert preset["screenshots"] == ["screenshot1.png"]
|
||||||
|
|
||||||
|
def test_load_nonexistent_preset():
|
||||||
|
project_dict = {"context_presets": {}}
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
load_context_preset(project_dict, "nonexistent")
|
||||||
|
|
||||||
|
def test_delete_context_preset():
|
||||||
|
project_dict = {
|
||||||
|
"context_presets": {
|
||||||
|
"test_preset": {
|
||||||
|
"files": ["file1.py"],
|
||||||
|
"screenshots": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_context_preset(project_dict, "test_preset")
|
||||||
|
|
||||||
|
assert "test_preset" not in project_dict["context_presets"]
|
||||||
|
|
||||||
|
def test_delete_nonexistent_preset_no_error():
|
||||||
|
project_dict = {"context_presets": {}}
|
||||||
|
# Should not raise error if it doesn't exist
|
||||||
|
delete_context_preset(project_dict, "nonexistent")
|
||||||
|
assert "nonexistent" not in project_dict["context_presets"]
|
||||||
14
tests/test_context_presets_removal.py
Normal file
14
tests/test_context_presets_removal.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import pytest
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
|
def test_context_presets_tab_removed_from_project_settings():
|
||||||
|
import src.gui_2 as gui_2
|
||||||
|
|
||||||
|
source = inspect.getsource(gui_2.App._gui_func)
|
||||||
|
assert "Context Presets" not in source, (
|
||||||
|
"Context Presets tab should be removed from Project Settings"
|
||||||
|
)
|
||||||
|
assert "_render_context_presets_panel" not in source, (
|
||||||
|
"Context presets panel call should be removed"
|
||||||
|
)
|
||||||
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
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ def test_gui2_hubs_exist_in_show_windows(app_instance: App) -> None:
|
|||||||
This ensures they will be available in the 'Windows' menu.
|
This ensures they will be available in the 'Windows' menu.
|
||||||
"""
|
"""
|
||||||
expected_hubs = [
|
expected_hubs = [
|
||||||
"Context Hub",
|
"Project Settings",
|
||||||
"AI Settings",
|
"AI Settings",
|
||||||
"Discussion Hub",
|
"Discussion Hub",
|
||||||
"Operations Hub",
|
"Operations Hub",
|
||||||
|
|||||||
35
tests/test_gui_context_presets.py
Normal file
35
tests/test_gui_context_presets.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import pytest
|
||||||
|
import time
|
||||||
|
from src.api_hook_client import ApiHookClient
|
||||||
|
|
||||||
|
def test_gui_context_preset_save_load(live_gui) -> None:
|
||||||
|
"""Verify that saving and loading context presets works via the GUI app."""
|
||||||
|
client = ApiHookClient()
|
||||||
|
assert client.wait_for_server(timeout=15)
|
||||||
|
|
||||||
|
preset_name = "test_gui_preset"
|
||||||
|
test_files = ["test.py"]
|
||||||
|
test_screenshots = ["test.png"]
|
||||||
|
|
||||||
|
client.push_event("custom_callback", {"callback": "simulate_save_preset", "args": [preset_name]})
|
||||||
|
time.sleep(1.5)
|
||||||
|
|
||||||
|
project_data = client.get_project()
|
||||||
|
project = project_data.get("project", {})
|
||||||
|
presets = project.get("context_presets", {})
|
||||||
|
|
||||||
|
assert preset_name in presets, f"Preset '{preset_name}' not found in project context_presets"
|
||||||
|
|
||||||
|
preset_entry = presets[preset_name]
|
||||||
|
preset_files = [f["path"] if isinstance(f, dict) else str(f) for f in preset_entry.get("files", [])]
|
||||||
|
assert preset_files == test_files
|
||||||
|
assert preset_entry.get("screenshots", []) == test_screenshots
|
||||||
|
|
||||||
|
# Load the preset
|
||||||
|
client.push_event("custom_callback", {"callback": "load_context_preset", "args": [preset_name]})
|
||||||
|
time.sleep(1.0)
|
||||||
|
|
||||||
|
context = client.get_context_state()
|
||||||
|
loaded_files = [f["path"] if isinstance(f, dict) else str(f) for f in context.get("files", [])]
|
||||||
|
assert loaded_files == test_files
|
||||||
|
assert context.get("screenshots", []) == test_screenshots
|
||||||
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")
|
||||||
@@ -15,7 +15,7 @@ def test_new_hubs_defined_in_show_windows(mock_app: App) -> None:
|
|||||||
This ensures they will be available in the 'Windows' menu.
|
This ensures they will be available in the 'Windows' menu.
|
||||||
"""
|
"""
|
||||||
expected_hubs = [
|
expected_hubs = [
|
||||||
"Context Hub",
|
"Project Settings",
|
||||||
"AI Settings",
|
"AI Settings",
|
||||||
"Discussion Hub",
|
"Discussion Hub",
|
||||||
"Operations Hub",
|
"Operations Hub",
|
||||||
@@ -53,7 +53,7 @@ def test_hub_windows_exist_in_gui2(app_instance_simple: Any) -> None:
|
|||||||
"""
|
"""
|
||||||
Verifies that the new Hub windows are present in the show_windows dictionary.
|
Verifies that the new Hub windows are present in the show_windows dictionary.
|
||||||
"""
|
"""
|
||||||
hubs = ["Context Hub", "AI Settings", "Discussion Hub", "Operations Hub"]
|
hubs = ["Project Settings", "AI Settings", "Discussion Hub", "Operations Hub"]
|
||||||
for hub in hubs:
|
for hub in hubs:
|
||||||
assert hub in app_instance_simple.show_windows
|
assert hub in app_instance_simple.show_windows
|
||||||
|
|
||||||
|
|||||||
@@ -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", [])
|
||||||
|
|||||||
22
tests/test_project_settings_rename.py
Normal file
22
tests/test_project_settings_rename.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import pytest
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
|
def test_context_hub_renamed_to_project_settings():
|
||||||
|
import src.gui_2 as gui_2
|
||||||
|
|
||||||
|
source = inspect.getsource(gui_2.App._gui_func)
|
||||||
|
assert "Project Settings" in source, (
|
||||||
|
"Context Hub should be renamed to Project Settings"
|
||||||
|
)
|
||||||
|
assert '"Context Hub"' not in source, '"Context Hub" string should be removed'
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_windows_key_updated():
|
||||||
|
import src.app_controller as app_controller
|
||||||
|
|
||||||
|
source = inspect.getsource(app_controller.AppController)
|
||||||
|
assert '"Project Settings"' in source or "'Project Settings'" in source, (
|
||||||
|
"show_windows key should be Project Settings"
|
||||||
|
)
|
||||||
|
assert '"Context Hub"' not in source, '"Context Hub" key should be removed'
|
||||||
42
tests/test_session_hub_merge.py
Normal file
42
tests/test_session_hub_merge.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import pytest
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_hub_window_removed():
|
||||||
|
import src.gui_2 as gui_2
|
||||||
|
|
||||||
|
source = inspect.getsource(gui_2.App._gui_func)
|
||||||
|
assert "Session Hub" not in source, "Session Hub window should be removed"
|
||||||
|
|
||||||
|
|
||||||
|
def test_discussion_hub_has_snapshot_tab():
|
||||||
|
import src.gui_2 as gui_2
|
||||||
|
|
||||||
|
source = inspect.getsource(gui_2.App._gui_func)
|
||||||
|
assert "Snapshot" in source, "Discussion Hub should have Snapshot tab"
|
||||||
|
assert "_render_snapshot_tab" in source, "Discussion Hub should call _render_snapshot_tab"
|
||||||
|
|
||||||
|
|
||||||
|
def test_discussion_hub_has_context_composition_placeholder():
|
||||||
|
import src.gui_2 as gui_2
|
||||||
|
|
||||||
|
source = inspect.getsource(gui_2.App._gui_func)
|
||||||
|
assert "Context Composition" in source, (
|
||||||
|
"Discussion Hub should have Context Composition tab placeholder"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_discussion_hub_has_takes_tab():
|
||||||
|
import src.gui_2 as gui_2
|
||||||
|
|
||||||
|
source = inspect.getsource(gui_2.App._gui_func)
|
||||||
|
assert "Takes" in source, "Discussion Hub should have Takes tab"
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_windows_no_session_hub():
|
||||||
|
import src.app_controller as app_controller
|
||||||
|
|
||||||
|
source = inspect.getsource(app_controller.AppController)
|
||||||
|
assert "Session Hub" not in source, (
|
||||||
|
"Session Hub should be removed from show_windows"
|
||||||
|
)
|
||||||
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
|
||||||
16
tests/test_takes_panel.py
Normal file
16
tests/test_takes_panel.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import pytest
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
|
def test_takes_tab_replaces_placeholder():
|
||||||
|
import src.gui_2 as gui_2
|
||||||
|
|
||||||
|
source = inspect.getsource(gui_2.App._gui_func)
|
||||||
|
assert "_render_takes_placeholder" not in source, "Placeholder should be replaced"
|
||||||
|
|
||||||
|
|
||||||
|
def test_takes_panel_has_synthesis():
|
||||||
|
import src.gui_2 as gui_2
|
||||||
|
|
||||||
|
source = inspect.getsource(gui_2.App._render_takes_panel)
|
||||||
|
assert "synthesis" in source.lower(), "Should have synthesis functionality"
|
||||||
75
tests/test_ui_summary_only_removal.py
Normal file
75
tests/test_ui_summary_only_removal.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import pytest
|
||||||
|
import inspect
|
||||||
|
from src import models
|
||||||
|
|
||||||
|
|
||||||
|
def test_ui_summary_only_not_in_projects_panel():
|
||||||
|
import src.gui_2 as gui_2
|
||||||
|
|
||||||
|
source = inspect.getsource(gui_2.App._render_projects_panel)
|
||||||
|
assert "ui_summary_only" not in source, (
|
||||||
|
"ui_summary_only checkbox should be removed from Projects panel"
|
||||||
|
)
|
||||||
|
assert "Summary Only" not in source, (
|
||||||
|
"Summary Only label should be removed from Projects panel"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ui_summary_only_not_in_app_controller_projects():
|
||||||
|
import src.app_controller as app_controller
|
||||||
|
|
||||||
|
source = inspect.getsource(app_controller.AppController)
|
||||||
|
assert "ui_summary_only" not in source, (
|
||||||
|
"ui_summary_only should be removed from AppController"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_item_has_per_file_flags():
|
||||||
|
item = models.FileItem(path="test.py")
|
||||||
|
assert hasattr(item, "auto_aggregate")
|
||||||
|
assert hasattr(item, "force_full")
|
||||||
|
assert item.auto_aggregate is True
|
||||||
|
assert item.force_full is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_item_serialization_with_flags():
|
||||||
|
item = models.FileItem(path="test.py", auto_aggregate=False, force_full=True)
|
||||||
|
data = item.to_dict()
|
||||||
|
|
||||||
|
assert data["auto_aggregate"] is False
|
||||||
|
assert data["force_full"] is True
|
||||||
|
|
||||||
|
restored = models.FileItem.from_dict(data)
|
||||||
|
assert restored.auto_aggregate is False
|
||||||
|
assert restored.force_full is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_without_summary_only_loads():
|
||||||
|
proj = {"project": {"name": "test", "paths": []}}
|
||||||
|
assert proj.get("project", {}).get("summary_only") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_aggregate_from_items_respects_auto_aggregate():
|
||||||
|
from pathlib import Path
|
||||||
|
from src import aggregate
|
||||||
|
|
||||||
|
items = [
|
||||||
|
{
|
||||||
|
"path": Path("file1.py"),
|
||||||
|
"entry": "file1.py",
|
||||||
|
"content": "print('hello')",
|
||||||
|
"auto_aggregate": True,
|
||||||
|
"force_full": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": Path("file2.py"),
|
||||||
|
"entry": "file2.py",
|
||||||
|
"content": "print('world')",
|
||||||
|
"auto_aggregate": False,
|
||||||
|
"force_full": False,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
result = aggregate._build_files_section_from_items(items)
|
||||||
|
assert "file1.py" in result
|
||||||
|
assert "file2.py" not in result
|
||||||
Reference in New Issue
Block a user