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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Claude Code fires a hook event (e.g. "I just used the Read tool")
hook-handler.py receives the event JSON on stdin
|
v
hook-handler.py POSTs it to http://localhost:3000/api/hooks
hook-handler.py POSTs it to http://localhost:4700/api/hooks
|
v
The server updates the SQLite DB and broadcasts via WebSocket
Expand Down Expand Up @@ -54,7 +54,7 @@ Critical design constraint: **it must never block Claude Code.** If the server i

### 3. The Server (`server/`)

A FastAPI app running on port 3000. It does four things:
A FastAPI app running on port 4700. It does four things:

| Component | File | What It Does |
|-----------|------|-------------|
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Web-based dashboard for monitoring and managing multiple Claude Code sessions.
## Running
```bash
source .venv/bin/activate
uvicorn server.main:app --port 3000 --reload
uvicorn server.main:app --port 4700 --reload
```

## Testing
Expand Down
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.DEFAULT_GOAL := help
PORT := 3000
PORT := 4700
# Prefer project .venv via uv so `make up` works without `source .venv/bin/activate`
UV_RUN := uv run

Expand All @@ -11,12 +11,13 @@ help: ## Show this help
setup install: ## Full local setup: uv venv + dev deps, Playwright Chromium, Claude hooks
uv sync --extra dev
uv run python -m playwright install chromium
bash scripts/setup.sh
bash scripts/setup.sh $(PORT)
@echo ""
@echo "Setup complete. Start the app: make up"
@echo "If Playwright/e2e fails (missing OS libs), run: uv run python -m playwright install --with-deps chromium"

up: ## Start the server (port 3000; override with PORT=3001)
up: ## Start the server (default 4700; override with PORT=XXXX)
@bash scripts/setup.sh $(PORT)
$(UV_RUN) uvicorn server.main:app --port $(PORT) --reload

down: ## Stop the server
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ pip install .
bash scripts/setup.sh

# Start the server
uvicorn server.main:app --port 3000
uvicorn server.main:app --port 4700
```

Then open **http://localhost:3000** in your browser.
Then open **http://localhost:4700** in your browser.

## Architecture

```
Browser (localhost:3000)
Browser (localhost:4700)
|
|-- REST API (/api/*) -- Sessions, history, search, analytics
|-- WebSocket (/ws/*) -- Real-time dashboard updates
Expand Down Expand Up @@ -122,7 +122,7 @@ source .venv/bin/activate
pytest

# Run with auto-reload
uvicorn server.main:app --port 3000 --reload
uvicorn server.main:app --port 4700 --reload
```

## Uninstalling
Expand Down
20 changes: 17 additions & 3 deletions public/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1638,14 +1638,28 @@ body {

.history-table tbody tr:hover { background: var(--surface-3); }

/* ---- Transcript Detail View ---- */
.transcript-container { max-width: 800px; margin: 0 auto; }
/* ---- Transcript Detail View (History tab) ---- */
#transcript-view {
display: flex;
flex-direction: column;
height: calc(100vh - 120px);
}

#transcript-view .transcript-header {
flex-shrink: 0;
}

#transcript-view .transcript-live {
flex: 1;
min-height: 0;
}

.transcript-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
margin-bottom: 12px;
padding: 0 4px;
}

.transcript-message {
Expand Down
4 changes: 0 additions & 4 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
<title>Claude Code Command Center</title>
<link rel="icon" type="image/svg+xml" href="/logo.svg">
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
</head>
<body>
<div id="app">
Expand Down Expand Up @@ -124,9 +123,6 @@ <h2>Settings</h2>
</div>

<script src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.js"></script>
<script src="/js/app.js"></script>
<script src="/js/dashboard.js"></script>
<script src="/js/terminal.js"></script>
Expand Down
8 changes: 7 additions & 1 deletion public/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,15 @@ const App = {
if (state.view === 'session' && state.sessionId) {
// Reopen session transcript without pushing another state
this._openSessionDirect(state.sessionId, state.sessionTitle);
} else if (state.view === 'history' && state.transcript && state.sessionId) {
// Reopen history transcript without pushing another state
if (typeof History !== 'undefined') {
History.showTranscriptDirect(state.sessionId, state.sessionTitle);
}
} else {
// Close any open transcript and switch view
// Close any open transcript/overlay and switch view
if (typeof SessionViewer !== 'undefined') SessionViewer.close();
if (typeof History !== 'undefined') History.closeTranscript();
this._switchViewDirect(state.view || 'dashboard');
}
});
Expand Down
59 changes: 34 additions & 25 deletions public/js/history.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,12 @@ const History = {
<div class="pagination" id="history-pagination"></div>
</div>
<div id="transcript-view" style="display:none">
<div class="transcript-container">
<div class="transcript-header">
<button class="btn btn-sm" id="transcript-back">&larr; Back to History</button>
<h2 id="transcript-title"></h2>
</div>
<div id="transcript-messages"></div>
<div class="pagination" id="transcript-pagination"></div>
<div class="transcript-header">
<button class="btn btn-sm" id="transcript-back">&larr; Back to History</button>
<h2 id="transcript-title"></h2>
</div>
<div class="transcript-live">
<div class="transcript-live-messages" id="transcript-messages"></div>
</div>
</div>
`;
Expand All @@ -73,11 +72,18 @@ const History = {
});

document.getElementById('transcript-back').addEventListener('click', () => {
document.getElementById('transcript-view').style.display = 'none';
document.getElementById('history-content').style.display = 'block';
this.closeTranscript();
window.history.back();
});
},

closeTranscript() {
const tv = document.getElementById('transcript-view');
const hc = document.getElementById('history-content');
if (tv) tv.style.display = 'none';
if (hc) hc.style.display = 'block';
},

async fetchSessions() {
try {
const resp = await fetch(`/api/history?limit=${this.pageSize}&offset=${this.currentOffset}`);
Expand Down Expand Up @@ -179,13 +185,26 @@ const History = {
}
},

async showTranscript(sessionId, title) {
showTranscriptDirect(sessionId, title) {
// Open transcript without pushing browser state (used by popstate handler)
this._openTranscript(sessionId, title);
},

showTranscript(sessionId, title) {
this._openTranscript(sessionId, title);
window.history.pushState(
{ view: 'history', transcript: true, sessionId, sessionTitle: title },
'', `#history/${sessionId}`
);
},

async _openTranscript(sessionId, title) {
document.getElementById('history-content').style.display = 'none';
document.getElementById('transcript-view').style.display = 'block';
document.getElementById('transcript-title').textContent = title || sessionId;

try {
const resp = await fetch(`/api/sessions/${sessionId}/transcript`);
const resp = await fetch(`/api/sessions/${sessionId}/transcript?limit=1000`);
const data = await resp.json();
this.renderTranscript(data.transcripts);
} catch (e) {
Expand All @@ -202,20 +221,10 @@ const History = {
return;
}

container.innerHTML = transcripts.map(t => {
const role = t.role || 'unknown';
const isCollapsible = role === 'tool_use' || role === 'tool_result';
const time = t.timestamp ? this._formatTime(t.timestamp) : '';

return `
<div class="transcript-message ${role}${isCollapsible ? '' : ''}" ${isCollapsible ? 'onclick="this.classList.toggle(\'expanded\')"' : ''}>
<span class="transcript-role">${this._esc(role)}</span>
<span class="transcript-time">${time}</span>
<button class="transcript-copy" onclick="event.stopPropagation();navigator.clipboard.writeText(this.parentElement.querySelector('.transcript-text').textContent)">Copy</button>
<div class="transcript-text">${this._esc(t.content || '')}</div>
</div>
`;
}).join('');
// Use the same rich renderer as the live transcript view
const planGroups = SessionViewer._detectPlanGroups(transcripts);
const agentGroups = SessionViewer._detectAgentGroups(transcripts);
container.innerHTML = SessionViewer._renderTranscriptWithGroups(transcripts, planGroups, agentGroups);
},

_formatDate(dateStr) {
Expand Down
Loading
Loading