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
40 changes: 40 additions & 0 deletions public/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,46 @@ body {
.preview-role.assistant { background: rgba(124, 58, 237, 0.15); color: #c4b5fd; }
.preview-role.tool_result { background: rgba(245, 158, 11, 0.15); color: #fcd34d; }

/* Expanded view toggle */
#expanded-view-btn.active {
background: var(--accent-muted);
border-color: var(--accent);
color: var(--accent-hover);
}

.card-expanded-preview {
margin: 6px 0 8px;
padding: 6px 8px;
background: var(--surface-2);
border-radius: var(--radius);
border-left: 2px solid var(--accent);
font-size: 11px;
max-height: 200px;
overflow-y: auto;
}

.card-expanded-preview .preview-msg {
padding: 3px 0;
color: var(--text-secondary);
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.card-expanded-preview .preview-msg + .preview-msg {
border-top: 1px solid var(--border);
}

.card-expanded-preview .expanded-loading {
color: var(--text-muted);
font-style: italic;
}

.session-grid.expanded-view {
grid-template-columns: repeat(auto-fill, minmax(440px, 1fr));
}

.card-task {
font-size: 12px;
color: var(--text-secondary);
Expand Down
12 changes: 12 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ <h1 class="topbar-title">Claude Code Command Center</h1>
</div>
</div>
<div class="topbar-actions">
<button class="btn btn-sm" id="expanded-view-btn" title="Toggle expanded view">Expanded</button>
<button class="btn btn-sm" id="settings-btn" title="Settings">&#9881; Settings</button>
</div>
</header>
Expand Down Expand Up @@ -92,6 +93,7 @@ <h2>Settings</h2>
<div class="settings-body">
<div class="settings-sidebar">
<button class="settings-tab active" data-tab="integrations">&#128279; Integrations</button>
<button class="settings-tab" data-tab="summaries">&#9881; AI Summaries</button>
</div>
<div class="settings-content">
<div class="settings-panel active" id="tab-integrations">
Expand All @@ -109,6 +111,16 @@ <h2>Settings</h2>
</div>
</div>
</div>
<div class="settings-panel" id="tab-summaries">
<div class="settings-section">
<div class="section-title">AI Session Summaries</div>
<div class="form-group">
<label for="summary-interval">Update title every N user messages</label>
<input type="number" class="input" id="summary-interval" min="1" max="100" value="5" />
<small class="form-hint">Runs <code>claude -p</code> in the background to generate an evolving session title. Set to a higher number to reduce frequency. Requires the <code>claude</code> CLI to be available.</small>
</div>
</div>
</div>
</div>
</div>
<div class="settings-footer">
Expand Down
4 changes: 4 additions & 0 deletions public/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const App = {
this.loadInitialData();
this.loadSettings();
this.setupHistory();
Dashboard.initExpandedView();
},

// ---- Browser History (back/forward) ----
Expand Down Expand Up @@ -105,6 +106,7 @@ const App = {
const keys = this.settings.jira_project_keys || [];
document.getElementById('jira-keys').value = keys.join(', ');
document.getElementById('jira-url').value = this.settings.jira_server_url || '';
document.getElementById('summary-interval').value = this.settings.summary_interval || 5;
indicator.textContent = '';
indicator.className = 'save-indicator';
modal.style.display = 'flex';
Expand All @@ -131,13 +133,15 @@ const App = {
const keysRaw = document.getElementById('jira-keys').value;
const keys = keysRaw.split(',').map(k => k.trim().toUpperCase()).filter(Boolean);
const url = document.getElementById('jira-url').value.trim();
const summaryInterval = parseInt(document.getElementById('summary-interval').value, 10) || 5;
try {
const resp = await fetch('/api/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jira_project_keys: keys,
jira_server_url: url || null,
summary_interval: summaryInterval,
}),
});
const data = await resp.json();
Expand Down
88 changes: 79 additions & 9 deletions public/js/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
const Dashboard = {
_expandedSubagentSections: new Set(),
_expandedSubagentTranscripts: new Set(),
_expandedView: false,

_emptyHTML: `<div class="empty-state" id="empty-state">
<div class="empty-icon">&#9678;</div>
Expand All @@ -30,6 +31,10 @@ const Dashboard = {
list.forEach(session => {
grid.appendChild(this.createCard(session));
});

if (this._expandedView) {
this._loadExpandedPreviews(list);
}
},

createCard(session) {
Expand Down Expand Up @@ -61,13 +66,19 @@ const Dashboard = {
this._loadSubagentTranscript(agentId, container);
}
});
if (this._expandedView) {
this._fetchExpandedPreview(session.id);
}
} else {
// New session — add card
const grid = document.getElementById('session-grid');
const empty = document.getElementById('empty-state');
if (empty) empty.remove();
const card = this.createCard(session);
grid.prepend(card);
if (this._expandedView) {
this._fetchExpandedPreview(session.id);
}
}

// Remove completed sessions from active view after a delay
Expand Down Expand Up @@ -125,14 +136,21 @@ const Dashboard = {
const lockIcon = isLocked ? '&#128274;' : '&#128275;';
const lockTitle = isLocked ? 'Title locked (click to unlock)' : 'Title auto-updates (click to lock)';

const previewText = s.last_activity_preview || '';
const previewLine = previewText
? `<div class="card-preview" onclick="event.stopPropagation(); Dashboard.togglePreview('${this._escapeHTML(s.id)}', this)">
<span class="preview-chevron">&#9656;</span>
<span class="preview-text">${this._escapeHTML(previewText)}</span>
</div>
<div class="card-preview-expanded" id="preview-${s.id}" style="display:none"></div>`
: '';
let previewLine = '';
if (this._expandedView) {
previewLine = `<div class="card-expanded-preview" id="expanded-preview-${s.id}">
<div class="expanded-loading">Loading...</div>
</div>`;
} else {
const previewText = s.last_activity_preview || '';
previewLine = previewText
? `<div class="card-preview" onclick="event.stopPropagation(); Dashboard.togglePreview('${this._escapeHTML(s.id)}', this)">
<span class="preview-chevron">&#9656;</span>
<span class="preview-text">${this._escapeHTML(previewText)}</span>
</div>
<div class="card-preview-expanded" id="preview-${s.id}" style="display:none"></div>`
: '';
}

return `
<div class="card-header">
Expand Down Expand Up @@ -229,7 +247,7 @@ const Dashboard = {
fetch(`/api/sessions/${sessionId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ display_name: val.trim() || '' }),
body: JSON.stringify(val.trim() ? { display_name: val.trim(), display_name_locked: true } : { display_name: '' }),
})
.then(r => r.json())
.then(data => {
Expand Down Expand Up @@ -377,6 +395,58 @@ const Dashboard = {
if (chevron) chevron.style.transform = 'rotate(90deg)';
},

initExpandedView() {
const stored = localStorage.getItem('cccc_expanded_view');
this._expandedView = stored === 'true';
const btn = document.getElementById('expanded-view-btn');
if (btn) {
this._updateExpandedBtn(btn);
btn.addEventListener('click', () => this.toggleExpandedView());
}
const grid = document.getElementById('session-grid');
if (grid && this._expandedView) grid.classList.add('expanded-view');
},

toggleExpandedView() {
this._expandedView = !this._expandedView;
localStorage.setItem('cccc_expanded_view', String(this._expandedView));
const btn = document.getElementById('expanded-view-btn');
if (btn) this._updateExpandedBtn(btn);
const grid = document.getElementById('session-grid');
if (grid) grid.classList.toggle('expanded-view', this._expandedView);
this.render(App.sessions);
},

_updateExpandedBtn(btn) {
btn.classList.toggle('active', this._expandedView);
btn.textContent = this._expandedView ? 'Compact' : 'Expanded';
},

_loadExpandedPreviews(sessions) {
sessions.forEach(s => this._fetchExpandedPreview(s.id));
},

async _fetchExpandedPreview(sessionId) {
const container = document.getElementById(`expanded-preview-${sessionId}`);
if (!container) return;
try {
const resp = await fetch(`/api/sessions/${sessionId}/transcript?limit=5`);
const data = await resp.json();
const msgs = data.transcripts || [];
if (msgs.length === 0) {
container.innerHTML = '<div class="expanded-loading">No activity yet</div>';
return;
}
container.innerHTML = msgs.map(t => {
const roleLabel = t.role === 'user' ? 'U' : t.role === 'assistant' ? 'A' : 'T';
const text = (t.content || '').substring(0, 200);
return `<div class="preview-msg"><span class="preview-role ${this._escapeHTML(t.role)}">${roleLabel}</span> ${this._escapeHTML(text)}</div>`;
}).join('');
} catch (e) {
container.innerHTML = '<div class="expanded-loading">Failed to load</div>';
}
},

async _loadSubagentTranscript(agentId, container) {
container.innerHTML = '<div class="subagent-loading">Loading...</div>';
try {
Expand Down
18 changes: 18 additions & 0 deletions server/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,24 @@ async def get_session_transcripts(
return [dict(r) for r in rows]


async def get_recent_conversation(session_id: str, limit: int = 5) -> list[dict]:
"""Get the most recent user+assistant messages for a session, in chronological order.

Skips tool_result entries so the result captures intent and decisions,
not noisy tool output.
"""
conn = await get_db()
cursor = await conn.execute(
"""SELECT role, content FROM transcripts
WHERE session_id = ? AND role IN ('user', 'assistant')
ORDER BY id DESC
LIMIT ?""",
(session_id, limit),
)
rows = await cursor.fetchall()
return [dict(r) for r in list(rows)[::-1]]


async def search_transcripts(query: str, limit: int = 50) -> list[dict]:
"""Full-text search across transcripts."""
db = await get_db()
Expand Down
5 changes: 5 additions & 0 deletions server/routes/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class NewSessionRequest(BaseModel):
class SettingsUpdate(BaseModel):
jira_project_keys: list[str] | None = None
jira_server_url: str | None = None
summary_interval: int | None = None


_UNSET = object()
Expand Down Expand Up @@ -158,6 +159,10 @@ async def update_settings(req: SettingsUpdate):
await db.set_setting("jira_project_keys", json.dumps(req.jira_project_keys))
if req.jira_server_url is not None:
await db.set_setting("jira_server_url", json.dumps(req.jira_server_url))
if req.summary_interval is not None:
if req.summary_interval < 1:
raise HTTPException(status_code=400, detail="summary_interval must be >= 1")
await db.set_setting("summary_interval", json.dumps(req.summary_interval))
return await get_settings()


Expand Down
Loading
Loading