chore: remove unused surfaces (VS Code extension, statusline, landing assets, Docker)

The repo shipped five install/publish surfaces that nothing in the project builds, tests or documents as supported any more. Each one carried its own stale docs, and the Docker/Podman path additionally described a Dockerfile whose COPY of statusline/ would have broken the build the moment that directory went away.

Removed: vscode-extension/ (16 files, marketplace extension for an ID we do not own); statusline/ (3 files, standalone CLI statusline); the repo-root landing/wiki PWA assets (sw.js, manifest.json, robots.txt, sitemap.xml, og-image.svg, favicon.svg, fonts/ - 17 files for pages that no longer exist); .superpowers/ (4 files of scratch brainstorm state, already covered by .gitignore:55); and the container publish path (Dockerfile, docker-compose.yml, .dockerignore, 5 Makefile targets, 2 npm scripts).

Kept deliberately - these are about the USER's containers, not ours: scripts/install-hooks.js container detection and AGENTHUB_ALLOW_CONTAINER_HOOKS (issue #193), server/lib/lane-detect.js docker-compose service detection, server/data/profile-templates/postgres-compose/, server/lib/stage-detect.js 'docker push' detection, the host.docker.internal Prometheus notes, the Config Explorer's reads of the user's own ~/.claude/statusline.py, and .devcontainer/ (its docker-in-docker feature is dropped since it existed only to build the removed Dockerfile).

Docs follow the code: ARCHITECTURE.md loses the VS Code, statusline and container sections plus two dead TOC anchors, and its PWA section is rewritten from three PWAs to one; INSTALL.md, SETUP.md, docs/DEPLOYMENT.md, server/README.md, client/README.md, docs/README.md, docs/HOOKS.md and docs/DATABASE.md drop the same. The Makefile target count in ARCHITECTURE.md is corrected 25 -> 17 (it was already stale).
This commit is contained in:
2026-09-10 13:44:28 +07:00
parent e972267a42
commit 61cc37ba32
65 changed files with 60 additions and 6626 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
# Optional dev-container image for AgentHub.
# Node 22 to match the production Dockerfile and the project's engines.
# Node 22 to match the project's engines.
# Adds the native-addon toolchain (better-sqlite3 builds via node-gyp) plus
# Python 3 (statusline.py and helper scripts) and the sqlite3 CLI.
# Python 3 (scripts/expand-ts-module-docs.py) and the sqlite3 CLI.
#
# This image is ONLY used by Dev Containers / Codespaces. Host-based development
# (npm run dev / npm start) is unaffected.
+4 -4
View File
@@ -22,13 +22,13 @@ dependencies (`npm run setup`).
| Component | Detail |
| ---------------- | ------------------------------------------------------------------- |
| Base image | `mcr.microsoft.com/devcontainers/javascript-node:22` (matches prod) |
| Base image | `mcr.microsoft.com/devcontainers/javascript-node:22` |
| Native toolchain | `build-essential` + `python3` so `better-sqlite3` compiles |
| Python | `python3` / `python` for `statusline.py` and helper scripts |
| Python | `python3` / `python` for `scripts/expand-ts-module-docs.py` |
| sqlite3 CLI | inspect the dashboard DB during development |
| Features | GitHub CLI, Docker-in-Docker (build/run the project's own Dockerfile) |
| Features | GitHub CLI |
| Forwarded ports | `4820` (server API + WebSocket), `5173` (Vite client) |
| Editor | ESLint + Prettier (format on save), Vitest, Docker, YAML, Tailwind |
| Editor | ESLint + Prettier (format on save), Vitest, YAML, Tailwind |
## Everyday commands
+5 -9
View File
@@ -4,9 +4,9 @@
// Used only when you choose "Dev Containers: Reopen in Container" (VS Code) or
// open the repo in a GitHub Codespace. It changes nothing for host-based dev.
//
// Covers the full project: the Express server (4820), the React/Vite client
// (5173) and the VS Code extension. The native `better-sqlite3`
// addon builds here (build-essential + python3 are installed in the Dockerfile).
// Covers the full project: the Express server (4820) and the React/Vite
// client (5173). The native `better-sqlite3` addon builds here
// (build-essential + python3 are installed in the Dockerfile).
//
// NOTE (issue #193): Claude Code hooks are a HOST-side concern. This container
// intentionally does NOT bind-mount ~/.claude and does NOT install hooks — the
@@ -20,10 +20,7 @@
},
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {},
// Lets you build/run the project's own production Dockerfile + docker-compose
// from inside the dev container (e.g. to reproduce issue #193 deliberately).
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
"ghcr.io/devcontainers/features/github-cli:1": {}
},
// Server (API + WebSocket) and the Vite client dev server. VS Code forwards
@@ -40,7 +37,7 @@
}
},
// Install root + client + vscode-extension deps.
// Install root + client deps.
// Never installs Claude Code hooks (host-only — see note above).
"postCreateCommand": "bash .devcontainer/post-create.sh",
"waitFor": "postCreateCommand",
@@ -59,7 +56,6 @@
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"vitest.explorer",
"ms-azuretools.vscode-docker",
"redhat.vscode-yaml",
"yzhang.markdown-all-in-one",
"ms-python.python",
+1 -1
View File
@@ -7,7 +7,7 @@
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
set -euo pipefail
echo "▶ Installing server + client + vscode-extension dependencies (npm run setup)…"
echo "▶ Installing server + client dependencies (npm run setup)…"
npm run setup
cat <<'EOF'
-15
View File
@@ -1,15 +0,0 @@
node_modules/
client/node_modules/
client/dist/
data/
.git/
.github/
*.md
!README.md
images/
wiki/
index.html
og-image.svg
favicon.svg
.prettierrc
.prettierignore
-10
View File
@@ -13,13 +13,3 @@
*.ttf binary
*.eot binary
*.db binary
# ── GitHub Linguist overrides — keep language stats honest ──────────────────
# wiki/mermaid.min.js is a vendored, minified third-party library (Mermaid);
# it is not project source code and must not count toward language stats.
wiki/mermaid.min.js linguist-vendored
# wiki/i18n-content.js is machine-assembled translation data (see its header:
# "AUTO-GENERATED wiki body-content translations ... Do not hand-edit").
# It is a data bundle keyed by English innerHTML, not hand-written JavaScript.
wiki/i18n-content.js linguist-generated
-12
View File
@@ -9,17 +9,5 @@ package-lock.json
client/package-lock.json
.vs/
*.md
index.html
wiki/index.html
*.yml
*.yaml
desktop/out/
desktop/release/
desktop/node_modules/
desktop/package-lock.json
desktop/assets/*.svg
desktop/assets/*.png
desktop/assets/*.icns
desktop/assets/icon.iconset/
wiki/mermaid.min.js
fonts/*.woff2
-130
View File
@@ -1,130 +0,0 @@
# Superpowers Workspace
This directory contains project-specific configuration and artifacts for the Superpowers workflow.
It acts as the working memory layer for agent-driven development, enabling structured planning, execution, and iteration across sessions.
---
## Purpose
Superpowers transforms coding agents into structured, process-driven collaborators. Instead of jumping straight into code, the agent:
1. Clarifies intent
2. Produces a design
3. Breaks work into executable steps
4. Executes via subagents
5. Verifies and iterates
This directory stores the outputs of that workflow so progress is persistent, inspectable, and reproducible.
---
## Directory Structure
Typical contents may include:
```
.superpowers/
├── brainstorm/ # Design explorations and refined specs
├── plans/ # Task breakdowns and execution plans
├── reviews/ # Code review outputs and feedback
├── runs/ # Execution logs or agent traces
└── README.md # This file
```
> Exact structure may evolve depending on which skills are triggered.
---
## Workflow Overview
Superpowers operates through automatic skill activation:
### 1. Brainstorming
* Refines vague ideas into concrete specs
* Explores alternatives
* Produces structured, reviewable design docs
### 2. Planning
* Converts approved designs into granular tasks
* Each task is:
* Small (2–5 min)
* Explicit (exact file paths + code)
* Verifiable
### 3. Execution
* Tasks are executed by subagents
* Includes:
* Spec compliance checks
* Code quality review
* Can run sequentially or in parallel
### 4. Verification
* Enforces test-first development (TDD)
* Ensures correctness before completion
* Prevents silent regressions
### 5. Completion
* Validates final state
* Offers merge / PR / discard options
* Cleans up working branches
---
## Key Principles
* **Test-first development** (RED → GREEN → REFACTOR)
* **Small, deterministic tasks**
* **Explicit over implicit**
* **Process over intuition**
* **Verification over assumption**
---
## How to Use
You don’t interact with this directory directly most of the time.
Instead:
* Start a task in your coding agent (Claude, Cursor, etc.)
* Let Superpowers skills activate automatically
* Review outputs when prompted (designs, plans, reviews)
Artifacts will be written here as the workflow progresses.
---
## When to Look Here
Check this directory when you want to:
* Review the current plan
* Inspect prior design decisions
* Debug agent behavior
* Resume interrupted work
* Audit what was executed
---
## Notes
* Files here are **source-of-truth for agent state**
* Safe to commit (recommended for team workflows)
* Avoid manual edits unless you understand the workflow implications
---
## Related
* Project root `CLAUDE.md` → global agent context
* `.claude/` → rules, skills, and subagents
* Superpowers upstream docs →
File diff suppressed because it is too large Load Diff
@@ -1 +0,0 @@
{"reason":"idle timeout","timestamp":1774536800234}
@@ -1 +0,0 @@
1542
+24 -213
View File
@@ -39,8 +39,6 @@ Architectural overview and technical reference for the Agent Dashboard system, c
![React Testing Library](https://img.shields.io/badge/React_Testing_Library-13.0-FF5733?style=flat-square&logo=testinglibrary&logoColor=white)
![ESLint](https://img.shields.io/badge/ESLint-8.44-4B32C3?style=flat-square&logo=eslint&logoColor=white)
![Prettier](https://img.shields.io/badge/Prettier-3.8-F7B93E?style=flat-square&logo=prettier&logoColor=white)
![Docker](https://img.shields.io/badge/Docker-20.10-2496ED?style=flat-square&logo=docker&logoColor=white)
![Podman](https://img.shields.io/badge/Podman-4.0-CC342D?style=flat-square&logo=podman&logoColor=white)
![Make](https://img.shields.io/badge/Make-4.3-000000?style=flat-square&logo=make&logoColor=white)
---
@@ -63,11 +61,10 @@ Architectural overview and technical reference for the Agent Dashboard system, c
- [Browser Notification System](#browser-notification-system)
- [Update Notifier Subsystem](#update-notifier-subsystem)
- [Sagi Companion Subsystem](#sagi-companion-subsystem)
- [VS Code Extension Architecture](#vs-code-extension-architecture)
- [Security Considerations](#security-considerations)
- [Performance Characteristics](#performance-characteristics)
- [Deployment Modes](#deployment-modes)
- [Statusline Utility](#statusline-utility)
- [Model Name Formatting](#model-name-formatting-client)
- [Technology Choices](#technology-choices)
- [Build & Run Targets](#build--run-targets)
@@ -473,57 +470,37 @@ graph TD
### Self-hosted assets (no external CDN)
Nothing the dashboard or docs render is fetched from a third-party CDN at runtime — all fonts and scripts are served locally, so every surface works fully offline and leaks nothing to external hosts.
Nothing the dashboard renders is fetched from a third-party CDN at runtime — fonts are served locally, so the UI works fully offline and leaks nothing to external hosts.
- **React app fonts** — Inter + JetBrains Mono are imported from `@fontsource` (latin subset) in `client/src/main.tsx`. Vite bundles the per-weight WOFF2 into `client/dist/assets/` with content hashes at build time; there is no Google Fonts `<link>`. Importing the `latin-*` subset entry points keeps the emitted set to one WOFF2 per weight.
- **Static pages (landing + wiki)** — load a self-hosted `@font-face` sheet at the repo-root `fonts/` directory (`fonts/fonts.css` + the `*.woff2` files). The root `index.html` references `fonts/fonts.css`; the wiki references `../fonts/fonts.css` (relative paths resolve under GitHub Pages).
- **Wiki Mermaid** — vendored as `wiki/mermaid.min.js` (the genuine minified `mermaid@10.9.6` from npm, with a provenance banner; `.prettierignore`d) and loaded via a local `<script>` instead of `cdn.jsdelivr.net`.
- **VS Code extension** — the inline `getErrorHtml()` error page dropped its Google Fonts loader for a system font stack (no bundler / local font path available in that webview).
Net effect: no `fonts.googleapis.com`, `fonts.gstatic.com`, or `cdn.jsdelivr.net` requests anywhere (verified by `git grep`).
Net effect: no `fonts.googleapis.com` or `fonts.gstatic.com` requests anywhere (verified by `git grep`).
### PWA Architecture
The project ships three independent Progressive Web Apps. Each has its own Web App Manifest and Service Worker, so the browser treats them as separate installable applications with isolated caches.
The dashboard ships as a Progressive Web App with its own Web App Manifest and
Service Worker (`client/public/manifest.json` + `client/public/sw.js`), so the
browser treats it as an installable application with its own cache.
```
┌─────────────────────────────────────────────────────────────────┐
│ PWA Surface Map │
├──────────────────┬──────────────────┬───────────────────────────┤
│ Dashboard │ Landing Page │ Wiki │
│ (client/) │ (root) │ (wiki/) │
├──────────────────┼──────────────────┼───────────────────────────┤
│ manifest.json │ manifest.json │ manifest.json │
│ sw.js │ sw.js │ sw.js │
│ id: dashboard │ id: landing │ id: wiki │
├──────────────────┼──────────────────┼───────────────────────────┤
│ Precache: │ Precache: │ Precache: │
│ /, manifest, │ index.html, │ index.html, style.css, │
│ favicon.svg │ favicon, og-img, │ script.js, manifest, │
│ │ manifest │ favicon │
│ Runtime cache: │ Runtime cache: │ Runtime cache: │
│ JS/CSS bundles │ screenshot PNGs │ (all precached) │
│ (cache-first) │ (cache-first) │ │
│ │ │ │
│ Skip: /api/*, │ N/A │ N/A │
│ /ws, __vite │ │ │
│ │ │ │
│ + Push notifs │ │ │
│ (VAPID pipeline) │ │ │
└──────────────────┴──────────────────┴───────────────────────────┘
```
**Service Worker lifecycle (all three):**
**Service Worker lifecycle:**
1. **Install** → `skipWaiting()` — new SW activates immediately, no waiting for tabs to close.
2. **Activate** → old caches deleted (keyed by `CACHE_NAME`: `dashboard-v1`, `landing-v1`, `wiki-v1`). Bump the version string to force a cache bust.
2. **Activate** → old caches deleted (keyed by `CACHE_NAME`, e.g. `dashboard-v2`). Bump the version string to force a cache bust.
3. **Fetch** → Navigation requests are network-first with offline fallback to cached HTML. Static assets are cache-first with runtime caching on miss.
**Dashboard SW specifics:** The fetch handler skips `/api/*`, `/ws`, and Vite HMR (`__vite`) URLs so live data and development tooling are never cached. Only responses with `response.type === "basic"` (same-origin) are stored. The existing push notification handlers (`push`, `notificationclick`) are preserved alongside the caching logic.
The fetch handler skips `/api/*`, `/ws`, and Vite HMR (`__vite`) URLs so live data
and development tooling are never cached. Only responses with
`response.type === "basic"` (same-origin) are stored. The push notification
handlers (`push`, `notificationclick`) live alongside the caching logic — their
payload icon is the same-origin `/favicon.svg` (see `server/lib/push.js`).
**Manifest icons:** All three manifests reference `favicon.svg` with `sizes="any"` and `type="image/svg+xml"` — supported in Chrome 107+, Firefox 110+, Edge 107+. Two icon entries per manifest: one with `purpose: "any"` and one with `purpose: "maskable"`.
**Manifest icons:** `favicon.svg` with `sizes="any"` and `type="image/svg+xml"` —
supported in Chrome 107+, Firefox 110+, Edge 107+. Two icon entries: one with
`purpose: "any"` and one with `purpose: "maskable"`.
**iOS meta tags:** All HTML files include `<meta name="apple-mobile-web-app-capable" content="yes">` and `<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">` for standalone home-screen mode on Safari.
**iOS meta tags:** `client/index.html` includes
`<meta name="apple-mobile-web-app-capable" content="yes">` and
`<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">`
for standalone home-screen mode on Safari.
### Client Module Graph
@@ -1056,7 +1033,7 @@ flowchart TD
- Uses Node.js `http` module directly — no dependencies.
- Resolution order is **env override → discovery file → default**:
- `CLAUDE_DASHBOARD_PORT` forces a single target (no fan-out, no discovery).
- Otherwise `server/lib/server-info.js` reads `~/.claude/.agenthub.json`, prunes dead-PID entries, and returns one live `port` per unique `dataDir` (lowest port wins when Docker and `npm run dev` share `~/.claude/agenthub`). The handler POSTs to each returned port in parallel.
- Otherwise `server/lib/server-info.js` reads `~/.claude/.agenthub.json`, prunes dead-PID entries, and returns one live `port` per unique `dataDir` (lowest port wins when two instances share `~/.claude/agenthub`). The handler POSTs to each returned port in parallel.
- If neither yields anything, the handler falls back to `4820`.
- Per-target promises never reject — a dead listener can't starve the others, and the handler can wait on `Promise.all` for clean exit timing.
@@ -2116,7 +2093,7 @@ The same shape is used by `GET /status`, `POST /check`, and the `update_status`
The dashboard does not expose an apply/restart endpoint by design. A process cannot reliably replace itself without an external supervisor, and several real constraints make an in-process self-update path strictly worse than letting the user run two commands in a terminal:
- **Supervisor ambiguity.** `npm run dev` (concurrently), `npm start`, `pm2`, `systemd`, `launchd`, and Docker each need different restart logic; an in-process helper could only encode one of them and would silently mis-restart the rest.
- **Supervisor ambiguity.** `npm run dev` (concurrently), `npm start`, `pm2`, `systemd`, and `launchd` each need different restart logic; an in-process helper could only encode one of them and would silently mis-restart the rest.
- **Silent failures.** `npm install` / `npm run build` / port-release timing issues surface as a dead server with no user-facing feedback once the original process has exited.
- **No rollback.** A partial pull + install leaves a broken checkout with no atomic recovery — the working tree is mutated mid-flight.
- **Branch coverage.** Even with the situation-aware `manual_command` produced by the detection layer, an automatic apply would still need branch-aware integration (rebase vs merge vs switch) and merge-conflict handling. That belongs in the user's shell, not in a background daemon.
@@ -2224,64 +2201,6 @@ Sagi's only contact with the rest of the app is four light, additive touchpoints
---
## VS Code Extension Architecture
The **AgentHub** VS Code extension provides an integrated monitoring experience directly within the editor. It communicates with the local dashboard server via standard HTTP APIs and renders the dashboard UI in a webview.
<p align="center">
<img src="vscode-extension/vscode.png" alt="VS Code Extension Screenshot" width="100%">
</p>
### Extension Components
| Component | Responsibility |
| --- | --- |
| **Extension Host** (`extension.js`) | Manages the extension lifecycle, registers commands, creates the status bar item, and coordinates the webview panel. |
| **Sidebar Provider** (`sidebar.js`) | Implements the `TreeDataProvider` for the Activity Bar. It performs background polling of the dashboard APIs (`/api/stats`, `/api/analytics`, `/api/sessions`) every 5 seconds. |
| **Status Bar Item** | Provides a persistent "Pulse" indicator in the VS Code status bar, showing active session and agent counts. |
| **Webview Panel** | A native VS Code tab that renders the dashboard React application. It supports deep linking to specific sessions or sub-pages. |
### Data Flow
```mermaid
graph TD
subgraph "VS Code Process"
SB[Status Bar]
SIDE[Sidebar TreeView]
WV[Webview Panel]
end
subgraph "Extension Host"
EH[extension.js]
SP[sidebar.js]
end
subgraph "Dashboard Server (localhost)"
API["/api/stats<br/>/api/analytics<br/>/api/sessions"]
DS[Express Server]
end
SIDE -->|Poll 5s| SP
SP -->|HTTP GET| API
EH -->|HTTP GET| API
SB -->|Pulse| EH
WV -->|Iframe src| DS
```
### Key Implementation Details
1. **Auto-Detection**: The extension checks both port `5173` (Vite dev server) and `4820` (Production server) on `localhost`. It prioritizes the production port for API data but can render the UI from either.
2. **Real-time Status**: The `SidebarProvider` uses a background loop with `onDidChangeTreeData` to automatically toggle between **Online** and **Offline** states as the local server starts or stops.
3. **Deep Linking**: Commands like `claude-code-agent-monitor.openDashboard` accept arguments (e.g., a session ID or page path like `analytics`) to route the webview to specific views within the React SPA.
4. **Theme Awareness**: The Activity Bar icon (`icon.svg` or `apple-touch-icon.png`) and sidebar icons use VS Code's `ThemeIcon` and `ThemeColor` to ensure they adapt to Light, Dark, and High Contrast themes.
For the extension source code, refer to the [vscode-extension/](./vscode-extension/) directory.
> [!TIP]
> Extension on VS Code Marketplace: [AgentHub](https://marketplace.visualstudio.com/items?itemName=hoangsonw.claude-code-agent-monitor)
---
## Security Considerations
| Area | Approach |
@@ -2368,59 +2287,7 @@ graph LR
| **File watching** | `node --watch` + Vite HMR | None |
| **Source maps** | Inline | External files |
### Container (Docker / Podman)
A multi-stage `Dockerfile` builds the client and server into a single production image. Both Docker and Podman are fully supported — the image is OCI-compliant.
```mermaid
graph LR
subgraph "Multi-Stage Build"
S1["Stage 1: server-deps\nnode:22-alpine\nnpm ci --omit=dev"]
S2["Stage 2: client-build\nnode:22-alpine\nnpm ci + vite build"]
S3["Stage 3: runtime\nnode:22-alpine\nCopies node_modules + client/dist"]
S1 --> S3
S2 --> S3
end
subgraph "Container Runtime"
VOL1["~/.claude (ro)\nlegacy session import"]
VOL2["agent-monitor-data\nSQLite persistence"]
S3 -->|"EXPOSE 4820"| SRV["node server/index.js\nport 4820"]
VOL1 --> SRV
VOL2 --> SRV
end
style S3 fill:#339933,stroke:#5cb85c,color:#fff
style SRV fill:#6366f1,stroke:#818cf8,color:#fff
```
**Usage:**
```bash
# Docker Compose
docker compose up -d --build
# Podman Compose
CLAUDE_HOME="$HOME/.claude" podman compose up -d --build
# Plain Docker / Podman (equivalent)
docker build -t agent-monitor .
docker run -d -p 4820:4820 \
-v "$HOME/.claude:/root/.claude:ro" \
-v agent-monitor-data:/app/data \
agent-monitor
```
> [!NOTE]
> **Hook note:** Claude Code hooks run on the host, not inside the container. The containerized server still receives hook events via HTTP on `localhost:4820` — run `npm run install-hooks` on the host after the container is up. `scripts/install-hooks.js` detects container execution and refuses there (issue #193) so it cannot write a container-internal handler path into a bind-mounted host `~/.claude`; the containerized server's boot-time auto-install is skipped for the same reason. Override with `AGENTHUB_ALLOW_CONTAINER_HOOKS=1` only when Claude Code itself runs inside the container.
---
## Statusline Utility
The `statusline/` directory contains a standalone CLI statusline for Claude Code, separate from the web dashboard. It renders a color-coded bar at the bottom of the Claude Code terminal showing model, user, working directory, git branch, context window usage, per-direction token counts, and session cost in USD.
### Model Name Formatting (Client)
## Model Name Formatting (Client)
The `client/src/lib/format.ts` module exports a `formatModelName()` utility that converts raw model identifiers stored in the database into human-readable display names throughout the UI (everywhere **except** the Settings page, which shows raw patterns for pricing rule configuration).
@@ -2447,60 +2314,6 @@ The function handles:
Components that consume this: SessionDetail, Analytics (donut chart + breakdown), Dashboard (model stats), MessageList, AgentCard, SessionDrillIn, and EventDetail.
### Data Flow
```mermaid
sequenceDiagram
participant CC as Claude Code
participant SH as statusline-command.sh
participant PY as statusline.py
participant GIT as git CLI
CC->>SH: stdin (JSON payload)
SH->>PY: Pipes stdin through
PY->>PY: Parse JSON (model, cwd, context_window, cost)
PY->>GIT: git symbolic-ref --short HEAD
GIT-->>PY: Branch name
PY->>PY: Build ANSI-colored segments (incl. tokens by direction, cost)
PY-->>CC: stdout (formatted statusline)
```
### Segments
| Segment | Source | Color Logic |
| ------------ | ------------------------------------- | ------------------------------------------------------------------------------------ |
| Model | `data.model.display_name` | Always cyan |
| User | `$USERNAME` / `$USER` env var | Always green |
| Working Dir | `data.workspace.current_dir` | Always yellow, `~` prefix for home |
| Git Branch | `git symbolic-ref --short HEAD` | Always magenta, hidden outside git repos |
| Context Bar | `data.context_window.used_percentage` | Green < 50%, Yellow 50–79%, Red >= 80% |
| Token Counts | `data.context_window.current_usage` | Green `↑` input, cyan `↓` output, dim `c` cache reads |
| Session Cost | `data.cost.total_cost_usd` | Green < $5, Yellow $5–$20, Red >= $20 (shown on API and subscription plans) |
### Integration
The statusline is configured in `~/.claude/settings.json` via the `statusLine` key:
```json
{
"statusLine": {
"type": "command",
"command": "bash \"/path/to/.claude/statusline-command.sh\""
}
}
```
Claude Code invokes this command on each update, piping a JSON payload to stdin. The script reads the JSON, extracts fields, runs `git` for branch info, and prints ANSI-formatted output to stdout.
**Design decisions:**
- **Python 3.6+** -- available on virtually all systems, handles ANSI and JSON natively
- **No dependencies** -- uses only stdlib (`sys`, `json`, `os`, `subprocess`)
- **Shell wrapper** -- `statusline-command.sh` sets `PYTHONUTF8=1` for Windows Unicode support and resolves the absolute path to the Python script
- **Fail-safe** -- exits silently on empty input or JSON parse errors, never blocks Claude Code
---
## Technology Choices
| Technology | Why This Over Alternatives |
@@ -2528,8 +2341,6 @@ make build Build the React client for production
make start Start the production server
make test Run all tests (server + client)
make format Format all files with Prettier
make docker-up Start via docker-compose
make docker-down Stop docker-compose stack
```
See `Makefile` for the complete set of 25 targets covering setup, dev, testing, formatting, data management, Codex extensions, and Docker/Podman workflows.
See `Makefile` for the complete set of 17 targets covering setup, dev, testing, formatting, and data management.
-67
View File
@@ -1,67 +0,0 @@
# Multi-stage Dockerfile for AgentHub - a Node.js server with a React client.
# This setup optimizes the final image size by separating the build and runtime stages.
# The first stage installs only the production dependencies for the server, while the second stage builds the React client.
# The final stage combines the necessary files and dependencies to run the application in production.
# Compatibility: This setup is compatible with both Podman and Docker. Runnable on any platform that supports Node.js and Alpine Linux.
#
# Author: Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
# ── Stage 1: Install server production deps ───────────────────────────
FROM node:22-alpine AS server-deps
WORKDIR /app
COPY package.json package-lock.json ./
# The root `postinstall` hook (scripts/postinstall.js) fires during `npm ci`, so
# the file must exist here or npm aborts with MODULE_NOT_FOUND before installing
# anything. It self-skips when client/ is absent (as it is in this stage), so
# copying just the one script keeps this deps-cache layer from busting on
# unrelated scripts/ edits. Do NOT use --ignore-scripts: that would also skip
# better-sqlite3's prebuild fetch and silently drop the native SQLite driver.
COPY scripts/postinstall.js ./scripts/postinstall.js
RUN npm ci --omit=dev
# ── Stage 2: Build React client ───────────────────────────────────────
FROM node:22-alpine AS client-build
WORKDIR /app/client
COPY client/package.json client/package-lock.json ./
RUN npm ci
COPY client/ ./
# vite.config.ts stamps the UI version from the repo-root package.json (one level
# up from the client dir). Provide it here so the built client shows the real
# release version; the config falls back gracefully if it is ever absent.
COPY package.json /app/package.json
RUN npm run build
# ── Stage 3: Production runtime ───────────────────────────────────────
FROM node:22-alpine
WORKDIR /app
# Runs Claude Code sessions inside a named tmux session per lane so both the
# dashboard (via node-pty attach) and a real terminal can share one live pane.
RUN apk add --no-cache tmux
COPY --from=server-deps /app/node_modules ./node_modules/
COPY package.json ./
COPY server/ ./server/
COPY scripts/ ./scripts/
COPY statusline/ ./statusline/
COPY --from=client-build /app/client/dist ./client/dist/
RUN mkdir -p data
EXPOSE 4820
ENV NODE_ENV=production
# In a container the app MUST bind all interfaces: the server binds loopback by
# default (GHSA-gr74-4xfh-6jw9), but a container's loopback is a separate
# namespace the published port cannot reach, so a loopback bind makes the port
# unreachable. Inside a container the trust boundary is the *host* port publish
# (keep it on 127.0.0.1 — see docker-compose.yml / INSTALL.md), not this bind.
ENV DASHBOARD_HOST=0.0.0.0
# The recommended ~/.claude bind mount is read-only, so the server cannot write
# its default data dir (~/.claude/agenthub). Persist to the mounted
# volume at /app/data instead.
ENV DASHBOARD_DATA_DIR=/app/data
CMD ["node", "server/index.js"]
+2 -87
View File
@@ -30,7 +30,7 @@ dashboard, run it from source, or stay on Node 20.
| Node.js | 20+ (22+ recommended) | Required for server and client |
| npm | 9+ | Comes with Node.js |
| Claude Code | 2.x+ | Required for hook integration |
| Python | 3.6+ | Optional — statusline utility only |
| Python | 3.6+ | Optional — only for `scripts/expand-ts-module-docs.py` (doc generation) |
| Git | Any | For cloning the repository |
| `superpowers` plugin | Any | Optional — required only by `/agenthub:ship-feature` and `/agenthub:ship-feature-lane`, which drive its skills step by step (and which the built-in pipeline templates name as their per-node commands). Nothing else in AgentHub needs it; `/agenthub-doctor` reports it as `WARN` when absent. **Installed automatically** by the plugin's `SessionStart` bootstrap (opt out with `AGENTHUB_AUTO_INSTALL_PLUGINS=0`); by hand: `claude plugin install superpowers@claude-plugins-official` |
@@ -85,7 +85,7 @@ This starts two processes concurrently:
Open **http://localhost:5173** in your browser.
> [!TIP]
> When you run the dashboard directly on the host with `npm run dev` or `npm start`, the server automatically writes the Claude Code hook configuration to `~/.claude/settings.json`. If you run the dashboard in Docker or Podman, install hooks from the host with `npm run install-hooks` after the container is up.
> When you run the dashboard directly on the host with `npm run dev` or `npm start`, the server automatically writes the Claude Code hook configuration to `~/.claude/settings.json`. If you run the dashboard inside a container, install hooks from the host with `npm run install-hooks` instead — a hook path written from inside a container points at the container filesystem.
---
@@ -120,8 +120,6 @@ The dashboard is a Progressive Web App. After opening it in a supported browser
2. Once installed, the dashboard launches in its own window with no browser chrome
3. Offline support: previously visited pages and assets are served from the Service Worker cache when the network is unavailable
The landing page and wiki are also installable PWAs with their own manifests and service workers — visit each in a browser to install independently.
---
## Step 5 — (Optional) Import existing Claude Code history
@@ -223,89 +221,6 @@ See [`.codex/README.md`](./.codex/README.md) for Codex extension details.
---
## Optional: VS Code extension
**AgentHub** is also available as a dedicated VS Code extension for seamless, integrated monitoring.
<p align="center">
<img src="vscode-extension/vscode.png" alt="VS Code Extension Screenshot" width="100%">
</p>
### Features
- **Real-time Sidebar**: Monitor agent status, health, and usage stats in the Activity Bar.
- **Pulse Status Bar**: High-level session and agent counts in the bottom status bar.
- **Direct Navigation**: Jump to specific dashboard pages or recent sessions.
- **Embedded Dashboard**: Full dashboard interface within a native VS Code tab.
### Installation
1. Open the [vscode-extension](./vscode-extension) folder in VS Code.
2. Install via the Marketplace or package it manually:
```bash
cd vscode-extension
npm install
# Generate .vsix for local install
npm run package
```
3. After installation, ensure the main dashboard server is running (`npm run dev`).
4. Look for the **Radar icon** in your VS Code Activity Bar.
For advanced configuration, refer to the [.vscode](./.vscode) and [vscode-extension](./vscode-extension) directories.
> [!TIP]
> Extension on VS Code Marketplace: [AgentHub](https://marketplace.visualstudio.com/items?itemName=hoangsonw.claude-code-agent-monitor)
---
## Container mode (Docker / Podman)
The repository includes both a multi-stage `Dockerfile` and a `docker-compose.yml` file. Docker and Podman are both supported.
### Compose
```bash
# Docker Compose
docker compose up -d --build
# Podman Compose
CLAUDE_HOME="$HOME/.claude" podman compose up -d --build
```
Open **http://localhost:4820** in your browser.
### Plain Docker / Podman
```bash
# Docker
docker build -t agent-monitor .
docker run -d --name agent-monitor \
-p 127.0.0.1:4820:4820 \
-v "$HOME/.claude:/root/.claude:ro" \
-v agent-monitor-data:/app/data \
agent-monitor
# Podman
podman build -t agent-monitor .
podman run -d --name agent-monitor \
-p 127.0.0.1:4820:4820 \
-v "$HOME/.claude:/root/.claude:ro" \
-v "$HOME/.claude/agenthub:/app/data" \
agent-monitor
```
### Container notes
| Mount | Purpose |
|---|---|
| `~/.claude:/root/.claude:ro` | Lets the server import legacy Claude session history |
| `~/.claude/agenthub:/app/data` | **Canonical SQLite database** (shared with native installs) |
> [!NOTE]
> The image sets `DASHBOARD_HOST=0.0.0.0` and `DASHBOARD_DATA_DIR=/app/data` internally (baked into the `Dockerfile`). The `0.0.0.0` bind is required because a container's loopback is a separate namespace the published port cannot reach; `/app/data` is bind-mounted to `~/.claude/agenthub` on the host so Docker uses the same database as `npm start`. The trust boundary is the **host** port publish — the examples use `-p 127.0.0.1:4820:4820`, so the dashboard is local-only. To expose it on a LAN, publish on `0.0.0.0` (`-p 4820:4820`) **and** set `DASHBOARD_TOKEN`.
> [!IMPORTANT]
> Claude Code hooks run on the host, not inside the container. After the container is healthy on `http://localhost:4820`, run `npm run install-hooks` on the host so Claude Code posts hook events back to the containerized server. The installer refuses to run inside a container (issue #193) to avoid writing a container-internal handler path into a bind-mounted `~/.claude`; use `AGENTHUB_ALLOW_CONTAINER_HOOKS=1` only if you run Claude Code inside the same container.
---
## Troubleshooting
### `npm run setup` shows `better-sqlite3` errors
-24
View File
@@ -94,30 +94,6 @@ import-history: ## Import sessions from ~/.claude/ history
clear-data: ## Delete all data rows (preserves schema)
node scripts/clear-data.js
# ─── Docker / Podman ───────────────────────────────────────────────────────
.PHONY: docker-build
docker-build: ## Build dashboard Docker image
docker build -t claude-agent-monitor:local .
.PHONY: docker-up
docker-up: ## Start via docker-compose
docker compose up -d
.PHONY: docker-down
docker-down: ## Stop docker-compose stack
docker compose down
.PHONY: docker-logs
docker-logs: ## Tail docker-compose logs
docker compose logs -f
.PHONY: podman-build
podman-build: ## Build dashboard Podman image
podman build -t localhost/claude-agent-monitor:local .
# ─── Help ───────────────────────────────────────────────────────────────────
.PHONY: help
+1 -1
View File
@@ -83,7 +83,7 @@ Details, including what to delete on uninstall:
## Install from a checkout
```bash
npm run setup # root, client and vscode-extension dependencies
npm run setup # root and client dependencies
npm run build # builds the client into client/dist
npm start # serves the built client and the API on :4820
```
+1 -1
View File
@@ -55,7 +55,7 @@ cần xóa lúc uninstall: [`docs/PLUGINS.md`](docs/PLUGINS.md).
## Cài từ checkout
```bash
npm run setup # cài dependency cho root, client và vscode-extension
npm run setup # cài dependency cho root và client
npm run build # build client vào client/dist
npm start # serve client đã build + API trên :4820
```
+4 -105
View File
@@ -48,48 +48,11 @@ npm run install-hooks
```
> [!TIP]
> Container note: do not rely on hook auto-install from inside Docker or Podman. The hook path written by a container would point at the container filesystem, not the host. Start the container first, then run `npm run install-hooks` on the host. As a safeguard (issue #193), the installer now **detects container execution and refuses to run** (exiting non-zero) so it can never poison a bind-mounted host `~/.claude`; the containerized server logs the same guidance instead of silently writing a bad path. If you genuinely run Claude Code inside the same container, override with `AGENTHUB_ALLOW_CONTAINER_HOOKS=1 npm run install-hooks`.
> Container note: if you run the dashboard inside a container (a Dev Container, Codespaces, or your own image), do not install hooks from in there — the hook path a container writes points at the container filesystem, not the host. Run `npm run install-hooks` on the host instead. As a safeguard (issue #193) the installer **detects container execution and refuses to run** (exiting non-zero) so it can never poison a bind-mounted host `~/.claude`. If Claude Code itself genuinely runs inside that container, override with `AGENTHUB_ALLOW_CONTAINER_HOOKS=1 npm run install-hooks`.
> [!NOTE]
> Prefer a ready-made dev environment? This repo ships an **optional** Dev Container (`.devcontainer/`) for VS Code / GitHub Codespaces — Node 22, native build tools for `better-sqlite3`, Python, and ports `4820`/`5173` preconfigured. It's purely opt-in and changes nothing for host-based development. See [`.devcontainer/README.md`](.devcontainer/README.md). (Hooks remain host-side there too.)
### Container runtime (Docker / Podman)
The repo includes both a multi-stage `Dockerfile` and a `docker-compose.yml` file. The container image serves the built client and API on port `4820`, stores SQLite data under `/app/data`, and can import legacy Claude history from a read-only `~/.claude` mount.
```bash
# Docker Compose
docker compose up -d --build
# Podman Compose
CLAUDE_HOME="$HOME/.claude" podman compose up -d --build
# Plain Docker
docker build -t agent-monitor .
docker run -d --name agent-monitor \
-p 127.0.0.1:4820:4820 \
-v "$HOME/.claude:/root/.claude:ro" \
-v agent-monitor-data:/app/data \
agent-monitor
# Plain Podman
podman build -t agent-monitor .
podman run -d --name agent-monitor \
-p 127.0.0.1:4820:4820 \
-v "$HOME/.claude:/root/.claude:ro" \
-v "$HOME/.claude/agenthub:/app/data" \
agent-monitor
```
Container-specific behavior:
- The dashboard is available at `http://localhost:4820`
- The image sets `DASHBOARD_HOST=0.0.0.0` (bind inside the container — its loopback is a separate namespace the published port cannot reach) and `DASHBOARD_DATA_DIR=/app/data` internally; both are baked into the `Dockerfile`
- The examples publish on `127.0.0.1` only, so the dashboard is local-only. To expose it on a LAN, publish on `0.0.0.0` (`-p 4820:4820`) **and** set `DASHBOARD_TOKEN`
- `~/.claude:/root/.claude:ro` is used for history import only
- `~/.claude/agenthub:/app/data` is the **canonical** SQLite database (shared with native installs)
- Claude Code hooks still execute on the host, so install them from the host with `npm run install-hooks`
### Environment variables
| Variable | Default | Description |
@@ -136,39 +99,18 @@ This repository ships extension files for both agent ecosystems:
See [`.codex/README.md`](./.codex/README.md) for Codex extension details.
### VS Code extension setup
**AgentHub** is available as an integrated VS Code extension for seamless monitoring within your editor.
- **Activity Bar View**: Adds a custom "Radar" icon to the activity bar providing real-time agent health, token counts, and session stats.
- **Status Bar Integration**: Displays live session and agent pulse counts in the bottom bar.
- **Embedded Dashboard**: Renders the full web dashboard directly in a VS Code editor tab.
- **Automated Detection**: Automatically finds your dashboard server on ports `5173` or `4820`.
<p align="center">
<img src="vscode-extension/vscode.png" alt="VS Code Extension Screenshot" width="100%">
</p>
To install or develop the extension:
1. Open the [vscode-extension](./vscode-extension) directory in VS Code.
2. Run `npm install` and `npm run package` to generate a local `.vsix` installer.
3. For developer details, see [vscode-extension/README.md](./vscode-extension/README.md).
> [!TIP]
> Extension on VS Code Marketplace: [AgentHub](https://marketplace.visualstudio.com/items?itemName=hoangsonw.claude-code-agent-monitor)
### PWA configuration (optional)
The dashboard, landing page, and wiki each ship as independent Progressive Web Apps. No configuration is required — manifests and service workers are included out of the box.
The dashboard ships as a Progressive Web App. No configuration is required — its manifest and service worker are included out of the box.
**Customising the manifest:** Edit the `manifest.json` in the relevant directory (`client/public/` for dashboard, root for landing, `wiki/` for wiki). Common fields to change:
**Customising the manifest:** Edit `client/public/manifest.json`. Common fields to change:
- `name` / `short_name` — displayed on the home screen / dock
- `theme_color` — address bar / title bar tint (default: `#6366f1`)
- `background_color` — splash screen background
- `start_url` — entry point when launched from home screen
**Updating the service worker cache:** Each SW has a `CACHE_NAME` constant (e.g. `dashboard-v2`). After deploying new assets, bump the version string to force browsers to re-fetch — though for the dashboard this is rarely needed: hashed `/assets/*` URLs are immutable per build, everything else is fetched network-first with cache fallback, and a `controllerchange` listener in the client reloads the page exactly once when a new SW takes over, so a rebuild propagates without a hard refresh.
**Updating the service worker cache:** The SW has a `CACHE_NAME` constant (e.g. `dashboard-v2`). After deploying new assets, bump the version string to force browsers to re-fetch — though for the dashboard this is rarely needed: hashed `/assets/*` URLs are immutable per build, everything else is fetched network-first with cache fallback, and a `controllerchange` listener in the client reloads the page exactly once when a new SW takes over, so a rebuild propagates without a hard refresh.
**Browser support:** PWA install prompts appear in Chrome 107+, Edge 107+, and Firefox 110+ (desktop and Android). Safari supports `apple-mobile-web-app-capable` for iOS home-screen mode but does not show an install banner.
@@ -349,16 +291,6 @@ Commonly used targets:
| `make format-check` | `npm run format:check` | Check formatting without writing |
| `make seed` | `npm run seed` | Load demo data |
| `make clear-data` | `npm run clear-data` | Delete all data rows |
| `make docker-up` | `docker compose up -d` | Start via docker-compose |
| `make docker-down` | `docker compose down` | Stop docker-compose stack |
---
## Statusline (optional)
The `statusline/` directory contains a standalone terminal statusline for Claude Code showing model, working directory, git branch, context window usage, and token counts. It is independent of the web dashboard.
See [statusline/README.md](./statusline/README.md) for installation instructions.
---
@@ -464,36 +396,3 @@ And make sure Claude Code posts hooks to the new port:
CLAUDE_DASHBOARD_PORT=4821 claude
# or edit scripts/hook-handler.js and change the default port
```
---
### Docker / Podman container starts but no sessions appear
**Check 1 — Is the container healthy?**
```bash
curl http://localhost:4820/api/health
# Expected: {"status":"ok","timestamp":"..."}
```
**Check 2 — Did you install hooks on the host?**
Hooks run on the host machine, not inside the container. After the container is up:
```bash
npm run install-hooks
```
**Check 3 — Are hooks pointing to the right port?**
Open `~/.claude/settings.json` and verify the hook commands reference `localhost:4820` (or whatever port the container is mapped to). If you changed the port mapping, update hooks accordingly.
---
### Docker build fails during `npm ci`
If the build fails in Stage 1 with `better-sqlite3` errors, this is expected and should not block the build — `better-sqlite3` is an optional dependency. If the build still fails:
- Ensure you are using the latest Dockerfile (it should use `node:22-alpine` and **not** install `python3`, `make`, or `g++`)
- Run `docker build --no-cache -t agent-monitor .` to force a clean rebuild
- Check that `package.json` has `better-sqlite3` under `optionalDependencies`, not `dependencies`
-2
View File
@@ -19,8 +19,6 @@ Enterprise-grade React + TypeScript dashboard for real-time Claude Code agent mo
![PostCSS](https://img.shields.io/badge/PostCSS-8.5-DD3A0A?style=flat-square&logo=postcss&logoColor=white)
![Autoprefixer](https://img.shields.io/badge/Autoprefixer-10.4-DD3735?style=flat-square&logo=autoprefixer&logoColor=white)
![ESLint](https://img.shields.io/badge/ESLint-8.44-4B32C3?style=flat-square&logo=eslint&logoColor=white)
![Docker](https://img.shields.io/badge/Docker-20.10-2496ED?style=flat-square&logo=docker&logoColor=white)
![Podman](https://img.shields.io/badge/Podman-4.0-CC342D?style=flat-square&logo=podman&logoColor=white)
![Prometheus](https://img.shields.io/badge/Prometheus-2.x-E6522C?style=flat-square&logo=prometheus&logoColor=white)
![Grafana](https://img.shields.io/badge/Grafana-10.x-F46800?style=flat-square&logo=grafana&logoColor=white)
![Vitest](https://img.shields.io/badge/Vitest-1.0-646CFF?style=flat-square&logo=vitest&logoColor=white)
+1 -1
View File
@@ -62,7 +62,7 @@
"url": "https://smartgift.vn"
},
"codeRepository": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub",
"featureList": "Real-time dashboard, Kanban board, Session tracking, Activity feed, Token usage analytics, Cost tracking, Browser notifications, WebSocket live updates, CLI statusline"
"featureList": "Real-time dashboard, Kanban board, Session tracking, Activity feed, Token usage analytics, Cost tracking, Browser notifications, WebSocket live updates"
}
</script>
</head>
-23
View File
@@ -1,23 +0,0 @@
services:
agent-monitor:
build: .
container_name: agent-monitor
ports:
# Publish on the host loopback only, so the dashboard is not reachable
# from the LAN out of the box (matches the server's default trust
# boundary — GHSA-gr74-4xfh-6jw9). To expose it on a LAN, change this to
# "${DASHBOARD_PORT:-4820}:4820" and set DASHBOARD_TOKEN below.
- "127.0.0.1:${DASHBOARD_PORT:-4820}:4820"
volumes:
# Same SQLite file as `npm start` / `npm run dev` on the host.
- ${CLAUDE_HOME:-~/.claude}/agenthub:/app/data
- ${CLAUDE_HOME:-~/.claude}:/root/.claude:ro
environment:
- NODE_ENV=production
- DASHBOARD_PORT=4820
# Optional — required when a Prometheus-in-Docker scraper reaches this
# container as anything other than loopback (e.g. host.docker.internal).
- DASHBOARD_ALLOWED_HOSTS=${DASHBOARD_ALLOWED_HOSTS:-}
# DASHBOARD_HOST=0.0.0.0 and DASHBOARD_DATA_DIR=/app/data are baked into
# the image (see Dockerfile) so both `docker run` and Compose work as-is.
restart: unless-stopped
+1 -1
View File
@@ -64,7 +64,7 @@ graph TB
```
**Database Location:**
- **Canonical (default):** `~/.claude/agenthub/dashboard.db` — shared by `npm start`, `npm run dev`, Docker (bind mount), and the desktop app when it uses the same data dir
- **Canonical (default):** `~/.claude/agenthub/dashboard.db` — shared by `npm start`, `npm run dev`, and the desktop app when it uses the same data dir
- **Override:** set `DASHBOARD_DATA_DIR` (directory) or `DASHBOARD_DB_PATH` (file path) for tests or custom deployments
- **Legacy:** repo-local `./data/dashboard.db` is migrated into the canonical location on first launch (see `server/db.js`)
- **Pre-rebrand:** an install that predates AgentHub keeps its data in `~/.claude/agent-dashboard/`. Server startup renames that directory to `~/.claude/agenthub/` once, before anything opens the database (`migrateLegacyDataDir()` in `server/lib/claude-home.js`). It is a same-filesystem rename, so it is instant even at multiple gigabytes and never leaves two copies. If the rename fails, or `DASHBOARD_DATA_DIR` is set, nothing moves and the old directory keeps being used.
+7 -116
View File
@@ -10,7 +10,6 @@ Enterprise deployment strategies for Agent Dashboard across development, staging
- [Deployment Architecture](#deployment-architecture)
- [Local Development](#local-development)
- [Production Deployment](#production-deployment)
- [Docker Deployment](#docker-deployment)
- [Cloud Deployment](#cloud-deployment)
- [Process Management](#process-management)
- [Monitoring & Logging](#monitoring--logging)
@@ -26,7 +25,6 @@ Enterprise deployment strategies for Agent Dashboard across development, staging
Agent Dashboard supports multiple deployment modes:
- **Local Development** - Hot reload for rapid iteration
- **Docker** - Containerized deployment with Docker/Podman
- **PM2** - Process management for production
- **Systemd** - System service on Linux
- **Cloud** - Deploy to AWS, Azure, GCP, or other cloud providers
@@ -35,7 +33,6 @@ Agent Dashboard supports multiple deployment modes:
graph TB
subgraph "Deployment Modes"
Dev[Local Development<br/>npm run dev]
Docker[Docker Container<br/>docker compose up]
PM2[PM2 Process Manager<br/>pm2 start]
Systemd[Systemd Service<br/>systemctl start]
Cloud[Cloud Platform<br/>Kubernetes, ECS, etc.]
@@ -48,8 +45,6 @@ graph TB
end
Dev --> DevEnv
Docker --> DevEnv
Docker --> StagingEnv
PM2 --> ProdEnv
Systemd --> ProdEnv
Cloud --> ProdEnv
@@ -260,118 +255,14 @@ CODEX_HOME= # where Codex keeps sessions/ (default ~
LOG_LEVEL=info # debug | info | warn | error
```
> **Reverse-proxy / Docker exposure:** the app binds loopback, so publish it to a
> **Reverse-proxy / container exposure:** the app binds loopback, so publish it to a
> network only through a proxy you control that adds TLS + auth, or set
> `DASHBOARD_HOST=0.0.0.0` **with** `DASHBOARD_TOKEN`. A `-p 4820:4820` Docker
> mapping assumes a trusted host network — do not expose it publicly without a
> `DASHBOARD_HOST=0.0.0.0` **with** `DASHBOARD_TOKEN`. A published `4820` port
> assumes a trusted host network — do not expose it publicly without a
> token and a proxy.
---
## Docker Deployment
### Docker Compose (Recommended)
```yaml
# docker-compose.yml
version: '3.8'
services:
agenthub:
build: .
ports:
- "4820:4820"
volumes:
- ./data:/app/data
environment:
- NODE_ENV=production
- PORT=4820
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4820/api/sessions"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
```
### Build & Run
```bash
# Build image
docker compose build
# Start container
docker compose up -d
# View logs
docker compose logs -f
# Stop container
docker compose down
```
### Multi-Stage Dockerfile
```dockerfile
# Build stage
FROM node:22-alpine AS builder
WORKDIR /app
# Install dependencies
COPY package*.json ./
COPY client/package*.json ./client/
RUN npm ci && cd client && npm ci
# Build client
COPY client ./client
RUN cd client && npm run build
# Production stage
FROM node:22-alpine
WORKDIR /app
# Copy built artifacts
COPY --from=builder /app/client/dist ./client/dist
COPY --from=builder /app/node_modules ./node_modules
COPY server ./server
COPY package.json ./
# Create data directory
RUN mkdir -p /app/data
EXPOSE 4820
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD node -e "require('http').get('http://localhost:4820/api/sessions', (res) => process.exit(res.statusCode === 200 ? 0 : 1))"
CMD ["node", "server/index.js"]
```
### Container Architecture
```mermaid
graph TB
subgraph "Docker Host"
subgraph "Container"
App[Node.js App<br/>:4820]
Volume[Volume Mount<br/>/app/data]
end
end
Host[Host Filesystem<br/>./data] -->|Bind Mount| Volume
App --> Volume
Client[External Clients] -->|Port 4820| App
style App fill:#2496ED,color:#fff
style Volume fill:#FFA500
```
---
## Cloud Deployment
### AWS (Elastic Beanstalk)
@@ -395,6 +286,9 @@ eb open
### Kubernetes
The repo ships no container image or `Dockerfile` — build your own from
`npm ci && npm run build` and push it to your registry, then reference it here.
```yaml
# k8s/deployment.yaml
apiVersion: apps/v1
@@ -603,19 +497,16 @@ graph TB
subgraph "Log Aggregation"
Stdout --> PM2[PM2 Logs]
Stdout --> Systemd[Systemd Journal]
Stdout --> Docker[Docker Logs]
end
subgraph "Monitoring Tools"
PM2 --> File[Log Files]
Systemd --> Journalctl[journalctl]
Docker --> DockerLogs[docker logs]
end
subgraph "Analysis"
File --> Splunk[Splunk/ELK]
Journalctl --> Splunk
DockerLogs --> Splunk
end
style App fill:#000000,color:#fff
@@ -912,7 +803,7 @@ NODE_ENV=development node server/index.js
This deployment guide covers:
- ✅ **Multiple deployment modes** - Local, Docker, PM2, Systemd, Cloud
- ✅ **Multiple deployment modes** - Local, PM2, Systemd, Cloud
- ✅ **Production best practices** - Environment variables, health checks, logging
- ✅ **Process management** - PM2, systemd service files
- ✅ **Monitoring & logging** - Metrics endpoint, log aggregation
+2 -2
View File
@@ -578,8 +578,8 @@ function sendToServer(hookType, payload) {
> Each server appends its `{port, pid, startedAt, dataDir}` entry on
> startup and removes it on a clean shutdown. The handler **prunes any
> entry whose PID is no longer alive** and POSTs the hook payload to one
> port per **unique SQLite data directory** (lowest port wins when Docker
> and `npm run dev` share `~/.claude/agenthub`).
> port per **unique SQLite data directory** (lowest port wins when two
> instances share `~/.claude/agenthub`).
> 3. If neither yields a target, the handler falls back to `4820`.
>
> Dashboards with **different** databases (e.g. the packaged desktop app using
+1 -1
View File
@@ -429,7 +429,7 @@ This documentation covers:
- ✅ **Complete architecture** - Frontend, backend, database, integrations
- ✅ **API reference** - REST endpoints, WebSocket events
- ✅ **Deployment guides** - Docker, PM2, systemd, cloud
- ✅ **Deployment guides** - PM2, systemd, cloud
- ✅ **Performance tuning** - Database, server, client optimizations
- ✅ **Integration guides** - Hooks, MCP, custom clients
- ✅ **Internationalization** - Language resources, switching flow, locale formatting, rollout checklist
-24
View File
@@ -1,24 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#6366f1"/>
<stop offset="100%" stop-color="#818cf8"/>
</linearGradient>
<linearGradient id="glow" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#a5b4fc" stop-opacity="0.9"/>
<stop offset="100%" stop-color="#c7d2fe" stop-opacity="0.6"/>
</linearGradient>
</defs>
<!-- Hexagon background -->
<polygon points="16,2 28,9 28,23 16,30 4,23 4,9" fill="url(#bg)" rx="2"/>
<!-- Center node -->
<circle cx="16" cy="16" r="3" fill="white" opacity="0.95"/>
<!-- Three connector lines radiating outward -->
<line x1="16" y1="13" x2="16" y2="7" stroke="white" stroke-width="1.5" stroke-linecap="round" opacity="0.7"/>
<line x1="18.6" y1="17.5" x2="24" y2="20.5" stroke="white" stroke-width="1.5" stroke-linecap="round" opacity="0.7"/>
<line x1="13.4" y1="17.5" x2="8" y2="20.5" stroke="white" stroke-width="1.5" stroke-linecap="round" opacity="0.7"/>
<!-- Outer nodes -->
<circle cx="16" cy="6" r="1.8" fill="url(#glow)"/>
<circle cx="24.5" cy="21" r="1.8" fill="url(#glow)"/>
<circle cx="7.5" cy="21" r="1.8" fill="url(#glow)"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

-109
View File
@@ -1,109 +0,0 @@
/**
* @file fonts.css
* @description Self-hosted Inter + JetBrains Mono @font-face declarations — no Google Fonts dependency.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
/* Inter */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("inter-latin-400-normal.woff2") format("woff2");
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("inter-latin-500-normal.woff2") format("woff2");
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("inter-latin-600-normal.woff2") format("woff2");
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("inter-latin-700-normal.woff2") format("woff2");
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 800;
font-display: swap;
src: url("inter-latin-800-normal.woff2") format("woff2");
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 900;
font-display: swap;
src: url("inter-latin-900-normal.woff2") format("woff2");
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* JetBrains Mono */
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("jetbrains-mono-latin-400-normal.woff2") format("woff2");
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("jetbrains-mono-latin-500-normal.woff2") format("woff2");
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("jetbrains-mono-latin-600-normal.woff2") format("woff2");
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("jetbrains-mono-latin-700-normal.woff2") format("woff2");
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-28
View File
@@ -1,28 +0,0 @@
{
"id": "claude-code-agent-monitor-landing",
"name": "AgentHub",
"short_name": "AgentHub",
"description": "Local-first monitoring dashboard for Claude Code sessions.",
"author": "Nguyễn Ngọc Trí Vĩ",
"start_url": ".",
"scope": ".",
"display": "standalone",
"theme_color": "#6366f1",
"background_color": "#07070b",
"orientation": "any",
"categories": ["developer-tools", "productivity"],
"icons": [
{
"src": "favicon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "favicon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "maskable"
}
]
}
-122
View File
@@ -1,122 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630">
<defs>
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0d0d16"/>
<stop offset="100%" stop-color="#12121e"/>
</linearGradient>
<linearGradient id="hexGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#6366f1"/>
<stop offset="100%" stop-color="#818cf8"/>
</linearGradient>
<linearGradient id="nodeGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#a5b4fc" stop-opacity="0.9"/>
<stop offset="100%" stop-color="#c7d2fe" stop-opacity="0.6"/>
</linearGradient>
<linearGradient id="titleGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#e2e2f0"/>
<stop offset="55%" stop-color="#a5b4fc"/>
<stop offset="100%" stop-color="#818cf8"/>
</linearGradient>
<radialGradient id="glow1" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#6366f1" stop-opacity="0.18"/>
<stop offset="100%" stop-color="#6366f1" stop-opacity="0"/>
</radialGradient>
<radialGradient id="glow2" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#10b981" stop-opacity="0.10"/>
<stop offset="100%" stop-color="#10b981" stop-opacity="0"/>
</radialGradient>
<filter id="blur1">
<feGaussianBlur stdDeviation="40"/>
</filter>
</defs>
<!-- Background -->
<rect width="1200" height="630" fill="url(#bgGrad)"/>
<!-- Ambient glow blobs -->
<ellipse cx="200" cy="200" rx="340" ry="280" fill="url(#glow1)" filter="url(#blur1)"/>
<ellipse cx="1050" cy="480" rx="280" ry="220" fill="url(#glow2)" filter="url(#blur1)"/>
<!-- Grid dots (subtle background texture) -->
<g opacity="0.07" fill="#6366f1">
<circle cx="80" cy="80" r="1.5"/><circle cx="160" cy="80" r="1.5"/><circle cx="240" cy="80" r="1.5"/><circle cx="320" cy="80" r="1.5"/>
<circle cx="80" cy="160" r="1.5"/><circle cx="160" cy="160" r="1.5"/><circle cx="240" cy="160" r="1.5"/><circle cx="320" cy="160" r="1.5"/>
<circle cx="80" cy="240" r="1.5"/><circle cx="160" cy="240" r="1.5"/><circle cx="240" cy="240" r="1.5"/><circle cx="320" cy="240" r="1.5"/>
<circle cx="900" cy="400" r="1.5"/><circle cx="980" cy="400" r="1.5"/><circle cx="1060" cy="400" r="1.5"/><circle cx="1140" cy="400" r="1.5"/>
<circle cx="900" cy="480" r="1.5"/><circle cx="980" cy="480" r="1.5"/><circle cx="1060" cy="480" r="1.5"/><circle cx="1140" cy="480" r="1.5"/>
<circle cx="900" cy="560" r="1.5"/><circle cx="980" cy="560" r="1.5"/><circle cx="1060" cy="560" r="1.5"/><circle cx="1140" cy="560" r="1.5"/>
</g>
<!-- Large background hex (decorative) -->
<polygon points="980,120 1120,200 1120,360 980,440 840,360 840,200"
fill="none" stroke="#6366f1" stroke-width="1" opacity="0.12"/>
<polygon points="980,150 1095,215 1095,345 980,410 865,345 865,215"
fill="none" stroke="#6366f1" stroke-width="0.5" opacity="0.08"/>
<!-- Left-side decorative connection lines -->
<line x1="60" y1="560" x2="200" y2="460" stroke="#6366f1" stroke-width="1" opacity="0.15"/>
<line x1="200" y1="460" x2="380" y2="540" stroke="#6366f1" stroke-width="1" opacity="0.15"/>
<circle cx="60" cy="560" r="3" fill="#6366f1" opacity="0.3"/>
<circle cx="200" cy="460" r="3" fill="#6366f1" opacity="0.3"/>
<circle cx="380" cy="540" r="3" fill="#6366f1" opacity="0.3"/>
<!-- ── Main icon (hex + nodes) centered-left ── -->
<g transform="translate(128, 265)">
<!-- Hex -->
<polygon points="64,0 112,28 112,92 64,120 16,92 16,28" fill="url(#hexGrad)"/>
<!-- Center node -->
<circle cx="64" cy="60" r="11" fill="white" opacity="0.95"/>
<!-- Lines -->
<line x1="64" y1="49" x2="64" y2="18" stroke="white" stroke-width="5" stroke-linecap="round" opacity="0.7"/>
<line x1="73.5" y1="65.5" x2="97" y2="79" stroke="white" stroke-width="5" stroke-linecap="round" opacity="0.7"/>
<line x1="54.5" y1="65.5" x2="31" y2="79" stroke="white" stroke-width="5" stroke-linecap="round" opacity="0.7"/>
<!-- Outer nodes -->
<circle cx="64" cy="14" r="7" fill="url(#nodeGrad)"/>
<circle cx="100" cy="80" r="7" fill="url(#nodeGrad)"/>
<circle cx="28" cy="80" r="7" fill="url(#nodeGrad)"/>
</g>
<!-- ── Typography ── -->
<!-- Title -->
<text x="290" y="298"
font-family="'Inter', 'Helvetica Neue', Arial, sans-serif"
font-size="72" font-weight="800" letter-spacing="-2"
fill="url(#titleGrad)">Agent Dashboard</text>
<!-- Subtitle -->
<text x="292" y="355"
font-family="'Inter', 'Helvetica Neue', Arial, sans-serif"
font-size="26" font-weight="400"
fill="#8888aa">Real-time monitoring platform for Claude Code agent activity</text>
<!-- Divider line -->
<line x1="290" y1="385" x2="840" y2="385" stroke="#252538" stroke-width="1"/>
<!-- Tech pills -->
<g transform="translate(290, 408)" font-family="'Inter', 'Helvetica Neue', Arial, sans-serif" font-size="15" font-weight="600">
<!-- Node.js -->
<rect x="0" y="0" width="88" height="28" rx="6" fill="#33993320" stroke="#33993340"/>
<text x="44" y="19" text-anchor="middle" fill="#6dbf6d">Node.js 18+</text>
<!-- React -->
<rect x="100" y="0" width="82" height="28" rx="6" fill="#61dafb18" stroke="#61dafb30"/>
<text x="141" y="19" text-anchor="middle" fill="#61dafb">React 18</text>
<!-- TypeScript -->
<rect x="194" y="0" width="100" height="28" rx="6" fill="#3178c618" stroke="#3178c630"/>
<text x="244" y="19" text-anchor="middle" fill="#5a9fd4">TypeScript</text>
<!-- SQLite -->
<rect x="306" y="0" width="72" height="28" rx="6" fill="#003b5718" stroke="#005f8a30"/>
<text x="342" y="19" text-anchor="middle" fill="#5bb8e0">SQLite</text>
<!-- WebSocket -->
<rect x="390" y="0" width="100" height="28" rx="6" fill="#10b98118" stroke="#10b98130"/>
<text x="440" y="19" text-anchor="middle" fill="#34d399">WebSocket</text>
<!-- MIT -->
<rect x="502" y="0" width="62" height="28" rx="6" fill="#f59e0b18" stroke="#f59e0b30"/>
<text x="533" y="19" text-anchor="middle" fill="#fbbf24">MIT</text>
</g>
<!-- Bottom author strip -->
<text x="290" y="580"
font-family="'Inter', 'Helvetica Neue', Arial, sans-serif"
font-size="18" font-weight="500"
fill="#55556a">github.com/SmartGift · smartgift.vn</text>
</svg>

Before

Width:  |  Height:  |  Size: 6.4 KiB

+2 -4
View File
@@ -55,7 +55,7 @@
"dev:client": "cd client && npm run dev",
"build": "cd client && npm run build",
"start": "node server/index.js",
"setup": "npm install && (cd client && npm install) && (cd vscode-extension && npm install) && npm run link-cli",
"setup": "npm install && (cd client && npm install) && npm run link-cli",
"update:pull-setup": "git pull --ff-only && npm run setup",
"install-hooks": "node scripts/install-hooks.js",
"seed": "node scripts/seed.js",
@@ -68,9 +68,7 @@
"format": "prettier --write .",
"format:check": "prettier --check .",
"openapi:yaml": "node scripts/generate-openapi-yaml.js",
"link-cli": "npm link || node -e \"console.warn('agenthub CLI not linked globally (no permission?). Link manually with: npm link')\"",
"docker:up": "docker compose up -d --build",
"docker:down": "docker compose down"
"link-cli": "npm link || node -e \"console.warn('agenthub CLI not linked globally (no permission?). Link manually with: npm link')\""
},
"dependencies": {
"@lydell/node-pty": "^1.2.0-beta.15",
-5
View File
@@ -1,5 +0,0 @@
# https://smartgift.github.io/Claude-Code-Agent-Monitor/
User-agent: *
Allow: /
Sitemap: https://smartgift.github.io/Claude-Code-Agent-Monitor/sitemap.xml
-34
View File
@@ -13,8 +13,6 @@ Enterprise-grade Node.js backend for Claude Code agent monitoring with real-time
![better--sqlite3](https://img.shields.io/badge/better--sqlite3-11.7-003B57?style=flat-square&logo=sqlite&logoColor=white)
![Autoprefixer](https://img.shields.io/badge/Autoprefixer-10.4-DD3735?style=flat-square&logo=autoprefixer&logoColor=white)
![ESLint](https://img.shields.io/badge/ESLint-8.44-4B32C3?style=flat-square&logo=eslint&logoColor=white)
![Docker](https://img.shields.io/badge/Docker-20.10-2496ED?style=flat-square&logo=docker&logoColor=white)
![Podman](https://img.shields.io/badge/Podman-4.0-CC342D?style=flat-square&logo=podman&logoColor=white)
![Prometheus](https://img.shields.io/badge/Prometheus-2.x-E6522C?style=flat-square&logo=prometheus&logoColor=white)
![Grafana](https://img.shields.io/badge/Grafana-10.x-F46800?style=flat-square&logo=grafana&logoColor=white)
![SSE](https://img.shields.io/badge/SSE-Server_Sent_Events-FF6600?style=flat-square&logo=googlechrome&logoColor=white)
@@ -1532,38 +1530,6 @@ pm2 start server/index.js --name agenthub
sudo systemctl start agenthub
```
### Docker Deployment
```dockerfile
# Dockerfile (root of project)
FROM node:22-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
COPY client/package*.json ./client/
RUN npm ci --production && cd client && npm ci --production
# Build client
COPY client ./client
RUN cd client && npm run build
# Copy server
COPY server ./server
COPY data ./data
EXPOSE 4820
CMD ["node", "server/index.js"]
```
```bash
# Build and run
docker build -t agenthub .
docker run -p 127.0.0.1:4820:4820 -v "$HOME/.claude/agenthub:/app/data" agenthub
```
---
## Configuration
+2 -2
View File
@@ -231,8 +231,8 @@ function resolveAllDashboardPorts() {
/**
* Ports that should receive hook POSTs. When several live servers share the
* same SQLite data directory, only the lowest port per directory is returned
* so parallel instances (Docker + dev, two terminals on the same DB) never
* double-ingest events.
* so parallel instances (two terminals on the same DB) never double-ingest
* events.
*
* @returns {number[]}
*/
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://smartgift.github.io/Claude-Code-Agent-Monitor/</loc>
<lastmod>2026-07-29</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
</urlset>
@@ -52,7 +52,6 @@ i18n architecture: **Supported languages** list, `supportedLngs`, the 15 namespa
- `INSTALL.md`, `SETUP.md` — install/run commands.
- `CLAUDE.md`, `AGENTS.md` — agent working guides; update when commands, file locations, or workflows change.
- `docs/README.md` — docs index; add a link when a new `docs/*.md` is created.
- `vscode-extension/README.md`, `statusline/README.md` — surface-specific; update only when that surface changes.
## Consistency invariants
-183
View File
@@ -1,183 +0,0 @@
# Claude Code Statusline
A color-coded statusline for Claude Code showing model, user, working directory, git branch, context window usage, token counts, and session cost.
## Preview
Example statusline with all segments visible in a git repo and 24% context window usage:
| Segment | Color | Example |
| ----------- | -------------------- | -------------------------------------------------- |
| Model | Cyan | `Sonnet 4.6` |
| User | Green | `nguyens6` |
| CWD | Yellow | `~/agenthub/client` |
| Git branch | Magenta | `main` (hidden outside git repos) |
| Context bar | Green → Yellow → Red | `████████░░ 79%` |
| Tokens | Green / Cyan / Dim | `3↑ 2↓ 156586c` (green `↑` in, cyan `↓` out, dim `c` cache reads) |
| Cost (USD) | Green → Yellow → Red | `$0.4231` (session total — shown on API and subscription plans) |
Context bar color thresholds:
- **Green** — under 50% used
- **Yellow** — 50–79% used
- **Red** — 80%+ used
Cost color thresholds:
- **Green** — under $5
- **Yellow** — $5–$20
- **Red** — $20+
## Requirements
- Python 3.6+
- Git (for branch detection)
- Claude Code 2.x+
## Installation
**1. Copy both files into your Claude config directory:**
```bash
# macOS / Linux
cp statusline.py ~/.claude/statusline.py
cp statusline-command.sh ~/.claude/statusline-command.sh
chmod +x ~/.claude/statusline-command.sh
# Windows (Git Bash)
cp statusline.py "$HOME/.claude/statusline.py"
cp statusline-command.sh "$HOME/.claude/statusline-command.sh"
```
**2. Update the path in `statusline-command.sh`:**
Open `~/.claude/statusline-command.sh` and replace the path with your own home directory:
```bash
#!/usr/bin/env bash
PYTHONUTF8=1 python3 "/your/home/.claude/statusline.py"
```
On Windows this looks like:
```bash
#!/usr/bin/env bash
PYTHONUTF8=1 python3 "C:/Users/YOUR_USERNAME/.claude/statusline.py"
```
On macOS/Linux:
```bash
#!/usr/bin/env bash
python3 "$HOME/.claude/statusline.py"
```
**3. Add to `~/.claude/settings.json`:**
```json
{
"statusLine": {
"type": "command",
"command": "bash \"/path/to/home/.claude/statusline-command.sh\""
}
}
```
Windows example:
```json
{
"statusLine": {
"type": "command",
"command": "bash \"C:/Users/YOUR_USERNAME/.claude/statusline-command.sh\""
}
}
```
macOS/Linux example:
```json
{
"statusLine": {
"type": "command",
"command": "bash \"/home/YOUR_USERNAME/.claude/statusline-command.sh\""
}
}
```
**4. Restart Claude Code** — fully exit and relaunch. Claude Code does not hot-reload `settings.json`, so the new statusline will not show up until the process is restarted.
## Troubleshooting
**The statusline still shows the default (e.g. `[Model] 📁 cwd`) after restarting.**
Claude Code resolves `statusLine` from multiple settings files, in this precedence (later wins):
1. `~/.claude/settings.json` (user, global)
2. `~/.claude/settings.local.json` (user, local — **often overrides the global**)
3. `<project>/.claude/settings.json` (project, shared)
4. `<project>/.claude/settings.local.json` (project, local)
If any of those later files defines its own `statusLine` block, it **replaces** the global one — yours will never run. Grep for it:
```bash
grep -l statusLine ~/.claude/settings*.json $(find . -maxdepth 3 -name 'settings*.json' -path '*.claude*' 2>/dev/null)
```
For each file that defines a competing `statusLine`, either delete that block (to fall back to the global one) or point it at the same script:
```json
{
"statusLine": {
"type": "command",
"command": "bash \"/path/to/home/.claude/statusline-command.sh\""
}
}
```
**The statusline is blank / missing segments.**
Run the script manually with a sample payload to confirm it works:
```bash
echo '{"model":{"display_name":"Sonnet 4.6"},"workspace":{"current_dir":"'"$HOME"'"},"context_window":{"used_percentage":25,"current_usage":{"input_tokens":1000,"output_tokens":500,"cache_read_input_tokens":200}},"cost":{"total_cost_usd":0.4231}}' \
| sh ~/.claude/statusline-command.sh
```
If nothing prints, check that `python3` is on your `PATH` and `~/.claude/statusline.py` is readable. The script always exits 0 by design so Claude Code never blocks — errors are silent, so test from the shell first.
## How It Works
Claude Code pipes a JSON object to the statusline command's stdin on each update. The script reads that JSON, extracts the relevant fields, and prints a color-coded string using ANSI escape codes.
Key fields used from the JSON payload:
```json
{
"model": { "display_name": "Sonnet 4.6" },
"workspace": { "current_dir": "C:\\Users\\..." },
"context_window": {
"used_percentage": 79,
"current_usage": {
"input_tokens": 3,
"output_tokens": 2,
"cache_read_input_tokens": 156586
}
},
"cost": { "total_cost_usd": 0.4231 }
}
```
## Customization
Edit `statusline.py` directly to change colors, reorder segments, or remove ones you don't want. Each segment is clearly labeled with a comment.
Color constants at the top of the file:
```python
CYAN = '\033[0;36m'
GREEN = '\033[0;32m'
YELLOW = '\033[0;33m'
MAGENTA = '\033[0;35m'
RED = '\033[0;31m'
DIM = '\033[2m'
```
-10
View File
@@ -1,10 +0,0 @@
#!/usr/bin/env bash
#
# @file statusline-command.sh — wrapper that Claude Code invokes for its custom statusline;
# pipes the JSON event payload into statusline.py (update the path to match your install).
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
#
PYTHONUTF8=1 python3 "C:/Users/nguyens6/.claude/statusline.py"
# IMPORTANT:
# Put the statusline.py file in the .claude folder in your user directory (e.g. C:/Users/nguyens6/.claude/statusline.py) and make sure to update the path in the above command if you place it somewhere else.
# Change the above path to the location of your statusline.py file. You can also change the command to run a different script if you want to display something else in your status line.
-136
View File
@@ -1,136 +0,0 @@
"""
statusline.py — Claude Code custom statusline renderer.
Reads a JSON event payload from stdin (emitted by Claude Code on every turn)
and prints a single ANSI-colored status line to stdout. Renders the model
display name, current user, working directory (with home collapsed to ``~``),
active git branch, a color-coded context window usage bar (green / yellow /
red thresholds), and token counts. Always exits 0 so it can never block
Claude Code — missing fields are silently skipped.
Intended to be referenced from Claude Code's ``statusLine`` setting (see
``statusline-command.sh`` for the wrapper) or installed to
``~/.claude/statusline.py``.
:file: statusline/statusline.py
:module: Claude-Code-Agent-Monitor.statusline
:author: Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
:maintainer: Nguyễn Ngọc Trí Vĩ (a.k.a. David Nguyen, smartgift)
:copyright: (c) 2026 Nguyễn Ngọc Trí Vĩ
:license: MIT
:repository: https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor
:requires: Python 3.6+
:encoding: utf-8
@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
"""
__file_name__ = "statusline.py"
__module__ = "Claude-Code-Agent-Monitor.statusline"
__author__ = "Nguyễn Ngọc Trí Vĩ"
__email__ = "vinnt@smartgift.vn"
__maintainer__ = "Nguyễn Ngọc Trí Vĩ"
__copyright__ = "Copyright (c) 2026 Nguyễn Ngọc Trí Vĩ"
__license__ = "MIT"
__version__ = "1.0.0"
__status__ = "Production"
__repository__ = "https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor"
import sys
import json
import os
import subprocess
sys.stdout.reconfigure(encoding='utf-8')
CYAN = '\033[0;36m'
GREEN = '\033[0;32m'
YELLOW = '\033[0;33m'
MAGENTA = '\033[0;35m'
RED = '\033[0;31m'
DIM = '\033[2m'
RESET = '\033[0m'
raw = sys.stdin.read().strip()
if not raw:
sys.exit(0)
try:
data = json.loads(raw)
except Exception:
sys.exit(0)
parts = []
# Model
model = (data.get('model') or {}).get('display_name', '')
if model:
parts.append(f"{CYAN}{model}{RESET}")
# User
user = os.environ.get('USERNAME') or os.environ.get('USER', '')
if user:
parts.append(f"{GREEN}{user}{RESET}")
# CWD — strip home prefix
cwd = (data.get('workspace') or {}).get('current_dir') or data.get('cwd', '')
if cwd:
home = os.path.expanduser('~') # C:\Users\nguyens6
if cwd.startswith(home):
cwd = '~' + cwd[len(home):].replace('\\', '/')
else:
cwd = cwd.replace('\\', '/')
parts.append(f"{YELLOW}{cwd}{RESET}")
# Git branch
git_dir = (data.get('workspace') or {}).get('current_dir') or data.get('cwd', '')
if git_dir:
try:
branch = subprocess.check_output(
['git', '-C', git_dir, '--no-optional-locks', 'symbolic-ref', '--short', 'HEAD'],
stderr=subprocess.DEVNULL
).decode().strip()
except Exception:
try:
branch = subprocess.check_output(
['git', '-C', git_dir, '--no-optional-locks', 'rev-parse', '--short', 'HEAD'],
stderr=subprocess.DEVNULL
).decode().strip()
except Exception:
branch = ''
if branch:
parts.append(f"{MAGENTA}{branch}{RESET}")
# Context bar
ctx = data.get('context_window') or {}
used_pct = ctx.get('used_percentage')
if used_pct is not None:
bar_len = 10
filled = round(bar_len * used_pct / 100)
bar = '█' * filled + '░' * (bar_len - filled)
color = RED if used_pct >= 80 else YELLOW if used_pct >= 50 else GREEN
parts.append(f"{color}{bar} {used_pct}%{RESET}")
# Tokens
usage = ctx.get('current_usage') or {}
in_tok = usage.get('input_tokens')
out_tok = usage.get('output_tokens')
cache = usage.get('cache_read_input_tokens')
if in_tok is not None and out_tok is not None:
tok_parts = [f"{GREEN}{in_tok}↑{RESET}", f"{CYAN}{out_tok}↓{RESET}"]
if cache:
tok_parts.append(f"{DIM}{cache}c{RESET}")
parts.append(' '.join(tok_parts))
# Session cost (USD) — shown on both API and subscription plans
cost = (data.get('cost') or {}).get('total_cost_usd')
if cost is not None:
try:
cost_f = float(cost)
cost_color = RED if cost_f >= 20 else YELLOW if cost_f >= 5 else GREEN
parts.append(f"{cost_color}${cost_f:.4f}{RESET}")
except (TypeError, ValueError):
pass
sep = f"{DIM} | {RESET}"
print(sep.join(parts))
-50
View File
@@ -1,50 +0,0 @@
/**
* @file sw.js
* @description Service worker for the project landing page — precaches the shell (index, favicon, OG image, manifest) and manages cache lifecycle for offline support.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const CACHE_NAME = "landing-v1";
const PRECACHE = ["./", "./index.html", "./favicon.svg", "./og-image.svg", "./manifest.json"];
self.addEventListener("install", (event) => {
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE)));
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches
.keys()
.then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
)
);
self.clients.claim();
});
self.addEventListener("fetch", (event) => {
const { request } = event;
if (request.method !== "GET") return;
if (request.mode === "navigate") {
// Network-first for HTML
event.respondWith(fetch(request).catch(() => caches.match("./index.html")));
return;
}
// Cache-first for assets, lazy-cache on miss
event.respondWith(
caches.match(request).then(
(cached) =>
cached ||
fetch(request).then((response) => {
if (response.ok) {
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
}
return response;
})
)
);
});
-3
View File
@@ -1,3 +0,0 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
}
-40
View File
@@ -1,40 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": [],
"presentation": {
"group": "1_extension",
"order": 1
}
},
{
"name": "Run Extension (clean profile)",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--disable-extensions",
"--profile-temp"
],
"presentation": {
"group": "1_extension",
"order": 2
}
},
{
"name": "Run Extension (open repo)",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/.."],
"presentation": {
"group": "1_extension",
"order": 3
}
}
]
}
-15
View File
@@ -1,15 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "package",
"type": "shell",
"command": "npx vsce package",
"problemMatcher": [],
"presentation": {
"reveal": "always",
"panel": "shared"
}
}
]
}
-79
View File
@@ -1,79 +0,0 @@
# AgentHub for VS Code
**AgentHub** brings a high-fidelity, real-time dashboard for your Claude Code AI sessions directly into your VS Code workspace. Monitor token usage, track agent health, and view deep analytics without switching windows.
![Extension Screenshot](https://git.smartgift.io.vn/Smartgift-AI/AgentHub/raw/branch/main/vscode-extension/vscode.png)
---
## 🚀 Key Features
### 📊 Integrated Dashboard
Open the full agent monitoring dashboard in a dedicated VS Code tab. Supports real-time updates via WebSockets and local-first data storage.
### 🩺 Live Agent Health
Track all 5 agent states in your sidebar with color-coded status indicators:
- **Working**: Active subagents currently processing tasks.
- **Connected**: Agents initialized and ready.
- **Idle**: Standby agents.
- **Completed**: Successfully finished tasks.
- **Error/Failed**: Instantly spot where things went wrong.
### 📈 Usage & Analytics
- **Token Tracking**: Real-time display of Billions (B) or Millions (M) of tokens consumed.
- **Cost Calculation**: Live estimate of total USD cost based on current model pricing.
- **Event Frequency**: High-signal stats on events per session and subagent spawning.
### 🧭 Deep Navigation
Jump directly to specific views from the sidebar:
- **Main Dashboard**: Global overview.
- **Analytics Hub**: Deep dives into model performance.
- **Agent Board**: Kanban-style view of subagent progress.
- **Recent Sessions**: One-click access to your last 10 Claude sessions.
---
## 🛠️ Getting Started
### 1. Requirements
Ensure you have the AgentHub server running locally:
```bash
npm install
npm run dev
```
### 2. Installation
- Search for **"AgentHub"** in the VS Code Marketplace.
- Click **Install**.
### 3. Usage
- Click the **Radar/Dashboard icon** in the Activity Bar.
- The extension automatically detects your local server on ports **5173** (dev) or **4820** (prod).
- Use the **Refresh** button in the sidebar to manually force an update.
---
## 📦 Publishing & Development
To generate a `.vsix` file for manual installation or marketplace upload:
1. Install `vsce` globally:
```bash
npm install -g @vscode/vsce
```
2. Navigate to the extension folder:
```bash
cd vscode-extension
```
3. Run the package command:
```bash
vsce package
```
*This will generate `claude-code-agent-monitor-1.0.0.vsix`.*
---
## 📜 License
Internal SmartGift build — all rights reserved.
Developed with ❤️ for the AI Engineer community.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

-338
View File
@@ -1,338 +0,0 @@
/**
* @file Main extension file for AgentHub VSCode extension
* Sets up the extension, registers commands, and manages the status bar item.
* Implements a dynamic dashboard view that checks for active servers on ports 5173 and 4820.
* Provides real-time status updates in the sidebar and status bar with background polling.
*
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const vscode = require("vscode");
const http = require("http");
const { DashboardWebviewProvider } = require("./sidebar");
let statusBarItem;
let outputChannel;
function activate(context) {
outputChannel = vscode.window.createOutputChannel("Claude Code Monitor");
outputChannel.appendLine("[activate] " + new Date().toISOString());
context.subscriptions.push(outputChannel);
const statusProvider = new DashboardWebviewProvider(context, outputChannel);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider("claude-code-monitor-view", statusProvider, {
webviewOptions: { retainContextWhenHidden: true },
})
);
statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
statusBarItem.command = "claude-code-agent-monitor.openDashboard";
context.subscriptions.push(statusBarItem);
updateStatusBar();
const statusInterval = setInterval(() => {
updateStatusBar();
}, 5000); // Auto-refresh every 5 seconds
let openDashboard = vscode.commands.registerCommand(
"claude-code-agent-monitor.openDashboard",
async (target) => {
const panel = vscode.window.createWebviewPanel(
"agentMonitor",
"AgentHub",
vscode.ViewColumn.One,
{ enableScripts: true, retainContextWhenHidden: true }
);
const updateWebview = async () => {
const ports = [5173, 4820];
let activePort = null;
for (const port of ports) {
if (await checkPort(port)) {
activePort = port;
break;
}
}
if (activePort) {
let suffix = "";
if (target) {
suffix = target.includes("-") ? `/sessions/${target}` : `/${target}`;
}
panel.webview.html = getDashboardHtml(activePort, suffix);
} else {
panel.webview.html = getErrorHtml();
}
};
panel.webview.onDidReceiveMessage((m) => {
if (m.command === "retry") updateWebview();
});
await updateWebview();
}
);
let openInBrowser = vscode.commands.registerCommand(
"claude-code-agent-monitor.openInBrowser",
async () => {
const isDevUp = await checkPort(5173);
const url = isDevUp ? "http://localhost:5173" : "http://localhost:4820";
vscode.env.openExternal(vscode.Uri.parse(url));
}
);
let refreshStatus = vscode.commands.registerCommand(
"claude-code-agent-monitor.refreshStatus",
() => {
statusProvider.refresh();
updateStatusBar();
vscode.window.showInformationMessage("Claude Code Monitor refreshed.");
}
);
let clearHistory = vscode.commands.registerCommand(
"claude-code-agent-monitor.clearHistory",
async () => {
if (
(await vscode.window.showWarningMessage("Clear all history?", { modal: true }, "Yes")) ===
"Yes"
) {
try {
const res = await request("DELETE", 4820, "/api/sessions");
if (res.statusCode === 200) {
statusProvider.refresh();
updateStatusBar();
vscode.window.showInformationMessage("History cleared.");
}
} catch (e) {
vscode.window.showErrorMessage("Failed to clear history.");
}
}
}
);
context.subscriptions.push(openDashboard, openInBrowser, refreshStatus, clearHistory);
context.subscriptions.push({ dispose: () => clearInterval(statusInterval) });
}
async function updateStatusBar() {
try {
const stats = await fetchJson(4820, "/api/stats");
if (stats) {
statusBarItem.text = `$(pulse) Claude: ${stats.sessions || 0}s | ${stats.agents || 0}a`;
statusBarItem.show();
} else statusBarItem.hide();
} catch (e) {
statusBarItem.hide();
}
}
function checkPort(port) {
return new Promise((r) => {
const req = http.get({ hostname: "localhost", port, path: "/", timeout: 500 }, (res) => {
r(true);
res.resume();
});
req.on("error", () => r(false));
});
}
function fetchJson(port, path) {
return new Promise((res, rej) => {
const req = http.get({ hostname: "localhost", port, path, timeout: 800 }, (r) => {
let d = "";
r.on("data", (c) => (d += c));
r.on("end", () => {
try {
res(JSON.parse(d));
} catch (e) {
rej(e);
}
});
});
req.on("error", rej);
});
}
function request(method, port, path) {
return new Promise((res, rej) => {
const req = http.request({ hostname: "localhost", port, path, method }, res);
req.on("error", rej);
req.end();
});
}
function getDashboardHtml(port, suffix) {
return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">
<style>body,html{margin:0;padding:0;height:100%;width:100%;overflow:hidden;background:#0f111a;}
iframe{border:none;height:calc(100% - 30px);width:100%;}
.t{height:30px;background:#1a1a2e;display:flex;align-items:center;padding:0 10px;font-size:11px;color:#888;border-bottom:1px solid #2e2e48;font-family:sans-serif;}
.d{width:8px;height:8px;border-radius:50%;background:#10b981;margin-right:8px;box-shadow:0 0 5px #10b981;}
.u{margin-left:auto;opacity:0.5;}</style></head>
<body><div class="t"><div class="d"></div>Live Dashboard: ${suffix || "/"} <div class="u">localhost:${port}</div></div>
<iframe src="http://localhost:${port}${suffix}"></iframe></body></html>`;
}
function getErrorHtml() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
:root {
--bg: #0b0c14;
--card: #141624;
--accent: #6366f1;
--accent-glow: rgba(99, 102, 241, 0.15);
--text: #ffffff;
--text-muted: #94a3b8;
--border: rgba(255, 255, 255, 0.08);
--code-bg: #000000;
}
* { box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
margin: 0; padding: 20px;
background-color: var(--bg);
background-image:
radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.1) 0px, transparent 50%),
radial-gradient(at 100% 100%, rgba(168, 85, 247, 0.1) 0px, transparent 50%);
color: var(--text);
display: flex; align-items: center; justify-content: center;
min-height: 100vh;
overflow-x: hidden;
}
.container {
max-width: 800px; width: 100%;
animation: fadeIn 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
.hero { text-align: center; margin-bottom: 48px; }
.logo-wrap {
width: 80px; height: 80px; background: var(--card); border: 1px solid var(--border);
border-radius: 24px; margin: 0 auto 24px; display: flex; align-items: center; justify-content: center;
box-shadow: 0 0 40px var(--accent-glow); position: relative;
}
.logo-wrap::after {
content: ''; position: absolute; inset: -1px; border-radius: 24px;
padding: 1px; background: linear-gradient(45deg, var(--accent), transparent);
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor; mask-composite: exclude;
}
h1 { font-size: 36px; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 12px 0; }
.subtitle { color: var(--text-muted); font-size: 18px; font-weight: 300; }
.main-card {
background: rgba(20, 22, 36, 0.7);
backdrop-filter: blur(20px);
border: 1px solid var(--border);
border-radius: 32px;
padding: 40px;
display: grid; grid-template-columns: 1.2fr 1fr; gap: 40px;
}
@media (max-width: 768px) { .main-card { grid-template-columns: 1fr; padding: 24px; } }
.setup-guide h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--accent); margin: 0 0 24px 0; }
.timeline { position: relative; padding-left: 32px; }
.timeline::before { content: ''; position: absolute; left: 7px; top: 8px; bottom: 8px; width: 1px; background: var(--border); }
.t-step { position: relative; margin-bottom: 32px; }
.t-step::before {
content: ''; position: absolute; left: -32px; top: 6px; width: 16px; height: 16px;
background: var(--bg); border: 2px solid var(--accent); border-radius: 50%; z-index: 1;
}
.t-label { font-size: 15px; font-weight: 500; margin-bottom: 8px; color: #fff; }
.t-code {
background: var(--code-bg); padding: 12px 16px; border-radius: 12px;
font-family: 'SFMono-Regular', Consolas, monospace; font-size: 13px; color: #a5b4fc;
border: 1px solid rgba(255,255,255,0.05); white-space: pre-wrap;
}
.side-panel { display: flex; flex-direction: column; justify-content: center; gap: 24px; }
.info-box { background: rgba(255,255,255,0.03); border-radius: 20px; padding: 24px; border: 1px solid var(--border); }
.info-box h3 { font-size: 15px; margin: 0 0 8px 0; display: flex; align-items: center; gap: 8px; }
.info-box p { font-size: 13px; color: var(--text-muted); margin: 0; line-height: 1.5; }
.btn {
width: 100%; padding: 16px; border-radius: 14px; font-size: 15px; font-weight: 600;
display: flex; align-items: center; justify-content: center; gap: 8px;
cursor: pointer; transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); text-decoration: none;
}
.btn-primary { background: var(--accent); color: #fff; border: none; box-shadow: 0 8px 24px rgba(99, 102, 241, 0.25); }
.btn-primary:hover { background: #4f46e5; transform: translateY(-2px); box-shadow: 0 12px 32px rgba(99, 102, 241, 0.35); }
.btn-secondary { background: rgba(255,255,255,0.05); color: #fff; border: 1px solid var(--border); }
.btn-secondary:hover { background: rgba(255,255,255,0.1); transform: translateY(-2px); }
.footer { text-align: center; margin-top: 48px; display: flex; justify-content: center; gap: 32px; }
.footer a { color: var(--text-muted); text-decoration: none; font-size: 14px; transition: color 0.2s; }
.footer a:hover { color: var(--accent); }
</style>
</head>
<body>
<div class="container">
<div class="hero">
<div class="logo-wrap">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: var(--accent)">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M12 8v4"/><path d="M12 16h.01"/>
</svg>
</div>
<h1>Dashboard Offline</h1>
<div class="subtitle">Unable to connect to the Claude Code Monitor server</div>
</div>
<div class="main-card">
<div class="setup-guide">
<h2>Quick Setup Guide</h2>
<div class="timeline">
<div class="t-step">
<div class="t-label">Initialize Repository</div>
<div class="t-code">git clone https://git.smartgift.io.vn/Smartgift-AI/AgentHub.git
cd AgentHub
npm run setup</div>
</div>
<div class="t-step" style="margin-bottom: 0;">
<div class="t-label">Launch Dashboard</div>
<div class="t-code">npm run dev</div>
</div>
</div>
</div>
<div class="side-panel">
<div class="info-box">
<h3>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<path d="M12 16v-4"/>
<path d="M12 8h.01"/>
</svg>
Troubleshooting
</h3>
<p>Ensure no other services are occupying ports 5173 or 4820. If the problem persists, check the documentation.</p>
</div>
<button class="btn btn-primary" onclick="acquireVsCodeApi().postMessage({command:'retry'})">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M23 4v6h-6"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
Retry Connection
</button>
<a href="https://git.smartgift.io.vn/Smartgift-AI/AgentHub/src/branch/main/docs" class="btn btn-secondary">
View Documentation
</a>
</div>
</div>
<div class="footer">
<a href="https://git.smartgift.io.vn/Smartgift-AI/AgentHub">Repository</a>
<a href="https://git.smartgift.io.vn/Smartgift-AI/AgentHub/issues">Support</a>
</div>
</div>
</body>
</html>`;
}
function deactivate() {
if (statusBarItem) statusBarItem.dispose();
}
module.exports = { activate, deactivate };
Binary file not shown.

Before

Width:  |  Height:  |  Size: 688 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

-8
View File
@@ -1,8 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM12 4C16.4183 4 20 7.58172 20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12C4 7.58172 7.58172 4 12 4Z" fill="currentColor" fill-opacity="0.8"/>
<circle cx="12" cy="12" r="3" fill="currentColor"/>
<path d="M12 7V9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M12 15V17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M7 12H9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M15 12H17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 783 B

-1612
View File
File diff suppressed because it is too large Load Diff
-115
View File
@@ -1,115 +0,0 @@
{
"name": "claude-code-agent-monitor",
"displayName": "AgentHub",
"description": "Professional integrated dashboard for monitoring Claude Code AI agent activity, token usage, and costs directly in VS Code.",
"version": "1.2.4",
"publisher": "hoangsonw",
"engines": {
"vscode": "^1.75.0"
},
"categories": [
"Data Science",
"Machine Learning",
"Programming Languages",
"Other"
],
"activationEvents": [
"onView:claude-code-monitor-view"
],
"main": "./extension.js",
"icon": "apple-touch-icon.png",
"galleryBanner": {
"color": "#0f111a",
"theme": "dark"
},
"repository": {
"type": "git",
"url": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub"
},
"homepage": "https://git.smartgift.io.vn/Smartgift-AI/AgentHub/src/branch/main/vscode-extension/README.md",
"contributes": {
"commands": [
{
"command": "claude-code-agent-monitor.openDashboard",
"title": "Open Agent Dashboard",
"category": "Claude Code",
"icon": "$(dashboard)"
},
{
"command": "claude-code-agent-monitor.refreshStatus",
"title": "Refresh Dashboard Status",
"category": "Claude Code",
"icon": "$(refresh)"
},
{
"command": "claude-code-agent-monitor.clearHistory",
"title": "Clear Monitor History",
"category": "Claude Code",
"icon": "$(trash)"
},
{
"command": "claude-code-agent-monitor.openInBrowser",
"title": "Open Dashboard in Browser",
"category": "Claude Code",
"icon": "$(globe)"
}
],
"viewsContainers": {
"activitybar": [
{
"id": "claude-code-monitor",
"title": "AgentHub",
"icon": "icon.svg"
}
]
},
"views": {
"claude-code-monitor": [
{
"id": "claude-code-monitor-view",
"name": "Monitor Control Center",
"type": "webview"
}
]
},
"menus": {
"view/title": [
{
"command": "claude-code-agent-monitor.openDashboard",
"when": "view == claude-code-monitor-view",
"group": "navigation@1"
},
{
"command": "claude-code-agent-monitor.refreshStatus",
"when": "view == claude-code-monitor-view",
"group": "navigation@2"
},
{
"command": "claude-code-agent-monitor.openInBrowser",
"when": "view == claude-code-monitor-view",
"group": "navigation@3"
}
]
}
},
"devDependencies": {
"@types/vscode": "^1.75.0",
"vsce": "^2.15.0"
},
"scripts": {
"package": "vsce package",
"publish": "vsce publish"
},
"keywords": [
"claude",
"anthropic",
"ai",
"agent",
"monitor",
"dashboard",
"tokens",
"llm"
],
"author": "Nguyễn Ngọc Trí Vĩ",
"license": "MIT"
}
-747
View File
@@ -1,747 +0,0 @@
/**
* @file Sidebar provider for AgentHub VSCode extension
* Renders a rich, real-time WebviewView showing backend status, live agent
* health, usage analytics, recent sessions, and quick navigation.
*
* Replaces the previous flat TreeDataProvider with a styled WebviewView that
* pushes JSON snapshots from background polling and receives action messages
* (open dashboard, open session, refresh, browser, clear history) back from
* the webview UI.
*
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const vscode = require("vscode");
const http = require("http");
const POLL_INTERVAL_MS = 5000;
const SPARK_HISTORY = 20;
class DashboardWebviewProvider {
constructor(context, output) {
this.context = context;
this.output = output;
this.view = null;
this.status = "Offline";
this.data = {};
this.history = {
sessions: [],
agents: [],
tokens: [],
cost: [],
};
this._pollHandle = null;
this._fetching = null;
}
log(msg) {
if (this.output) {
try {
this.output.appendLine("[" + new Date().toISOString() + "] " + msg);
} catch (_) {}
}
}
resolveWebviewView(view) {
this.view = view;
view.webview.options = {
enableScripts: true,
localResourceRoots: [],
};
view.webview.html = this.getHtml();
view.webview.onDidReceiveMessage((msg) => this.handleMessage(msg));
view.onDidChangeVisibility(() => {
if (this.view && this.view.visible) this.refresh();
});
view.onDidDispose(() => {
this.view = null;
if (this._pollHandle) clearInterval(this._pollHandle);
this._pollHandle = null;
});
this.refresh();
if (!this._pollHandle) {
this._pollHandle = setInterval(() => this.refresh(), POLL_INTERVAL_MS);
}
}
refresh() {
return this.fetchAll()
.then(() => {
try {
this.pushSnapshot();
} catch (e) {
this.log("pushSnapshot threw: " + e.message);
}
})
.catch((e) => this.log("refresh threw: " + e.message));
}
handleMessage(msg) {
if (!msg || !msg.command) return;
switch (msg.command) {
case "openDashboard":
vscode.commands.executeCommand("claude-code-agent-monitor.openDashboard", msg.target || "");
break;
case "openInBrowser":
vscode.commands.executeCommand("claude-code-agent-monitor.openInBrowser");
break;
case "refresh":
vscode.commands.executeCommand("claude-code-agent-monitor.refreshStatus");
break;
case "clearHistory":
vscode.commands.executeCommand("claude-code-agent-monitor.clearHistory");
break;
case "ready":
this.log("webview READY");
this.refresh();
break;
case "log":
this.log("[webview] " + msg.text);
break;
case "error":
this.log("[webview ERROR] " + msg.text);
break;
}
}
pushSnapshot() {
if (!this.view) {
this.log("pushSnapshot skipped: view is null");
return;
}
const d = this.data || {};
const t = (d.analytics && d.analytics.tokens) || {};
const totalTokens =
(t.total_input || 0) +
(t.total_output || 0) +
(t.total_cache_read || 0) +
(t.total_cache_write || 0);
const snapshot = {
status: this.status,
port: d.port || null,
stats: {
ws: (d.stats && d.stats.ws_connections) || 0,
sessions: (d.stats && d.stats.total_sessions) || 0,
events: (d.stats && d.stats.total_events) || 0,
agents_total:
(d.stats &&
d.stats.agents_by_status &&
Object.values(d.stats.agents_by_status).reduce((a, b) => a + b, 0)) ||
0,
agents_by_status: (d.stats && d.stats.agents_by_status) || {},
},
analytics: {
tokens: {
input: t.total_input || 0,
output: t.total_output || 0,
cache_read: t.total_cache_read || 0,
cache_write: t.total_cache_write || 0,
total: totalTokens,
},
cost: (d.analytics && d.analytics.total_cost) || 0,
subagents: (d.analytics && d.analytics.total_subagents) || 0,
},
sessions: (Array.isArray(d.sessions) ? d.sessions : []).slice(0, 12).map((s) => ({
id: s.id,
name: s.name || s.id.substring(0, 8),
status: s.status || "unknown",
model: s.model || "unknown",
started_at: s.started_at,
})),
history: this.history,
ts: Date.now(),
};
this.view.webview.postMessage({ type: "snapshot", payload: snapshot });
}
pushHistory(snapshot) {
const cap = (arr, v) => {
arr.push(v);
if (arr.length > SPARK_HISTORY) arr.shift();
};
cap(this.history.sessions, snapshot.stats.sessions);
cap(this.history.agents, snapshot.stats.agents_total);
cap(this.history.tokens, snapshot.analytics.tokens.total);
cap(this.history.cost, snapshot.analytics.cost);
}
async fetchAll() {
if (this._fetching) return this._fetching;
this._fetching = (async () => {
const ports = [4820, 5173];
let foundActive = false;
for (const p of ports) {
const up = await this.ping(p);
this.log("ping " + p + " => " + up);
if (up) {
this.status = "Online";
this.data.port = p;
foundActive = true;
try {
this.data.stats = await this.f(4820, "/api/stats");
this.data.analytics = await this.f(4820, "/api/analytics");
const sess = await this.f(4820, "/api/sessions?limit=12");
this.data.sessions = Array.isArray(sess)
? sess
: (sess && (sess.sessions || sess.rows || sess.data)) || [];
if (!Array.isArray(this.data.sessions)) this.data.sessions = [];
this.log("fetched stats+analytics+sessions ok");
} catch (e) {
this.log("fetch error: " + (e && e.message ? e.message : e));
}
break;
}
}
if (!foundActive) {
this.status = "Offline";
this.data = {};
}
this.log("fetchAll done, status=" + this.status);
})();
try {
await this._fetching;
} finally {
this._fetching = null;
}
// Update sparkline history off the freshest snapshot
const t = (this.data.analytics && this.data.analytics.tokens) || {};
const totalTokens =
(t.total_input || 0) +
(t.total_output || 0) +
(t.total_cache_read || 0) +
(t.total_cache_write || 0);
this.pushHistory({
stats: {
sessions: (this.data.stats && this.data.stats.total_sessions) || 0,
agents_total:
(this.data.stats &&
this.data.stats.agents_by_status &&
Object.values(this.data.stats.agents_by_status).reduce((a, b) => a + b, 0)) ||
0,
},
analytics: {
tokens: { total: totalTokens },
cost: (this.data.analytics && this.data.analytics.total_cost) || 0,
},
});
}
ping(p) {
return new Promise((resolve) => {
let done = false;
const finish = (v) => {
if (done) return;
done = true;
try {
req.destroy();
} catch (_) {}
resolve(v);
};
const req = http.get(
{ hostname: "127.0.0.1", port: p, path: p === 4820 ? "/api/health" : "/", timeout: 800 },
(res) => {
res.resume();
finish(true);
}
);
req.on("error", () => finish(false));
req.on("timeout", () => finish(false));
});
}
f(p, path) {
return new Promise((resolve, reject) => {
let done = false;
const finish = (fn, v) => {
if (done) return;
done = true;
try {
req.destroy();
} catch (_) {}
fn(v);
};
const req = http.get({ hostname: "127.0.0.1", port: p, path, timeout: 1500 }, (r) => {
let d = "";
r.on("data", (c) => (d += c));
r.on("end", () => {
try {
finish(resolve, JSON.parse(d));
} catch (e) {
finish(reject, e);
}
});
});
req.on("error", (e) => finish(reject, e));
req.on("timeout", () => finish(reject, new Error("timeout")));
});
}
getHtml() {
const nonce = makeNonce();
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-${nonce}';" />
<style>
:root {
--bg: var(--vscode-sideBar-background, #1e1e2e);
--fg: var(--vscode-foreground, #e6e6e6);
--muted: var(--vscode-descriptionForeground, #9aa0a6);
--border: var(--vscode-panel-border, rgba(255,255,255,0.08));
--hover: var(--vscode-list-hoverBackground, rgba(255,255,255,0.05));
--accent: var(--vscode-charts-blue, #6366f1);
--accent-2: var(--vscode-charts-purple, #a855f7);
--green: var(--vscode-charts-green, #10b981);
--red: var(--vscode-charts-red, #ef4444);
--yellow: var(--vscode-charts-yellow, #f59e0b);
--orange: var(--vscode-charts-orange, #f97316);
--card: color-mix(in srgb, var(--bg) 70%, white 5%);
}
* { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
background: var(--bg); color: var(--fg);
font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif);
font-size: 12px;
overflow-x: hidden;
}
body {
padding: 10px 10px 18px;
animation: fade .35s ease both;
}
@keyframes fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
/* ===== Header ===== */
.hero {
display: flex; align-items: center; gap: 10px;
padding: 12px;
border-radius: 12px;
background: linear-gradient(135deg,
color-mix(in srgb, var(--accent) 18%, transparent),
color-mix(in srgb, var(--accent-2) 12%, transparent));
border: 1px solid var(--border);
margin-bottom: 12px;
position: relative; overflow: hidden;
}
.hero::after {
content: ''; position: absolute; inset: 0;
background: radial-gradient(circle at 90% 0%, color-mix(in srgb, var(--accent) 30%, transparent), transparent 60%);
pointer-events: none;
}
.logo {
width: 30px; height: 30px; border-radius: 9px;
background: color-mix(in srgb, var(--accent) 30%, var(--bg));
display: grid; place-items: center; flex-shrink: 0;
box-shadow: 0 0 18px color-mix(in srgb, var(--accent) 30%, transparent);
}
.logo svg { width: 18px; height: 18px; }
.hero-title { font-weight: 600; font-size: 13px; letter-spacing: .2px; }
.hero-sub { color: var(--muted); font-size: 10.5px; margin-top: 1px; }
.pill {
display: inline-flex; align-items: center; gap: 6px;
padding: 3px 8px; border-radius: 999px; font-size: 10.5px; font-weight: 600;
background: color-mix(in srgb, var(--green) 18%, transparent);
color: var(--green); border: 1px solid color-mix(in srgb, var(--green) 35%, transparent);
margin-left: auto; position: relative; z-index: 1;
}
.pill.off {
background: color-mix(in srgb, var(--red) 18%, transparent);
color: var(--red); border-color: color-mix(in srgb, var(--red) 35%, transparent);
}
.pill .dot {
width: 7px; height: 7px; border-radius: 50%; background: currentColor;
box-shadow: 0 0 0 0 currentColor;
animation: pulse 1.6s ease-out infinite;
}
.pill.off .dot { animation: none; }
@keyframes pulse {
0% { box-shadow: 0 0 0 0 color-mix(in srgb, currentColor 60%, transparent); }
70% { box-shadow: 0 0 0 6px color-mix(in srgb, currentColor 0%, transparent); }
100% { box-shadow: 0 0 0 0 color-mix(in srgb, currentColor 0%, transparent); }
}
/* ===== Section ===== */
.section { margin-top: 14px; }
.section-h {
display: flex; align-items: center; justify-content: space-between;
font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
color: var(--muted); margin: 0 2px 6px;
}
.section-h .chip {
font-size: 9.5px; letter-spacing: .04em; text-transform: none;
padding: 2px 7px; border-radius: 999px; background: var(--hover);
color: var(--fg);
}
/* ===== Stats grid ===== */
.stats {
display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
}
.stat {
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 10px 8px;
overflow: hidden; min-width: 0;
display: flex; flex-direction: column; gap: 2px;
transition: transform .15s ease, border-color .15s ease;
}
.stat:hover { transform: translateY(-1px); border-color: color-mix(in srgb, var(--accent) 35%, var(--border)); }
.stat .label {
color: var(--muted); font-size: 10.5px;
display: flex; align-items: center; gap: 6px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.stat .label svg { width: 11px; height: 11px; opacity: .85; flex-shrink: 0; }
.stat .val {
font-size: 17px; font-weight: 600; margin-top: 2px; letter-spacing: -.01em;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
font-variant-numeric: tabular-nums;
}
.stat .sub {
color: var(--muted); font-size: 10px; margin-top: 1px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.stat .spark-wrap {
margin-top: 6px; height: 18px; width: 100%;
display: flex; align-items: flex-end;
}
.stat svg.spark { display: block; width: 100%; height: 18px; opacity: .9; }
/* ===== Agent health bar ===== */
.health {
background: var(--card); border: 1px solid var(--border);
border-radius: 10px; padding: 10px;
}
.bar {
display: flex; height: 8px; border-radius: 999px; overflow: hidden;
background: var(--hover);
}
.bar > span { transition: width .4s cubic-bezier(.2,.7,.2,1); }
.legend {
display: grid; grid-template-columns: 1fr 1fr; gap: 4px 12px; margin-top: 8px;
font-size: 10.5px; color: var(--muted);
}
.legend .row { display: flex; align-items: center; gap: 6px; }
.legend .swatch { width: 8px; height: 8px; border-radius: 2px; }
.legend .num { color: var(--fg); font-variant-numeric: tabular-nums; margin-left: auto; font-weight: 600; }
/* ===== Sessions ===== */
.session {
display: flex; align-items: center; gap: 8px;
padding: 7px 8px; border-radius: 8px; cursor: pointer;
border: 1px solid transparent; transition: background .12s ease, border-color .12s ease;
}
.session:hover { background: var(--hover); border-color: var(--border); }
.session .avatar {
width: 22px; height: 22px; border-radius: 6px;
display: grid; place-items: center; flex-shrink: 0;
background: color-mix(in srgb, var(--accent) 22%, transparent);
color: var(--accent);
}
.session .avatar svg { width: 12px; height: 12px; }
.session.completed .avatar { background: color-mix(in srgb, var(--green) 22%, transparent); color: var(--green); }
.session.error .avatar { background: color-mix(in srgb, var(--red) 22%, transparent); color: var(--red); }
.session.active .avatar, .session.working .avatar { background: color-mix(in srgb, var(--yellow) 22%, transparent); color: var(--yellow); }
.session .meta { min-width: 0; flex: 1; }
.session .name { font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.session .sub { color: var(--muted); font-size: 10px; display: flex; gap: 6px; align-items: center; }
.session .badge {
background: var(--hover); padding: 1px 6px; border-radius: 999px; font-size: 9.5px;
}
/* ===== Buttons / nav ===== */
.nav { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.nav button, .actions button {
all: unset; cursor: pointer;
display: flex; align-items: center; gap: 7px; padding: 8px 10px;
border-radius: 8px; background: var(--card); border: 1px solid var(--border);
font-size: 11.5px; transition: transform .12s ease, background .12s ease, border-color .12s ease;
}
.nav button:hover, .actions button:hover {
background: var(--hover);
border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
transform: translateY(-1px);
}
.nav button svg, .actions button svg { width: 13px; height: 13px; opacity: .9; }
.actions { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; }
.actions button.danger:hover {
border-color: color-mix(in srgb, var(--red) 50%, var(--border));
color: var(--red);
}
/* ===== Empty / offline ===== */
.empty {
text-align: center; color: var(--muted); padding: 16px 8px;
border: 1px dashed var(--border); border-radius: 10px;
font-size: 11px;
}
.empty .big { font-size: 13px; color: var(--fg); margin-bottom: 4px; font-weight: 600; }
.empty code {
display: inline-block; margin-top: 6px; padding: 3px 7px; border-radius: 6px;
background: var(--hover); font-family: var(--vscode-editor-font-family, Consolas, monospace);
font-size: 10.5px;
}
/* ===== Skeleton ===== */
.skel {
background: linear-gradient(90deg, var(--hover) 0%, color-mix(in srgb, var(--hover) 60%, transparent) 50%, var(--hover) 100%);
background-size: 200% 100%; animation: shimmer 1.4s linear infinite;
border-radius: 6px;
}
@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
</style>
</head>
<body>
<div class="hero">
<div class="logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M12 8v4"/><path d="M12 16h.01"/>
</svg>
</div>
<div>
<div class="hero-title">Claude Code Monitor</div>
<div class="hero-sub" id="hero-sub">Connecting…</div>
</div>
<div class="pill off" id="pill"><span class="dot"></span><span id="pill-text">Offline</span></div>
</div>
<div id="content">
<div class="empty">
<div class="big">Loading…</div>
Polling backend for live data.
</div>
</div>
<script nonce="${nonce}">
(function(){
try {
const vscode = acquireVsCodeApi();
window.addEventListener('error', (ev) => {
try { vscode.postMessage({ command: 'error', text: (ev && ev.message) + ' @ ' + (ev && ev.filename) + ':' + (ev && ev.lineno) }); } catch(_){}
try { document.getElementById('hero-sub').textContent = 'JS ERROR: ' + (ev && ev.message); } catch(_){}
});
// Synchronous proof-of-life so we can see the script ran:
try { document.getElementById('hero-sub').textContent = 'script ready · awaiting data'; } catch(_){}
vscode.postMessage({ command: 'log', text: 'script booted' });
const $ = (id) => document.getElementById(id);
const fmt = (n) => {
n = Number(n) || 0;
if (n >= 1e9) return (n/1e9).toFixed(2) + 'B';
if (n >= 1e6) return (n/1e6).toFixed(2) + 'M';
if (n >= 1e3) return (n/1e3).toFixed(1) + 'k';
return String(n);
};
const ago = (iso) => {
if (!iso) return '';
const s = (Date.now() - new Date(iso).getTime()) / 1000;
if (s < 60) return Math.max(1, Math.floor(s)) + 's ago';
if (s < 3600) return Math.floor(s/60) + 'm ago';
if (s < 86400) return Math.floor(s/3600) + 'h ago';
return Math.floor(s/86400) + 'd ago';
};
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const sparkline = (values, color) => {
const v = (values || []).map(Number).filter(x => !isNaN(x));
if (v.length < 2) return '<div class="spark-wrap"></div>';
const w = 100, h = 18, pad = 1.5;
const min = Math.min.apply(null, v), max = Math.max.apply(null, v);
const range = (max - min) || 1;
const step = (w - pad*2) / (v.length - 1);
const pts = v.map((x, i) => [pad + i*step, h - pad - ((x - min) / range) * (h - pad*2)]);
const d = pts.map((p, i) => (i ? 'L' : 'M') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' ');
const area = d + ' L ' + (w-pad).toFixed(1) + ' ' + (h-pad) + ' L ' + pad + ' ' + (h-pad) + ' Z';
return '<div class="spark-wrap"><svg class="spark" viewBox="0 0 '+w+' '+h+'" preserveAspectRatio="none"><path d="'+area+'" fill="'+color+'" opacity="0.18"/><path d="'+d+'" fill="none" stroke="'+color+'" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" vector-effect="non-scaling-stroke"/></svg></div>';
};
const ICONS = {
sessions: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 9-9"/><path d="M3 4v5h5"/></svg>',
agents: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 0 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 0 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 0 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 0 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>',
tokens: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z"/><path d="M8 12h8M12 8v8"/></svg>',
cost: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20"/></svg>',
play: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>',
check: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M20 6L9 17l-5-5"/></svg>',
error: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><path d="M15 9l-6 6M9 9l6 6"/></svg>',
pulse: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9-6-18-3 9H2"/></svg>',
dashboard: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/></svg>',
chart: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18"/><path d="M7 14l3-3 4 4 5-7"/></svg>',
kanban: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18M15 3v18"/></svg>',
list: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg>',
settings: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 0 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 0 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 0 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 0 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>',
refresh: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>',
globe: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15 15 0 0 1 0 20a15 15 0 0 1 0-20z"/></svg>',
trash: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>',
};
const STATUS_ORDER = [
['working', 'var(--yellow)'],
['connected', 'var(--accent-2)'],
['idle', 'var(--green)'],
['completed', 'var(--accent)'],
['error', 'var(--red)'],
];
const renderOffline = () => {
$('content').innerHTML = \`
<div class="empty">
<div class="big">Backend offline</div>
Start the dashboard to begin monitoring.
<div><code>npm run dev</code></div>
<div style="margin-top:10px;">
<button class="" onclick="postCmd('refresh')" style="all:unset; cursor:pointer; padding:6px 12px; border-radius:8px; border:1px solid var(--border); background:var(--card);">Retry</button>
</div>
</div>
\`;
};
const renderOnline = (s) => {
const ah = s.stats.agents_by_status || {};
const totalA = STATUS_ORDER.reduce((sum, [k]) => sum + (ah[k] || 0), 0) || 1;
const segs = STATUS_ORDER
.map(([k, c]) => '<span style="background:'+c+';width:'+(((ah[k]||0)/totalA)*100)+'%"></span>')
.join('');
const legend = STATUS_ORDER
.map(([k, c]) => '<div class="row"><span class="swatch" style="background:'+c+'"></span>'+k+'<span class="num">'+(ah[k]||0)+'</span></div>')
.join('');
const sessions = (s.sessions || []).length
? s.sessions.map(x => {
const cls = (x.status === 'completed' || x.status === 'active' || x.status === 'error' || x.status === 'working') ? x.status : '';
const icon = x.status === 'completed' ? ICONS.check : x.status === 'error' ? ICONS.error : ICONS.play;
return \`
<div class="session \${cls}" data-id="\${esc(x.id)}" title="\${esc(x.id)} · \${esc(x.model)}">
<div class="avatar">\${icon}</div>
<div class="meta">
<div class="name">\${esc(x.name)}</div>
<div class="sub"><span>\${esc(ago(x.started_at))}</span><span class="badge">\${esc(x.model)}</span></div>
</div>
</div>\`;
}).join('')
: '<div class="empty">No recent sessions yet.</div>';
$('content').innerHTML = \`
<div class="section">
<div class="section-h">Live Stats <span class="chip">auto · 5s</span></div>
<div class="stats">
<div class="stat">
<div class="label">\${ICONS.sessions} Sessions</div>
<div class="val">\${fmt(s.stats.sessions)}</div>
<div class="sub">\${fmt(s.stats.events)} events</div>
\${sparkline(s.history.sessions, 'var(--accent)')}
</div>
<div class="stat">
<div class="label">\${ICONS.agents} Agents</div>
<div class="val">\${fmt(s.stats.agents_total)}</div>
<div class="sub">\${s.stats.ws} WS live</div>
\${sparkline(s.history.agents, 'var(--accent-2)')}
</div>
<div class="stat">
<div class="label">\${ICONS.tokens} Tokens</div>
<div class="val">\${fmt(s.analytics.tokens.total)}</div>
<div class="sub">in \${fmt(s.analytics.tokens.input)} · out \${fmt(s.analytics.tokens.output)}</div>
\${sparkline(s.history.tokens, 'var(--green)')}
</div>
<div class="stat">
<div class="label">\${ICONS.cost} Cost</div>
<div class="val">$\${(s.analytics.cost || 0).toFixed(2)}</div>
<div class="sub">\${s.analytics.subagents} subagents</div>
\${sparkline(s.history.cost, 'var(--orange)')}
</div>
</div>
</div>
<div class="section">
<div class="section-h">Agent Health <span class="chip">\${totalA} total</span></div>
<div class="health">
<div class="bar">\${segs}</div>
<div class="legend">\${legend}</div>
</div>
</div>
<div class="section">
<div class="section-h">Recent Sessions <span class="chip">\${(s.sessions||[]).length}</span></div>
<div id="sessions">\${sessions}</div>
</div>
<div class="section">
<div class="section-h">Quick Navigation</div>
<div class="nav">
<button data-nav="">\${ICONS.dashboard} Dashboard</button>
<button data-nav="analytics">\${ICONS.chart} Analytics</button>
<button data-nav="kanban">\${ICONS.kanban} Agent Board</button>
<button data-nav="sessions">\${ICONS.list} Sessions</button>
<button data-nav="settings" style="grid-column: span 2;">\${ICONS.settings} System Settings</button>
</div>
</div>
<div class="section">
<div class="section-h">Actions</div>
<div class="actions">
<button data-act="refresh">\${ICONS.refresh} Refresh</button>
<button data-act="openInBrowser">\${ICONS.globe} Browser</button>
<button data-act="clearHistory" class="danger">\${ICONS.trash} Clear</button>
</div>
</div>
\`;
document.querySelectorAll('[data-nav]').forEach(b => b.addEventListener('click', () => {
vscode.postMessage({ command: 'openDashboard', target: b.getAttribute('data-nav') });
}));
document.querySelectorAll('[data-act]').forEach(b => b.addEventListener('click', () => {
vscode.postMessage({ command: b.getAttribute('data-act') });
}));
document.querySelectorAll('.session').forEach(el => el.addEventListener('click', () => {
vscode.postMessage({ command: 'openDashboard', target: el.getAttribute('data-id') });
}));
};
const postCmd = (c, target) => vscode.postMessage({ command: c, target });
window.postCmd = postCmd;
const apply = (s) => {
const isOn = s.status === 'Online';
const pill = $('pill');
pill.classList.toggle('off', !isOn);
$('pill-text').textContent = isOn ? 'Online' : 'Offline';
$('hero-sub').textContent = isOn
? ('localhost:' + (s.port || '4820') + ' · live')
: 'No backend detected on 4820 / 5173';
if (isOn) renderOnline(s); else renderOffline();
};
window.addEventListener('message', (e) => {
const m = e.data;
if (m && m.type === 'snapshot') apply(m.payload);
});
vscode.postMessage({ command: 'ready' });
} catch (e) {
try { document.getElementById('hero-sub').textContent = 'BOOT ERROR: ' + (e && e.message); } catch(_){}
}
})();
</script>
</body>
</html>`;
}
}
function makeNonce() {
let s = "";
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 32; i++) s += chars[Math.floor(Math.random() * chars.length)];
return s;
}
module.exports = { DashboardWebviewProvider };
Binary file not shown.

Before

Width:  |  Height:  |  Size: 988 KiB