Add task detail view, pipeline visualization, approve/reject workflow
API (web/api.py) — 5 new endpoints:
GET /api/tasks/{id}/pipeline — agent_logs as pipeline steps
GET /api/tasks/{id}/full — task + steps + related decisions
POST /api/tasks/{id}/approve — mark done, optionally add decision
POST /api/tasks/{id}/reject — return to pending with reason
POST /api/tasks/{id}/run — launch pipeline in background (202)
Frontend:
TaskDetail (/task/:id) — full task page with:
- Pipeline graph: role cards with icons, arrows, status colors
- Click step → expand output (pre-formatted, JSON detected)
- Action bar: Approve (with optional decision), Reject, Run Pipeline
- Polling for live pipeline updates
Dashboard: review_tasks badge ("awaiting review" in yellow)
ProjectView: task rows are now clickable links to /task/:id
Runner: output_summary no longer truncated (full output for GUI).
Models: get_project_summary includes review_tasks count.
13 new API tests, 105 total, all passing. Frontend builds clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fabae74c19
commit
38c252fc1b
9 changed files with 550 additions and 7 deletions
107
web/api.py
107
web/api.py
|
|
@ -3,6 +3,7 @@ Kin Web API — FastAPI backend reading ~/.kin/kin.db via core.models.
|
|||
Run: uvicorn web.api:app --reload --port 8420
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -11,6 +12,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|||
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from core.db import init_db
|
||||
|
|
@ -134,6 +136,111 @@ def create_task(body: TaskCreate):
|
|||
return t
|
||||
|
||||
|
||||
@app.get("/api/tasks/{task_id}/pipeline")
|
||||
def get_task_pipeline(task_id: str):
|
||||
"""Get agent_logs for a task (pipeline steps)."""
|
||||
conn = get_conn()
|
||||
t = models.get_task(conn, task_id)
|
||||
if not t:
|
||||
conn.close()
|
||||
raise HTTPException(404, f"Task '{task_id}' not found")
|
||||
rows = conn.execute(
|
||||
"""SELECT id, agent_role, action, output_summary, success,
|
||||
duration_seconds, tokens_used, model, cost_usd, created_at
|
||||
FROM agent_logs WHERE task_id = ? ORDER BY created_at""",
|
||||
(task_id,),
|
||||
).fetchall()
|
||||
steps = [dict(r) for r in rows]
|
||||
conn.close()
|
||||
return steps
|
||||
|
||||
|
||||
@app.get("/api/tasks/{task_id}/full")
|
||||
def get_task_full(task_id: str):
|
||||
"""Task + pipeline steps + related decisions."""
|
||||
conn = get_conn()
|
||||
t = models.get_task(conn, task_id)
|
||||
if not t:
|
||||
conn.close()
|
||||
raise HTTPException(404, f"Task '{task_id}' not found")
|
||||
rows = conn.execute(
|
||||
"""SELECT id, agent_role, action, output_summary, success,
|
||||
duration_seconds, tokens_used, model, cost_usd, created_at
|
||||
FROM agent_logs WHERE task_id = ? ORDER BY created_at""",
|
||||
(task_id,),
|
||||
).fetchall()
|
||||
steps = [dict(r) for r in rows]
|
||||
decisions = models.get_decisions(conn, t["project_id"])
|
||||
# Filter to decisions linked to this task
|
||||
task_decisions = [d for d in decisions if d.get("task_id") == task_id]
|
||||
conn.close()
|
||||
return {**t, "pipeline_steps": steps, "related_decisions": task_decisions}
|
||||
|
||||
|
||||
class TaskApprove(BaseModel):
|
||||
decision_title: str | None = None
|
||||
decision_description: str | None = None
|
||||
decision_type: str = "decision"
|
||||
|
||||
|
||||
@app.post("/api/tasks/{task_id}/approve")
|
||||
def approve_task(task_id: str, body: TaskApprove | None = None):
|
||||
"""Approve a task: set status=done, optionally add a decision."""
|
||||
conn = get_conn()
|
||||
t = models.get_task(conn, task_id)
|
||||
if not t:
|
||||
conn.close()
|
||||
raise HTTPException(404, f"Task '{task_id}' not found")
|
||||
models.update_task(conn, task_id, status="done")
|
||||
decision = None
|
||||
if body and body.decision_title:
|
||||
decision = models.add_decision(
|
||||
conn, t["project_id"], body.decision_type,
|
||||
body.decision_title, body.decision_description or body.decision_title,
|
||||
task_id=task_id,
|
||||
)
|
||||
conn.close()
|
||||
return {"status": "done", "decision": decision}
|
||||
|
||||
|
||||
class TaskReject(BaseModel):
|
||||
reason: str
|
||||
|
||||
|
||||
@app.post("/api/tasks/{task_id}/reject")
|
||||
def reject_task(task_id: str, body: TaskReject):
|
||||
"""Reject a task: set status=pending with reason in review field."""
|
||||
conn = get_conn()
|
||||
t = models.get_task(conn, task_id)
|
||||
if not t:
|
||||
conn.close()
|
||||
raise HTTPException(404, f"Task '{task_id}' not found")
|
||||
models.update_task(conn, task_id, status="pending", review={"rejected": body.reason})
|
||||
conn.close()
|
||||
return {"status": "pending", "reason": body.reason}
|
||||
|
||||
|
||||
@app.post("/api/tasks/{task_id}/run")
|
||||
def run_task(task_id: str):
|
||||
"""Launch pipeline for a task in background. Returns 202."""
|
||||
conn = get_conn()
|
||||
t = models.get_task(conn, task_id)
|
||||
if not t:
|
||||
conn.close()
|
||||
raise HTTPException(404, f"Task '{task_id}' not found")
|
||||
conn.close()
|
||||
# Launch kin run in background subprocess
|
||||
kin_root = Path(__file__).parent.parent
|
||||
subprocess.Popen(
|
||||
[sys.executable, "-m", "cli.main", "run", task_id, "--db",
|
||||
str(DB_PATH)],
|
||||
cwd=str(kin_root),
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
return JSONResponse({"status": "started", "task_id": task_id}, status_code=202)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Decisions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue