Compare commits
7 Commits
33a603c0c5
...
63fd391dff
| Author | SHA1 | Date | |
|---|---|---|---|
| 63fd391dff | |||
| 6eb88a4041 | |||
| 28fcaa7eae | |||
| 386e36a92b | |||
| 1491619310 | |||
| 4e0bcd5188 | |||
| d5f056c3d1 |
21
.dockerignore
Normal file
21
.dockerignore
Normal 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
34
Dockerfile
Normal 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"]
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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/)*
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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.
|
||||||
|
|||||||
10
config.toml
10
config.toml
@@ -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
288
gui_2.py
@@ -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()
|
||||||
|
|||||||
@@ -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
BIN
test_output.log
Normal file
Binary file not shown.
BIN
test_output_final.log
Normal file
BIN
test_output_final.log
Normal file
Binary file not shown.
BIN
test_output_phase4.log
Normal file
BIN
test_output_phase4.log
Normal file
Binary file not shown.
BIN
test_output_phase4_fixed.log
Normal file
BIN
test_output_phase4_fixed.log
Normal file
Binary file not shown.
@@ -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
110
tests/test_headless_api.py
Normal 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()
|
||||||
48
tests/test_headless_startup.py
Normal file
48
tests/test_headless_startup.py
Normal 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()
|
||||||
Reference in New Issue
Block a user