Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 19 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
Binary file added docs/screenshots/subagent-tile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/subagent-transcript.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
249 changes: 249 additions & 0 deletions public/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
21 changes: 19 additions & 2 deletions public/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
},
Expand Down
Loading
Loading