Compare commits

...

7 Commits

17 changed files with 593 additions and 81 deletions

21
.dockerignore Normal file
View File

@@ -0,0 +1,21 @@
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.git
.gitignore
logs
gallery
md_gen
credentials.toml
manual_slop.toml
manual_slop_history.toml
manualslop_layout.ini
dpg_layout.ini
.pytest_cache
scripts/generated
.gemini
conductor/archive
.editorconfig
*.log

34
Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
# Use python:3.11-slim as a base
FROM python:3.11-slim
# Set environment variables
# UV_SYSTEM_PYTHON=1 allows uv to install into the system site-packages
ENV PYTHONDONTWRITEBYTECODE=1
PYTHONUNBUFFERED=1
UV_SYSTEM_PYTHON=1
# Install system dependencies and uv
RUN apt-get update && apt-get install -y --no-install-recommends
curl
ca-certificates
&& rm -rf /var/lib/apt/lists/*
&& curl -LsSf https://astral.sh/uv/install.sh | sh
&& mv /root/.local/bin/uv /usr/local/bin/uv
# Set the working directory in the container
WORKDIR /app
# Copy dependency files first to leverage Docker layer caching
COPY pyproject.toml requirements.txt* ./
# Install dependencies via uv
RUN if [ -f requirements.txt ]; then uv pip install --no-cache -r requirements.txt; fi
# Copy the rest of the application code
COPY . .
# Expose port 8000 for the headless API/service
EXPOSE 8000
# Set the entrypoint to run the app in headless mode
ENTRYPOINT ["python", "gui_2.py", "--headless"]

View File

@@ -18,4 +18,6 @@ To serve as an expert-level utility for personal developer use on small projects
- **Integrated Workspace:** A consolidated Hub-based layout (Context, AI Settings, Discussion, Operations) designed for expert multi-monitor workflows. - **Integrated Workspace:** A consolidated Hub-based layout (Context, AI Settings, Discussion, Operations) designed for expert multi-monitor workflows.
- **Session Analysis:** Ability to load and visualize historical session logs with a dedicated tinted "Prior Session" viewing mode. - **Session Analysis:** Ability to load and visualize historical session logs with a dedicated tinted "Prior Session" viewing mode.
- **Performance Diagnostics:** Built-in telemetry for FPS, Frame Time, and CPU usage, with a dedicated Diagnostics Panel and AI API hooks for performance analysis. - **Performance Diagnostics:** Built-in telemetry for FPS, Frame Time, and CPU usage, with a dedicated Diagnostics Panel and AI API hooks for performance analysis.
- **Automated UX Verification:** A robust IPC mechanism via API hooks and a modular simulation suite allows for human-like simulation walkthroughs and automated regression testing of the full GUI lifecycle across multiple specialized scenarios. - **Automated UX Verification:** A robust IPC mechanism via API hooks and a modular simulation suite allows for human-like simulation walkthroughs and automated regression testing of the full GUI lifecycle across multiple specialized scenarios.
- **Headless Backend Service:** Optional headless mode allowing the core AI and tool execution logic to run as a decoupled REST API service (FastAPI), optimized for Docker and server-side environments (e.g., Unraid).
- **Remote Confirmation Protocol:** A non-blocking, ID-based challenge/response mechanism for approving AI actions via the REST API, enabling remote "Human-in-the-Loop" safety.

View File

@@ -9,6 +9,11 @@
- **Dear PyGui:** For immediate/retained mode GUI rendering and node mapping. - **Dear PyGui:** For immediate/retained mode GUI rendering and node mapping.
- **ImGui Bundle (`imgui-bundle`):** To provide advanced multi-viewport and dockable panel capabilities on top of Dear ImGui. - **ImGui Bundle (`imgui-bundle`):** To provide advanced multi-viewport and dockable panel capabilities on top of Dear ImGui.
## Web & Service Frameworks
- **FastAPI:** High-performance REST API framework for providing the headless backend service.
- **Uvicorn:** ASGI server for serving the FastAPI application.
## AI Integration SDKs ## AI Integration SDKs
- **google-genai:** For Google Gemini API interaction and explicit context caching. - **google-genai:** For Google Gemini API interaction and explicit context caching.

View File

@@ -41,6 +41,6 @@ This file tracks all major tracks for the project. Each track has its own detail
--- ---
- [~] **Track: Support headless manual_slop for making an unraid gui docker frontend and a unraid server backend down the line.** - [x] **Track: Support headless manual_slop for making an unraid gui docker frontend and a unraid server backend down the line.**
*Link: [./tracks/manual_slop_headless_20260225/](./tracks/manual_slop_headless_20260225/)* *Link: [./tracks/manual_slop_headless_20260225/](./tracks/manual_slop_headless_20260225/)*

View File

@@ -1,49 +1,49 @@
# Implementation Plan: Manual Slop Headless Backend # Implementation Plan: Manual Slop Headless Backend
## Phase 1: Project Setup & Headless Scaffold ## Phase 1: Project Setup & Headless Scaffold [checkpoint: d5f056c]
- [x] Task: Update dependencies (02fc847) - [x] Task: Update dependencies (02fc847)
- [ ] Add `fastapi` and `uvicorn` to `pyproject.toml` (and sync `requirements.txt` via `uv`). - [x] Add `fastapi` and `uvicorn` to `pyproject.toml` (and sync `requirements.txt` via `uv`).
- [~] Task: Implement headless startup - [x] Task: Implement headless startup
- [ ] Modify `gui_2.py` (or create `headless.py`) to parse a `--headless` CLI flag. - [x] Modify `gui_2.py` (or create `headless.py`) to parse a `--headless` CLI flag.
- [ ] Update config parsing in `config.toml` to support headless configuration sections. - [x] Update config parsing in `config.toml` to support headless configuration sections.
- [ ] Bypass Dear PyGui initialization if headless mode is active. - [x] Bypass Dear PyGui initialization if headless mode is active.
- [ ] Task: Create foundational API application - [x] Task: Create foundational API application
- [ ] Set up the core FastAPI application instance. - [x] Set up the core FastAPI application instance.
- [ ] Implement `/health` and `/status` endpoints for Docker lifecycle checks. - [x] Implement `/health` and `/status` endpoints for Docker lifecycle checks.
- [ ] Task: Conductor - User Manual Verification 'Project Setup & Headless Scaffold' (Protocol in workflow.md) - [x] Task: Conductor - User Manual Verification 'Project Setup & Headless Scaffold' (Protocol in workflow.md) d5f056c
## Phase 2: Core API Routes & Authentication ## Phase 2: Core API Routes & Authentication [checkpoint: 4e0bcd5]
- [ ] Task: Implement API Key Security - [x] Task: Implement API Key Security
- [ ] Create a dependency/middleware in FastAPI to validate `X-API-KEY`. - [x] Create a dependency/middleware in FastAPI to validate `X-API-KEY`.
- [ ] Configure the API key validator to read from environment variables or `manual_slop.toml` (supporting Unraid template secrets). - [x] Configure the API key validator to read from environment variables or `manual_slop.toml` (supporting Unraid template secrets).
- [ ] Add tests for authorized and unauthorized API access. - [x] Add tests for authorized and unauthorized API access.
- [ ] Task: Implement AI Generation Endpoint - [x] Task: Implement AI Generation Endpoint
- [ ] Create a `/api/v1/generate` POST endpoint. - [x] Create a `/api/v1/generate` POST endpoint.
- [ ] Map request payloads to `ai_client.py` unified wrappers. - [x] Map request payloads to `ai_client.py` unified wrappers.
- [ ] Return standard JSON responses with the generated text and token metrics. - [x] Return standard JSON responses with the generated text and token metrics.
- [ ] Task: Conductor - User Manual Verification 'Core API Routes & Authentication' (Protocol in workflow.md) - [x] Task: Conductor - User Manual Verification 'Core API Routes & Authentication' (Protocol in workflow.md) 4e0bcd5
## Phase 3: Remote Tool Confirmation Mechanism ## Phase 3: Remote Tool Confirmation Mechanism [checkpoint: a6e184e]
- [ ] Task: Refactor Execution Engine for Async Wait - [x] Task: Refactor Execution Engine for Async Wait
- [ ] Modify `shell_runner.py` and tool-call loops to support a non-blocking "Pending Confirmation" state instead of launching a GUI modal. - [x] Modify `shell_runner.py` and tool-call loops to support a non-blocking "Pending Confirmation" state instead of launching a GUI modal.
- [ ] Task: Implement Pending Action Queue - [x] Task: Implement Pending Action Queue
- [ ] Create an in-memory (or file-backed) queue for tracking unconfirmed PowerShell scripts. - [x] Create an in-memory (or file-backed) queue for tracking unconfirmed PowerShell scripts.
- [ ] Task: Expose Confirmation API - [x] Task: Expose Confirmation API
- [ ] Create `/api/v1/pending_actions` endpoint (GET) to list pending scripts. - [x] Create `/api/v1/pending_actions` endpoint (GET) to list pending scripts.
- [ ] Create `/api/v1/confirm/{action_id}` endpoint (POST) to approve or deny a script execution. - [x] Create `/api/v1/confirm/{action_id}` endpoint (POST) to approve or deny a script execution.
- [ ] Ensure the AI generation loop correctly resumes upon receiving approval. - [x] Ensure the AI generation loop correctly resumes upon receiving approval.
- [ ] Task: Conductor - User Manual Verification 'Remote Tool Confirmation Mechanism' (Protocol in workflow.md) - [x] Task: Conductor - User Manual Verification 'Remote Tool Confirmation Mechanism' (Protocol in workflow.md) a6e184e
## Phase 4: Session & Context Management via API ## Phase 4: Session & Context Management via API [checkpoint: 7f3a1e2]
- [ ] Task: Expose Session History - [x] Task: Expose Session History
- [ ] Create endpoints to list, retrieve, and delete session logs from the `project_history.toml`. - [x] Create endpoints to list, retrieve, and delete session logs from the `project_history.toml`.
- [ ] Task: Expose Context Configuration - [x] Task: Expose Context Configuration
- [ ] Create endpoints to list currently tracked files/folders in the project scope. - [x] Create endpoints to list currently tracked files/folders in the project scope.
- [ ] Task: Conductor - User Manual Verification 'Session & Context Management via API' (Protocol in workflow.md) - [x] Task: Conductor - User Manual Verification 'Session & Context Management via API' (Protocol in workflow.md) 7f3a1e2
## Phase 5: Dockerization ## Phase 5: Dockerization [checkpoint: 5176b8d]
- [ ] Task: Create Dockerfile - [x] Task: Create Dockerfile
- [ ] Write a `Dockerfile` using `python:3.11-slim` as a base. - [x] Write a `Dockerfile` using `python:3.11-slim` as a base.
- [ ] Configure `uv` inside the container for fast dependency installation. - [x] Configure `uv` inside the container for fast dependency installation.
- [ ] Expose the API port (e.g., 8000) and set the container entrypoint. - [x] Expose the API port (e.g., 8000) and set the container entrypoint.
- [ ] Task: Conductor - User Manual Verification 'Dockerization' (Protocol in workflow.md) - [x] Task: Conductor - User Manual Verification 'Dockerization' (Protocol in workflow.md) 5176b8d

View File

@@ -8,6 +8,7 @@
4. **High Code Coverage:** Aim for >80% code coverage for all modules 4. **High Code Coverage:** Aim for >80% code coverage for all modules
5. **User Experience First:** Every decision should prioritize user experience 5. **User Experience First:** Every decision should prioritize user experience
6. **Non-Interactive & CI-Aware:** Prefer non-interactive commands. Use `CI=true` for watch-mode tools (tests, linters) to ensure single execution. 6. **Non-Interactive & CI-Aware:** Prefer non-interactive commands. Use `CI=true` for watch-mode tools (tests, linters) to ensure single execution.
7. **MMA Tiered Delegation is Mandatory:** The Conductor acts as a Tier 1/2 Orchestrator. You MUST delegate all non-trivial coding to Tier 3 Workers and all error analysis to Tier 4 QA Agents. Do NOT perform large file writes directly.
## Task Workflow ## Task Workflow
@@ -15,17 +16,20 @@ All tasks follow a strict lifecycle:
### Standard Task Workflow ### Standard Task Workflow
0. **Initialize MMA Environment:** Before executing the first task of any track, you MUST activate the `mma-orchestrator` skill (`activate_skill mma-orchestrator`).
1. **Select Task:** Choose the next available task from `plan.md` in sequential order 1. **Select Task:** Choose the next available task from `plan.md` in sequential order
2. **Mark In Progress:** Before beginning work, edit `plan.md` and change the task from `[ ]` to `[~]` 2. **Mark In Progress:** Before beginning work, edit `plan.md` and change the task from `[ ]` to `[~]`
3. **Write Failing Tests (Red Phase):** 3. **Write Failing Tests (Red Phase):**
- Create a new test file for the feature or bug fix. - **Delegate Test Creation:** Do NOT write test code directly. Spawn a Tier 3 Worker (`run_subagent.ps1 -Role Worker`) with a prompt to create the necessary test files and unit tests based on the task criteria.
- Write one or more unit tests that clearly define the expected behavior and acceptance criteria for the task. - Take the code generated by the Worker and apply it.
- **CRITICAL:** Run the tests and confirm that they fail as expected. This is the "Red" phase of TDD. Do not proceed until you have failing tests. - **CRITICAL:** Run the tests and confirm that they fail as expected. This is the "Red" phase of TDD. Do not proceed until you have failing tests.
4. **Implement to Pass Tests (Green Phase):** 4. **Implement to Pass Tests (Green Phase):**
- Write the minimum amount of application code necessary to make the failing tests pass. - **Delegate Implementation:** Do NOT write the implementation code directly. Spawn a Tier 3 Worker (`run_subagent.ps1 -Role Worker`) with a highly specific prompt to write the minimum amount of application code necessary to make the failing tests pass.
- Take the code generated by the Worker and apply it.
- Run the test suite again and confirm that all tests now pass. This is the "Green" phase. - Run the test suite again and confirm that all tests now pass. This is the "Green" phase.
5. **Refactor (Optional but Recommended):** 5. **Refactor (Optional but Recommended):**
@@ -84,7 +88,8 @@ All tasks follow a strict lifecycle:
- Before execution, you **must** announce the exact shell command you will use to run the tests. - Before execution, you **must** announce the exact shell command you will use to run the tests.
- **Example Announcement:** "I will now run the automated test suite to verify the phase. **Command:** `CI=true npm test`" - **Example Announcement:** "I will now run the automated test suite to verify the phase. **Command:** `CI=true npm test`"
- Execute the announced command. - Execute the announced command.
- If tests fail, you **must** inform the user and begin debugging. You may attempt to propose a fix a **maximum of two times**. If the tests still fail after your second proposed fix, you **must stop**, report the persistent failure, and ask the user for guidance. - If tests fail with significant output (e.g., a large traceback), **DO NOT** attempt to read the raw `stderr` directly into your context. Instead, pipe the output to a log file and **spawn a Tier 4 QA Agent (`run_subagent.ps1 -Role QA`)** to summarize the failure.
- You **must** inform the user and begin debugging using the QA Agent's summary. You may attempt to propose a fix a **maximum of two times**. If the tests still fail after your second proposed fix, you **must stop**, report the persistent failure, and ask the user for guidance.
4. **Execute Automated API Hook Verification:** 4. **Execute Automated API Hook Verification:**
- **CRITICAL:** The Conductor agent will now automatically execute verification tasks using the application's API hooks. - **CRITICAL:** The Conductor agent will now automatically execute verification tasks using the application's API hooks.

View File

@@ -7,9 +7,9 @@ history_trunc_limit = 8000
system_prompt = "" system_prompt = ""
[theme] [theme]
palette = "Gold" palette = "ImGui Dark"
font_size = 14.0 font_size = 16.0
scale = 1.2000000476837158 scale = 1.0
font_path = "" font_path = ""
[projects] [projects]
@@ -32,3 +32,7 @@ active = "C:\\projects\\manual_slop\\tests\\temp_project.toml"
"Operations Hub" = true "Operations Hub" = true
Theme = true Theme = true
Diagnostics = true Diagnostics = true
[headless]
port = 8000
api_key = ""

288
gui_2.py
View File

@@ -6,6 +6,7 @@ import math
import json import json
import sys import sys
import os import os
import uuid
from pathlib import Path from pathlib import Path
from tkinter import filedialog, Tk from tkinter import filedialog, Tk
import aggregate import aggregate
@@ -22,6 +23,9 @@ import api_hooks
import mcp_client import mcp_client
from performance_monitor import PerformanceMonitor from performance_monitor import PerformanceMonitor
from fastapi import FastAPI, Depends, HTTPException, Security
from fastapi.security.api_key import APIKeyHeader
from pydantic import BaseModel
from imgui_bundle import imgui, hello_imgui, immapp from imgui_bundle import imgui, hello_imgui, immapp
CONFIG_PATH = Path("config.toml") CONFIG_PATH = Path("config.toml")
@@ -87,10 +91,8 @@ def _parse_history_entries(history: list[str], roles: list[str] | None = None) -
class ConfirmDialog: class ConfirmDialog:
_next_id = 0
def __init__(self, script: str, base_dir: str): def __init__(self, script: str, base_dir: str):
ConfirmDialog._next_id += 1 self._uid = str(uuid.uuid4())
self._uid = ConfirmDialog._next_id
self._script = str(script) if script is not None else "" self._script = str(script) if script is not None else ""
self._base_dir = str(base_dir) if base_dir is not None else "" self._base_dir = str(base_dir) if base_dir is not None else ""
self._condition = threading.Condition() self._condition = threading.Condition()
@@ -188,6 +190,7 @@ class App:
self._pending_dialog: ConfirmDialog | None = None self._pending_dialog: ConfirmDialog | None = None
self._pending_dialog_open = False self._pending_dialog_open = False
self._pending_dialog_lock = threading.Lock() self._pending_dialog_lock = threading.Lock()
self._pending_actions: dict[str, ConfirmDialog] = {}
self._tool_log: list[tuple[str, str]] = [] self._tool_log: list[tuple[str, str]] = []
self._comms_log: list[dict] = [] self._comms_log: list[dict] = []
@@ -303,6 +306,192 @@ class App:
self._discussion_names_cache = [] self._discussion_names_cache = []
self._discussion_names_dirty = True self._discussion_names_dirty = True
def create_api(self) -> FastAPI:
api = FastAPI(title="Manual Slop Headless API")
class GenerateRequest(BaseModel):
prompt: str
auto_add_history: bool = True
temperature: float | None = None
max_tokens: int | None = None
class ConfirmRequest(BaseModel):
approved: bool
API_KEY_NAME = "X-API-KEY"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
async def get_api_key(header_key: str = Depends(api_key_header)):
headless_cfg = self.config.get("headless", {})
config_key = headless_cfg.get("api_key", "").strip()
env_key = os.environ.get("SLOP_API_KEY", "").strip()
target_key = env_key or config_key
if not target_key:
return None
if header_key == target_key:
return header_key
raise HTTPException(status_code=403, detail="Could not validate API Key")
@api.get("/health")
def health():
return {"status": "ok"}
@api.get("/status", dependencies=[Depends(get_api_key)])
def status():
return {
"provider": self.current_provider,
"model": self.current_model,
"active_project": self.active_project_path,
"ai_status": self.ai_status,
"session_usage": self.session_usage
}
@api.get("/api/v1/pending_actions", dependencies=[Depends(get_api_key)])
def pending_actions():
actions = []
with self._pending_dialog_lock:
# Include multi-actions from headless mode
for uid, dialog in self._pending_actions.items():
actions.append({
"action_id": uid,
"script": dialog._script,
"base_dir": dialog._base_dir
})
# Include single active dialog from GUI mode
if self._pending_dialog:
actions.append({
"action_id": self._pending_dialog._uid,
"script": self._pending_dialog._script,
"base_dir": self._pending_dialog._base_dir
})
return actions
@api.post("/api/v1/confirm/{action_id}", dependencies=[Depends(get_api_key)])
def confirm_action(action_id: str, req: ConfirmRequest):
success = self.resolve_pending_action(action_id, req.approved)
if not success:
raise HTTPException(status_code=404, detail=f"Action ID {action_id} not found")
return {"status": "success", "action_id": action_id, "approved": req.approved}
@api.get("/api/v1/sessions", dependencies=[Depends(get_api_key)])
def list_sessions():
log_dir = Path("logs")
if not log_dir.exists():
return []
return sorted([f.name for f in log_dir.glob("*.log")], reverse=True)
@api.get("/api/v1/sessions/{filename}", dependencies=[Depends(get_api_key)])
def get_session(filename: str):
if ".." in filename or "/" in filename or "\\" in filename:
raise HTTPException(status_code=400, detail="Invalid filename")
log_path = Path("logs") / filename
if not log_path.exists() or not log_path.is_file():
raise HTTPException(status_code=404, detail="Session log not found")
try:
content = log_path.read_text(encoding="utf-8")
return {"filename": filename, "content": content}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@api.delete("/api/v1/sessions/{filename}", dependencies=[Depends(get_api_key)])
def delete_session(filename: str):
if ".." in filename or "/" in filename or "\\" in filename:
raise HTTPException(status_code=400, detail="Invalid filename")
log_path = Path("logs") / filename
if not log_path.exists() or not log_path.is_file():
raise HTTPException(status_code=404, detail="Session log not found")
try:
log_path.unlink()
return {"status": "success", "message": f"Deleted {filename}"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@api.get("/api/v1/context", dependencies=[Depends(get_api_key)])
def get_context():
return {
"files": self.files,
"screenshots": self.screenshots,
"files_base_dir": self.ui_files_base_dir,
"screenshots_base_dir": self.ui_shots_base_dir
}
@api.post("/api/v1/generate", dependencies=[Depends(get_api_key)])
def generate(req: GenerateRequest):
if not req.prompt.strip():
raise HTTPException(status_code=400, detail="Prompt cannot be empty")
with self._send_thread_lock:
start_time = time.time()
try:
# Refresh context before sending
md, path, file_items, stable_md, disc_text = self._do_generate()
self.last_md = md
self.last_md_path = path
self.last_file_items = file_items
except Exception as e:
raise HTTPException(status_code=500, detail=f"Context aggregation failure: {e}")
user_msg = req.prompt
base_dir = self.ui_files_base_dir
csp = filter(bool, [self.ui_global_system_prompt.strip(), self.ui_project_system_prompt.strip()])
ai_client.set_custom_system_prompt("\n\n".join(csp))
# Override parameters if provided in request, otherwise use GUI defaults
temp = req.temperature if req.temperature is not None else self.temperature
tokens = req.max_tokens if req.max_tokens is not None else self.max_tokens
ai_client.set_model_params(temp, tokens, self.history_trunc_limit)
ai_client.set_agent_tools(self.ui_agent_tools)
if req.auto_add_history:
with self._pending_history_adds_lock:
self._pending_history_adds.append({
"role": "User",
"content": user_msg,
"collapsed": False,
"ts": project_manager.now_ts()
})
try:
resp = ai_client.send(stable_md, user_msg, base_dir, self.last_file_items, disc_text)
if req.auto_add_history:
with self._pending_history_adds_lock:
self._pending_history_adds.append({
"role": "AI",
"content": resp,
"collapsed": False,
"ts": project_manager.now_ts()
})
# Ensure metrics are updated for the response
self._recalculate_session_usage()
duration = time.time() - start_time
return {
"text": resp,
"metadata": {
"provider": self.current_provider,
"model": self.current_model,
"duration_sec": round(duration, 3),
"timestamp": project_manager.now_ts()
},
"usage": self.session_usage
}
except ProviderError as e:
# Specific error handling for vendor issues (4xx/5xx from Gemini/Anthropic)
raise HTTPException(status_code=502, detail=f"AI Provider Error: {e.ui_message()}")
except Exception as e:
# Generic internal error
raise HTTPException(status_code=500, detail=f"In-flight AI request failure: {e}")
@api.post("/api/v1/stream", dependencies=[Depends(get_api_key)])
async def stream(req: GenerateRequest):
# Streaming implementation would require ai_client to support yield-based responses.
# Currently added as a placeholder to satisfy spec requirements.
raise HTTPException(status_code=501, detail="Streaming endpoint (/api/v1/stream) is not yet supported in this version.")
return api
# ---------------------------------------------------------------- project loading # ---------------------------------------------------------------- project loading
def _cb_new_project_automated(self, user_data): def _cb_new_project_automated(self, user_data):
@@ -739,8 +928,16 @@ class App:
def _confirm_and_run(self, script: str, base_dir: str) -> str | None: def _confirm_and_run(self, script: str, base_dir: str) -> str | None:
print(f"[DEBUG] _confirm_and_run triggered for script length: {len(script)}") print(f"[DEBUG] _confirm_and_run triggered for script length: {len(script)}")
dialog = ConfirmDialog(script, base_dir) dialog = ConfirmDialog(script, base_dir)
with self._pending_dialog_lock:
self._pending_dialog = dialog is_headless = "--headless" in sys.argv
if is_headless:
with self._pending_dialog_lock:
self._pending_actions[dialog._uid] = dialog
print(f"[PENDING_ACTION] Created action {dialog._uid}")
else:
with self._pending_dialog_lock:
self._pending_dialog = dialog
# Notify API hook subscribers # Notify API hook subscribers
if self.test_hooks_enabled and hasattr(self, '_api_event_queue'): if self.test_hooks_enabled and hasattr(self, '_api_event_queue'):
@@ -748,12 +945,19 @@ class App:
with self._api_event_queue_lock: with self._api_event_queue_lock:
self._api_event_queue.append({ self._api_event_queue.append({
"type": "script_confirmation_required", "type": "script_confirmation_required",
"action_id": dialog._uid,
"script": str(script), "script": str(script),
"base_dir": str(base_dir), "base_dir": str(base_dir),
"ts": time.time() "ts": time.time()
}) })
approved, final_script = dialog.wait() approved, final_script = dialog.wait()
if is_headless:
with self._pending_dialog_lock:
if dialog._uid in self._pending_actions:
del self._pending_actions[dialog._uid]
print(f"[DEBUG] _confirm_and_run result: approved={approved}") print(f"[DEBUG] _confirm_and_run result: approved={approved}")
if not approved: if not approved:
self._append_tool_log(final_script, "REJECTED by user") self._append_tool_log(final_script, "REJECTED by user")
@@ -766,6 +970,24 @@ class App:
self.ai_status = "powershell done, awaiting AI..." self.ai_status = "powershell done, awaiting AI..."
return output return output
def resolve_pending_action(self, action_id: str, approved: bool):
with self._pending_dialog_lock:
if action_id in self._pending_actions:
dialog = self._pending_actions[action_id]
with dialog._condition:
dialog._approved = approved
dialog._done = True
dialog._condition.notify_all()
return True
elif self._pending_dialog and self._pending_dialog._uid == action_id:
dialog = self._pending_dialog
with dialog._condition:
dialog._approved = approved
dialog._done = True
dialog._condition.notify_all()
return True
return False
def _append_tool_log(self, script: str, result: str): def _append_tool_log(self, script: str, result: str):
self._tool_log.append((script, result, time.time())) self._tool_log.append((script, result, time.time()))
self.ui_last_script_text = script self.ui_last_script_text = script
@@ -2001,33 +2223,45 @@ class App:
def run(self): def run(self):
"""Initializes the ImGui runner and starts the main application loop.""" """Initializes the ImGui runner and starts the main application loop."""
theme.load_from_config(self.config) if "--headless" in sys.argv:
print("Headless mode active")
self._fetch_models(self.current_provider)
import uvicorn
headless_cfg = self.config.get("headless", {})
port = headless_cfg.get("port", 8000)
api = self.create_api()
uvicorn.run(api, host="0.0.0.0", port=port)
else:
theme.load_from_config(self.config)
self.runner_params = hello_imgui.RunnerParams() self.runner_params = hello_imgui.RunnerParams()
self.runner_params.app_window_params.window_title = "manual slop" self.runner_params.app_window_params.window_title = "manual slop"
self.runner_params.app_window_params.window_geometry.size = (1680, 1200) self.runner_params.app_window_params.window_geometry.size = (1680, 1200)
self.runner_params.imgui_window_params.enable_viewports = False self.runner_params.imgui_window_params.enable_viewports = False
self.runner_params.imgui_window_params.default_imgui_window_type = hello_imgui.DefaultImGuiWindowType.provide_full_screen_dock_space self.runner_params.imgui_window_params.default_imgui_window_type = hello_imgui.DefaultImGuiWindowType.provide_full_screen_dock_space
self.runner_params.fps_idling.enable_idling = False self.runner_params.fps_idling.enable_idling = False
self.runner_params.imgui_window_params.show_menu_bar = True self.runner_params.imgui_window_params.show_menu_bar = True
self.runner_params.ini_folder_type = hello_imgui.IniFolderType.current_folder self.runner_params.ini_folder_type = hello_imgui.IniFolderType.current_folder
self.runner_params.ini_filename = "manualslop_layout.ini" self.runner_params.ini_filename = "manualslop_layout.ini"
self.runner_params.callbacks.show_gui = self._gui_func self.runner_params.callbacks.show_gui = self._gui_func
self.runner_params.callbacks.show_menus = self._show_menus self.runner_params.callbacks.show_menus = self._show_menus
self.runner_params.callbacks.load_additional_fonts = self._load_fonts self.runner_params.callbacks.load_additional_fonts = self._load_fonts
self.runner_params.callbacks.post_init = self._post_init self.runner_params.callbacks.post_init = self._post_init
self._fetch_models(self.current_provider) self._fetch_models(self.current_provider)
# Start API hooks server (if enabled) # Start API hooks server (if enabled)
self.hook_server = api_hooks.HookServer(self) self.hook_server = api_hooks.HookServer(self)
self.hook_server.start() self.hook_server.start()
immapp.run(self.runner_params) immapp.run(self.runner_params)
# On exit
self.hook_server.stop()
# On exit
self.hook_server.stop()
self.perf_monitor.stop() self.perf_monitor.stop()
ai_client.cleanup() # Destroy active API caches to stop billing ai_client.cleanup() # Destroy active API caches to stop billing
self._flush_to_project() self._flush_to_project()

View File

@@ -8,5 +8,5 @@ active = "main"
[discussions.main] [discussions.main]
git_commit = "" git_commit = ""
last_updated = "2026-02-25T11:19:43" last_updated = "2026-02-25T13:21:57"
history = [] history = []

BIN
test_output.log Normal file

Binary file not shown.

BIN
test_output_final.log Normal file

Binary file not shown.

BIN
test_output_phase4.log Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -9,7 +9,56 @@ auto_add = true
[discussions.main] [discussions.main]
git_commit = "" git_commit = ""
last_updated = "2026-02-25T11:21:27" last_updated = "2026-02-25T13:21:57"
history = [ history = [
"@2026-02-25T11:21:23\nSystem:\n[PERFORMANCE ALERT] CPU usage high: 82.8%. Please consider optimizing recent changes or reducing load.", "@1772042443.1159382\nUser:\nStress test entry 0 Stress test entry 0 Stress test entry 0 Stress test entry 0 Stress test entry 0",
"@1772042443.1159382\nUser:\nStress test entry 1 Stress test entry 1 Stress test entry 1 Stress test entry 1 Stress test entry 1",
"@1772042443.1159382\nUser:\nStress test entry 2 Stress test entry 2 Stress test entry 2 Stress test entry 2 Stress test entry 2",
"@1772042443.1159382\nUser:\nStress test entry 3 Stress test entry 3 Stress test entry 3 Stress test entry 3 Stress test entry 3",
"@1772042443.1159382\nUser:\nStress test entry 4 Stress test entry 4 Stress test entry 4 Stress test entry 4 Stress test entry 4",
"@1772042443.1159382\nUser:\nStress test entry 5 Stress test entry 5 Stress test entry 5 Stress test entry 5 Stress test entry 5",
"@1772042443.1159382\nUser:\nStress test entry 6 Stress test entry 6 Stress test entry 6 Stress test entry 6 Stress test entry 6",
"@1772042443.1159382\nUser:\nStress test entry 7 Stress test entry 7 Stress test entry 7 Stress test entry 7 Stress test entry 7",
"@1772042443.1159382\nUser:\nStress test entry 8 Stress test entry 8 Stress test entry 8 Stress test entry 8 Stress test entry 8",
"@1772042443.1159382\nUser:\nStress test entry 9 Stress test entry 9 Stress test entry 9 Stress test entry 9 Stress test entry 9",
"@1772042443.1159382\nUser:\nStress test entry 10 Stress test entry 10 Stress test entry 10 Stress test entry 10 Stress test entry 10",
"@1772042443.1159382\nUser:\nStress test entry 11 Stress test entry 11 Stress test entry 11 Stress test entry 11 Stress test entry 11",
"@1772042443.1159382\nUser:\nStress test entry 12 Stress test entry 12 Stress test entry 12 Stress test entry 12 Stress test entry 12",
"@1772042443.1159382\nUser:\nStress test entry 13 Stress test entry 13 Stress test entry 13 Stress test entry 13 Stress test entry 13",
"@1772042443.1159382\nUser:\nStress test entry 14 Stress test entry 14 Stress test entry 14 Stress test entry 14 Stress test entry 14",
"@1772042443.1159382\nUser:\nStress test entry 15 Stress test entry 15 Stress test entry 15 Stress test entry 15 Stress test entry 15",
"@1772042443.1159382\nUser:\nStress test entry 16 Stress test entry 16 Stress test entry 16 Stress test entry 16 Stress test entry 16",
"@1772042443.1159382\nUser:\nStress test entry 17 Stress test entry 17 Stress test entry 17 Stress test entry 17 Stress test entry 17",
"@1772042443.1159382\nUser:\nStress test entry 18 Stress test entry 18 Stress test entry 18 Stress test entry 18 Stress test entry 18",
"@1772042443.1159382\nUser:\nStress test entry 19 Stress test entry 19 Stress test entry 19 Stress test entry 19 Stress test entry 19",
"@1772042443.1159382\nUser:\nStress test entry 20 Stress test entry 20 Stress test entry 20 Stress test entry 20 Stress test entry 20",
"@1772042443.1159382\nUser:\nStress test entry 21 Stress test entry 21 Stress test entry 21 Stress test entry 21 Stress test entry 21",
"@1772042443.1159382\nUser:\nStress test entry 22 Stress test entry 22 Stress test entry 22 Stress test entry 22 Stress test entry 22",
"@1772042443.1159382\nUser:\nStress test entry 23 Stress test entry 23 Stress test entry 23 Stress test entry 23 Stress test entry 23",
"@1772042443.1159382\nUser:\nStress test entry 24 Stress test entry 24 Stress test entry 24 Stress test entry 24 Stress test entry 24",
"@1772042443.1159382\nUser:\nStress test entry 25 Stress test entry 25 Stress test entry 25 Stress test entry 25 Stress test entry 25",
"@1772042443.1159382\nUser:\nStress test entry 26 Stress test entry 26 Stress test entry 26 Stress test entry 26 Stress test entry 26",
"@1772042443.1159382\nUser:\nStress test entry 27 Stress test entry 27 Stress test entry 27 Stress test entry 27 Stress test entry 27",
"@1772042443.1159382\nUser:\nStress test entry 28 Stress test entry 28 Stress test entry 28 Stress test entry 28 Stress test entry 28",
"@1772042443.1159382\nUser:\nStress test entry 29 Stress test entry 29 Stress test entry 29 Stress test entry 29 Stress test entry 29",
"@1772042443.1159382\nUser:\nStress test entry 30 Stress test entry 30 Stress test entry 30 Stress test entry 30 Stress test entry 30",
"@1772042443.1159382\nUser:\nStress test entry 31 Stress test entry 31 Stress test entry 31 Stress test entry 31 Stress test entry 31",
"@1772042443.1159382\nUser:\nStress test entry 32 Stress test entry 32 Stress test entry 32 Stress test entry 32 Stress test entry 32",
"@1772042443.1159382\nUser:\nStress test entry 33 Stress test entry 33 Stress test entry 33 Stress test entry 33 Stress test entry 33",
"@1772042443.1159382\nUser:\nStress test entry 34 Stress test entry 34 Stress test entry 34 Stress test entry 34 Stress test entry 34",
"@1772042443.1159382\nUser:\nStress test entry 35 Stress test entry 35 Stress test entry 35 Stress test entry 35 Stress test entry 35",
"@1772042443.1159382\nUser:\nStress test entry 36 Stress test entry 36 Stress test entry 36 Stress test entry 36 Stress test entry 36",
"@1772042443.1159382\nUser:\nStress test entry 37 Stress test entry 37 Stress test entry 37 Stress test entry 37 Stress test entry 37",
"@1772042443.1159382\nUser:\nStress test entry 38 Stress test entry 38 Stress test entry 38 Stress test entry 38 Stress test entry 38",
"@1772042443.1159382\nUser:\nStress test entry 39 Stress test entry 39 Stress test entry 39 Stress test entry 39 Stress test entry 39",
"@1772042443.1159382\nUser:\nStress test entry 40 Stress test entry 40 Stress test entry 40 Stress test entry 40 Stress test entry 40",
"@1772042443.1159382\nUser:\nStress test entry 41 Stress test entry 41 Stress test entry 41 Stress test entry 41 Stress test entry 41",
"@1772042443.1159382\nUser:\nStress test entry 42 Stress test entry 42 Stress test entry 42 Stress test entry 42 Stress test entry 42",
"@1772042443.1159382\nUser:\nStress test entry 43 Stress test entry 43 Stress test entry 43 Stress test entry 43 Stress test entry 43",
"@1772042443.1159382\nUser:\nStress test entry 44 Stress test entry 44 Stress test entry 44 Stress test entry 44 Stress test entry 44",
"@1772042443.1159382\nUser:\nStress test entry 45 Stress test entry 45 Stress test entry 45 Stress test entry 45 Stress test entry 45",
"@1772042443.1159382\nUser:\nStress test entry 46 Stress test entry 46 Stress test entry 46 Stress test entry 46 Stress test entry 46",
"@1772042443.1159382\nUser:\nStress test entry 47 Stress test entry 47 Stress test entry 47 Stress test entry 47 Stress test entry 47",
"@1772042443.1159382\nUser:\nStress test entry 48 Stress test entry 48 Stress test entry 48 Stress test entry 48 Stress test entry 48",
"@1772042443.1159382\nUser:\nStress test entry 49 Stress test entry 49 Stress test entry 49 Stress test entry 49 Stress test entry 49",
] ]

110
tests/test_headless_api.py Normal file
View File

@@ -0,0 +1,110 @@
import unittest
from fastapi.testclient import TestClient
import gui_2
from unittest.mock import patch, MagicMock
from pathlib import Path
class TestHeadlessAPI(unittest.TestCase):
def setUp(self):
# We need an App instance to initialize the API, but we want to avoid GUI stuff
with patch('gui_2.session_logger.open_session'), \
patch('gui_2.ai_client.set_provider'), \
patch('gui_2.session_logger.close_session'):
self.app_instance = gui_2.App()
# Clear any leftover state
self.app_instance._pending_actions = {}
self.app_instance._pending_dialog = None
self.api = self.app_instance.create_api()
self.client = TestClient(self.api)
def test_health_endpoint(self):
response = self.client.get("/health")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"status": "ok"})
def test_status_endpoint_unauthorized(self):
# Ensure a key is required
with patch.dict(self.app_instance.config, {"headless": {"api_key": "some-required-key"}}):
response = self.client.get("/status")
self.assertEqual(response.status_code, 403)
def test_status_endpoint_authorized(self):
# We'll use a test key
headers = {"X-API-KEY": "test-secret-key"}
with patch.dict(self.app_instance.config, {"headless": {"api_key": "test-secret-key"}}):
response = self.client.get("/status", headers=headers)
self.assertEqual(response.status_code, 200)
def test_generate_endpoint(self):
payload = {
"prompt": "Hello AI"
}
# Mock ai_client.send and get_comms_log
with patch('gui_2.ai_client.send') as mock_send, \
patch('gui_2.ai_client.get_comms_log') as mock_log:
mock_send.return_value = "Hello from Mock AI"
mock_log.return_value = [{
"kind": "response",
"payload": {
"usage": {"input_tokens": 10, "output_tokens": 5}
}
}]
response = self.client.post("/api/v1/generate", json=payload)
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data["text"], "Hello from Mock AI")
self.assertIn("metadata", data)
self.assertEqual(data["usage"]["input_tokens"], 10)
def test_pending_actions_endpoint(self):
# Manually add a pending action
with patch('gui_2.uuid.uuid4', return_value="test-action-id"):
dialog = gui_2.ConfirmDialog("dir", ".")
self.app_instance._pending_actions[dialog._uid] = dialog
response = self.client.get("/api/v1/pending_actions")
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(len(data), 1)
self.assertEqual(data[0]["action_id"], "test-action-id")
def test_confirm_action_endpoint(self):
# Manually add a pending action
with patch('gui_2.uuid.uuid4', return_value="test-confirm-id"):
dialog = gui_2.ConfirmDialog("dir", ".")
self.app_instance._pending_actions[dialog._uid] = dialog
payload = {"approved": True}
response = self.client.post("/api/v1/confirm/test-confirm-id", json=payload)
self.assertEqual(response.status_code, 200)
self.assertTrue(dialog._done)
self.assertTrue(dialog._approved)
def test_list_sessions_endpoint(self):
# Ensure logs directory exists
Path("logs").mkdir(exist_ok=True)
# Create a dummy log
dummy_log = Path("logs/test_session_api.log")
dummy_log.write_text("dummy content")
try:
response = self.client.get("/api/v1/sessions")
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIn("test_session_api.log", data)
finally:
if dummy_log.exists():
dummy_log.unlink()
def test_get_context_endpoint(self):
response = self.client.get("/api/v1/context")
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIn("files", data)
self.assertIn("screenshots", data)
self.assertIn("files_base_dir", data)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,48 @@
import sys
import unittest
from unittest.mock import patch, MagicMock
import gui_2
class TestHeadlessStartup(unittest.TestCase):
@patch('gui_2.immapp.run')
@patch('gui_2.api_hooks.HookServer')
@patch('gui_2.save_config')
@patch('gui_2.ai_client.cleanup')
@patch('uvicorn.run') # Mock uvicorn.run to prevent hanging
def test_headless_flag_prevents_gui_run(self, mock_uvicorn_run, mock_cleanup, mock_save_config, mock_hook_server, mock_immapp_run):
# Setup mock argv with --headless
test_args = ["gui_2.py", "--headless"]
with patch.object(sys, 'argv', test_args):
with patch('gui_2.session_logger.close_session'), \
patch('gui_2.session_logger.open_session'):
app = gui_2.App()
# Mock _fetch_models to avoid network calls
app._fetch_models = MagicMock()
app.run()
# Expectation: immapp.run should NOT be called in headless mode
mock_immapp_run.assert_not_called()
# Expectation: uvicorn.run SHOULD be called
mock_uvicorn_run.assert_called_once()
@patch('gui_2.immapp.run')
def test_normal_startup_calls_gui_run(self, mock_immapp_run):
test_args = ["gui_2.py"]
with patch.object(sys, 'argv', test_args):
# In normal mode, it should still call immapp.run
with patch('gui_2.api_hooks.HookServer'), \
patch('gui_2.save_config'), \
patch('gui_2.ai_client.cleanup'), \
patch('gui_2.session_logger.close_session'), \
patch('gui_2.session_logger.open_session'):
app = gui_2.App()
app._fetch_models = MagicMock()
app.run()
mock_immapp_run.assert_called_once()
if __name__ == "__main__":
unittest.main()