diff --git a/CLAUDE.md b/CLAUDE.md index 4b9c01a..da4bb57 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,6 +21,17 @@ source .venv/bin/activate pytest ``` +## Before Committing +Always run linting, formatting, type checking, and tests before creating commits: +```bash +source .venv/bin/activate +ruff check . +ruff format . +mypy server/ +pytest +``` +Or use `make check` which runs all of them. + ## Project Structure - `server/` — FastAPI backend - `public/` — Static frontend files diff --git a/Makefile b/Makefile index 972ca4e..4ac39f5 100644 --- a/Makefile +++ b/Makefile @@ -1,31 +1,40 @@ .DEFAULT_GOAL := help -PYTHON := python PORT := 3000 +# Prefer project .venv via uv so `make up` works without `source .venv/bin/activate` +UV_RUN := uv run -.PHONY: help up down test lint format typecheck check clean +.PHONY: help setup install up down test lint format typecheck check clean help: ## Show this help @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' -up: ## Start the server (port 3000) - uvicorn server.main:app --port $(PORT) --reload +setup install: ## Full local setup: uv venv + dev deps, Playwright Chromium, Claude hooks + uv sync --extra dev + uv run python -m playwright install chromium + bash scripts/setup.sh + @echo "" + @echo "Setup complete. Start the app: make up" + @echo "If Playwright/e2e fails (missing OS libs), run: uv run python -m playwright install --with-deps chromium" + +up: ## Start the server (port 3000; override with PORT=3001) + $(UV_RUN) uvicorn server.main:app --port $(PORT) --reload down: ## Stop the server @pkill -f "uvicorn server.main:app" 2>/dev/null && echo "Server stopped" || echo "Server not running" test: ## Run all tests - $(PYTHON) -m pytest tests/ -v + $(UV_RUN) python -m pytest tests/ -v lint: ## Run ruff linter and format check - ruff check . - ruff format --check . + $(UV_RUN) ruff check . + $(UV_RUN) ruff format --check . format: ## Auto-format code with ruff - ruff check --fix . - ruff format . + $(UV_RUN) ruff check --fix . + $(UV_RUN) ruff format . typecheck: ## Run mypy type checking - mypy server/ + $(UV_RUN) mypy server/ check: lint typecheck test ## Run all checks (lint + typecheck + test) diff --git a/docs/screenshots/subagent-tile.png b/docs/screenshots/subagent-tile.png new file mode 100644 index 0000000..c9386ee Binary files /dev/null and b/docs/screenshots/subagent-tile.png differ diff --git a/docs/screenshots/subagent-transcript.png b/docs/screenshots/subagent-transcript.png new file mode 100644 index 0000000..fec03d4 Binary files /dev/null and b/docs/screenshots/subagent-transcript.png differ diff --git a/public/css/style.css b/public/css/style.css index 8ba2d8d..0bb2ae0 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -456,6 +456,174 @@ body { .context-bar-fill.high { background: var(--status-waiting); } .context-bar-fill.critical { background: var(--danger); } +/* Subagents section */ +.card-subagents { + margin: 8px 0 4px; + border-top: 1px solid var(--border); + padding-top: 6px; +} + +.subagents-header { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--text-secondary); + cursor: pointer; + padding: 2px 0; +} + +.subagents-header:hover { + color: var(--text-primary); +} + +.subagents-list { + margin-top: 4px; +} + +.subagent-row { + display: flex; + align-items: center; + gap: 8px; + padding: 3px 0 3px 16px; + font-size: 12px; + color: var(--text-secondary); +} + +.subagent-type { + font-weight: 600; + color: var(--text-primary); + min-width: 60px; +} + +.subagent-desc { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.subagent-duration { + font-family: 'SF Mono', 'Fira Code', monospace; + font-size: 11px; + opacity: 0.7; +} + +.status-dot.small { + width: 6px; + height: 6px; + min-width: 6px; +} + +/* Subagents section */ +.card-subagents { + margin: 8px 0 4px; + border-top: 1px solid var(--border); + padding-top: 6px; +} + +.subagents-header { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--text-secondary); + cursor: pointer; + padding: 2px 0; +} + +.subagents-header:hover { + color: var(--text); +} + +.subagents-list { + margin-top: 4px; +} + +.subagent-item { + border-left: 2px solid var(--border); + margin-left: 4px; +} + +.subagent-row { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0 4px 12px; + font-size: 12px; + color: var(--text-secondary); + cursor: pointer; + border-radius: var(--radius); +} + +.subagent-row:hover { + background: var(--surface-2); +} + +.subagent-type { + font-weight: 600; + color: var(--text); + min-width: 50px; +} + +.subagent-desc { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.subagent-duration { + font-family: var(--font-mono); + font-size: 11px; + opacity: 0.7; +} + +.subagent-expand { + margin-left: auto; + font-size: 10px; + transition: transform 0.15s; +} + +.card-subagents-done { + font-size: 11px; + color: var(--text-dim); + padding: 4px 0; + border-top: 1px solid var(--border); + margin-top: 6px; +} + +.subagents-done-count { + color: var(--text-dim); + font-weight: 400; +} + +.subagent-transcript { + padding: 6px 12px 6px 16px; + font-size: 12px; + max-height: 300px; + overflow-y: auto; + background: var(--surface-0); + border-radius: 0 0 var(--radius) var(--radius); +} + +.subagent-transcript .preview-msg { + padding: 3px 0; + border-bottom: 1px solid var(--border); + line-height: 1.4; + word-break: break-word; +} + +.subagent-transcript .preview-msg:last-child { + border-bottom: none; +} + +.subagent-loading { + color: var(--text-dim); + font-style: italic; + padding: 4px 0; +} + .card-footer { display: flex; justify-content: space-between; @@ -1351,6 +1519,87 @@ body { display: block; } +/* ---- Agent Blocks in Transcript ---- */ +.agent-block { + margin: 8px 0 12px 38px; + border: 1px solid var(--border-light); + border-left: 3px solid #06b6d4; + border-radius: 0 var(--radius) var(--radius) 0; + background: var(--surface-1); + overflow: hidden; +} + +.agent-block-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: var(--surface-2); + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); + border-bottom: 1px solid var(--border); +} + +.agent-block-icon { font-size: 14px; } + +.agent-block-title { + font-weight: 700; + color: var(--text); + font-size: 13px; +} + +.agent-type-badge { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.4px; + padding: 2px 8px; + border-radius: 3px; + background: rgba(6, 182, 212, 0.15); + color: #06b6d4; +} + +.agent-block-prompt { + padding: 8px 12px; + font-size: 13px; + color: var(--text-secondary); + border-bottom: 1px solid var(--border); +} + +.agent-block-result { + padding: 8px 12px; + font-size: 13px; + line-height: 1.5; + color: var(--text-secondary); + max-height: 200px; + overflow-y: auto; +} + +.agent-transcript-toggle { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + font-size: 12px; + color: var(--text-dim); + cursor: pointer; + border-top: 1px solid var(--border); + background: var(--surface-2); +} + +.agent-transcript-toggle:hover { + color: var(--text); +} + +.agent-transcript-content { + border-top: 1px solid var(--border); + padding: 8px; + max-height: 500px; + overflow-y: auto; + background: var(--surface-0); +} + /* ---- History View ---- */ .history-header { display: flex; diff --git a/public/js/app.js b/public/js/app.js index 5037ba6..5eee6c7 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -251,8 +251,25 @@ const App = { this.updateStats(); } else if (data.type === 'session_update') { const session = data.session; - this.sessions[session.id] = session; - Dashboard.updateCard(session); + if (session.parent_session_id) { + // Subagent update — route to parent card, never create a tile + const parent = this.sessions[session.parent_session_id]; + if (parent) { + const subs = parent.subagents || []; + const idx = subs.findIndex(s => s.id === session.id); + if (idx >= 0) subs[idx] = session; + else subs.push(session); + parent.subagents = subs; + Dashboard.updateCard(parent); + } + } else { + // Parent session update — preserve existing subagents if not provided + if (!session.subagents && this.sessions[session.id]) { + session.subagents = this.sessions[session.id].subagents || []; + } + this.sessions[session.id] = session; + Dashboard.updateCard(session); + } this.updateStats(); } }, diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 4ac4009..a11f82c 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -3,6 +3,9 @@ */ const Dashboard = { + _expandedSubagentSections: new Set(), + _expandedSubagentTranscripts: new Set(), + _emptyHTML: `

No active sessions

@@ -51,6 +54,13 @@ const Dashboard = { const existing = document.querySelector(`.session-card[data-session-id="${session.id}"]`); if (existing) { existing.innerHTML = this._cardHTML(session); + // Reload transcripts for any expanded subagent transcripts + this._expandedSubagentTranscripts.forEach(agentId => { + const container = document.getElementById(`subagent-transcript-${agentId}`); + if (container && container.style.display !== 'none') { + this._loadSubagentTranscript(agentId, container); + } + }); } else { // New session — add card const grid = document.getElementById('session-grid'); @@ -148,6 +158,7 @@ const Dashboard = {
+ ${this._subagentsHTML(s.subagents)}