diff --git a/.gitignore b/.gitignore
index aafcb34..4392d8a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
node_modules/
.DS_Store
*.log
+.superpowers/
+docs/superpowers/
diff --git a/src/data.js b/src/data.js
index 28d8a5d..c50eaef 100644
--- a/src/data.js
+++ b/src/data.js
@@ -21,14 +21,14 @@ function scanOpenCodeSessions() {
try {
// Use sqlite3 CLI to avoid Node version dependency
const rows = execSync(
- `sqlite3 "${OPENCODE_DB}" "SELECT s.id, s.title, s.directory, s.time_created, s.time_updated, COUNT(m.id) as msg_count FROM session s LEFT JOIN message m ON m.session_id = s.id GROUP BY s.id ORDER BY s.time_updated DESC"`,
+ `sqlite3 -separator $'\\t' "${OPENCODE_DB}" "SELECT s.id, s.title, s.directory, s.time_created, s.time_updated, COUNT(m.id) as msg_count FROM session s LEFT JOIN message m ON m.session_id = s.id GROUP BY s.id ORDER BY s.time_updated DESC"`,
{ encoding: 'utf8', timeout: 5000 }
).trim();
if (!rows) return sessions;
for (const row of rows.split('\n')) {
- const parts = row.split('|');
+ const parts = row.split('\t');
if (parts.length < 6) continue;
const [id, title, directory, timeCreated, timeUpdated, msgCount] = parts;
@@ -128,14 +128,14 @@ function scanKiroSessions() {
try {
const rows = execSync(
- `sqlite3 "${KIRO_DB}" "SELECT key, conversation_id, created_at, updated_at, substr(value, 1, 500) FROM conversations_v2 ORDER BY updated_at DESC"`,
+ `sqlite3 -separator $'\\t' "${KIRO_DB}" "SELECT key, conversation_id, created_at, updated_at, substr(value, 1, 500) FROM conversations_v2 ORDER BY updated_at DESC"`,
{ encoding: 'utf8', timeout: 5000 }
).trim();
if (!rows) return sessions;
for (const row of rows.split('\n')) {
- const parts = row.split('|');
+ const parts = row.split('\t');
if (parts.length < 5) continue;
const [directory, convId, createdAt, updatedAt, valuePeek] = parts;
@@ -262,6 +262,26 @@ function scanCodexSessions() {
}
} catch {}
+ // Extract MCP/Skill from Codex session file
+ const mcpSet = new Set();
+ const skillSet = new Set();
+ try {
+ const sLines = fs.readFileSync(f, 'utf8').split('\n').filter(Boolean);
+ for (const sl of sLines) {
+ try {
+ const entry = JSON.parse(sl);
+ if (entry.type === 'response_item' && entry.payload) {
+ const pType = entry.payload.type;
+ const pName = entry.payload.name || '';
+ if (pType === 'function_call' && pName.startsWith('mcp__')) {
+ const parts = pName.split('__');
+ if (parts.length >= 3) mcpSet.add(parts[1]);
+ }
+ }
+ } catch {}
+ }
+ } catch {}
+
const existing = sessions.find(s => s.id === sid);
if (existing) {
existing.has_detail = true;
@@ -270,6 +290,8 @@ function scanCodexSessions() {
existing.project = cwd;
existing.project_short = cwd.replace(os.homedir(), '~');
}
+ if (mcpSet.size > 0) existing.mcp_servers = Array.from(mcpSet);
+ if (skillSet.size > 0) existing.skills = Array.from(skillSet);
} else {
sessions.push({
id: sid,
@@ -283,6 +305,8 @@ function scanCodexSessions() {
has_detail: true,
file_size: stat.size,
detail_messages: 0,
+ mcp_servers: Array.from(mcpSet),
+ skills: Array.from(skillSet),
});
}
}
@@ -294,7 +318,14 @@ function scanCodexSessions() {
// ── Public API ─────────────────────────────────────────────
-function loadSessions() {
+let sessionsCache = null;
+let sessionsCacheAt = 0;
+const SESSIONS_TTL = 30000; // 30 seconds
+
+function loadSessions(forceRefresh) {
+ if (!forceRefresh && sessionsCache && (Date.now() - sessionsCacheAt) < SESSIONS_TTL) {
+ return sessionsCache;
+ }
const sessions = {};
// Load Claude Code sessions
@@ -367,22 +398,50 @@ function loadSessions() {
s.file_size = fs.statSync(sessionFile).size;
try {
let msgCount = 0;
+ const mcpSet = new Set();
+ const skillSet = new Set();
const sLines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean);
for (const sl of sLines) {
try {
const entry = JSON.parse(sl);
if (entry.type === 'user' || entry.type === 'assistant') msgCount++;
+ if (entry.type === 'assistant') {
+ const content = (entry.message || {}).content;
+ if (Array.isArray(content)) {
+ for (const block of content) {
+ if (block.type !== 'tool_use') continue;
+ const name = block.name || '';
+ if (name.startsWith('mcp__')) {
+ const parts = name.split('__');
+ if (parts.length >= 3) mcpSet.add(parts[1]);
+ } else if (name === 'Skill') {
+ const skill = (block.input || {}).skill;
+ if (skill) skillSet.add(skill.includes(':') ? skill.split(':')[0] : skill);
+ }
+ }
+ }
+ }
} catch {}
}
s.detail_messages = msgCount;
- } catch { s.detail_messages = 0; }
+ s.mcp_servers = Array.from(mcpSet);
+ s.skills = Array.from(skillSet);
+ } catch { s.detail_messages = 0; s.mcp_servers = []; s.skills = []; }
} else {
s.has_detail = false;
s.file_size = 0;
s.detail_messages = 0;
+ s.mcp_servers = [];
+ s.skills = [];
}
}
+ // Ensure all sessions have mcp_servers/skills (non-Claude sessions skip enrichment loop)
+ for (const s of Object.values(sessions)) {
+ if (!s.mcp_servers) s.mcp_servers = [];
+ if (!s.skills) s.skills = [];
+ }
+
const result = Object.values(sessions).sort((a, b) => b.last_ts - a.last_ts);
for (const s of result) {
@@ -392,6 +451,8 @@ function loadSessions() {
s.date = dt.getFullYear() + '-' + String(dt.getMonth()+1).padStart(2,'0') + '-' + String(dt.getDate()).padStart(2,'0');
}
+ sessionsCache = result;
+ sessionsCacheAt = Date.now();
return result;
}
@@ -420,11 +481,20 @@ function loadSessionDetail(sessionId, project) {
if (entry.type === 'user' || entry.type === 'assistant') {
const content = extractContent((entry.message || {}).content);
if (content) {
- messages.push({ role: entry.type, content: content.slice(0, 2000), uuid: entry.uuid || '' });
+ const msg = { role: entry.type, content: content.slice(0, 2000), uuid: entry.uuid || '' };
+ if (entry.type === 'assistant') {
+ const rawContent = (entry.message || {}).content;
+ if (Array.isArray(rawContent)) {
+ const tools = extractTools(rawContent);
+ if (tools.length > 0) msg.tools = tools;
+ }
+ }
+ messages.push(msg);
}
}
} else {
if (entry.type === 'response_item' && entry.payload) {
+ const pType = entry.payload.type;
const role = entry.payload.role;
if (role === 'user' || role === 'assistant') {
const content = extractContent(entry.payload.content);
@@ -432,11 +502,36 @@ function loadSessionDetail(sessionId, project) {
messages.push({ role: role, content: content.slice(0, 2000), uuid: '' });
}
}
+ // Collect Codex function_call tools for the last assistant message
+ if (pType === 'function_call') {
+ const pName = entry.payload.name || '';
+ const lastMsg = messages.length > 0 ? messages[messages.length - 1] : null;
+ if (lastMsg && lastMsg.role === 'assistant') {
+ if (!lastMsg.tools) lastMsg.tools = [];
+ if (pName.startsWith('mcp__')) {
+ const parts = pName.split('__');
+ if (parts.length >= 3) {
+ const key = 'mcp:' + parts[1] + ':' + parts.slice(2).join('__');
+ if (!lastMsg._toolSeen) lastMsg._toolSeen = new Set();
+ if (!lastMsg._toolSeen.has(key)) {
+ lastMsg._toolSeen.add(key);
+ lastMsg.tools.push({ type: 'mcp', server: parts[1], tool: parts.slice(2).join('__') });
+ }
+ }
+ }
+ }
+ }
}
}
} catch {}
}
+ // Clean up internal dedup markers
+ for (const m of messages) {
+ if (m._toolSeen) delete m._toolSeen;
+ if (m.tools && m.tools.length === 0) delete m.tools;
+ }
+
return { messages: messages.slice(0, 200) };
}
@@ -617,6 +712,32 @@ function isSystemMessage(text) {
return false;
}
+function extractTools(contentBlocks) {
+ const tools = [];
+ const seen = new Set();
+ for (const block of contentBlocks) {
+ if (block.type !== 'tool_use') continue;
+ const name = block.name || '';
+ if (name.startsWith('mcp__')) {
+ const parts = name.split('__');
+ if (parts.length >= 3) {
+ const key = 'mcp:' + parts[1] + ':' + parts.slice(2).join('__');
+ if (!seen.has(key)) {
+ seen.add(key);
+ tools.push({ type: 'mcp', server: parts[1], tool: parts.slice(2).join('__') });
+ }
+ }
+ } else if (name === 'Skill') {
+ const skill = (block.input || {}).skill;
+ if (skill && !seen.has('skill:' + skill)) {
+ seen.add('skill:' + skill);
+ tools.push({ type: 'skill', skill: skill.includes(':') ? skill.split(':')[0] : skill });
+ }
+ }
+ }
+ return tools;
+}
+
function extractContent(raw) {
if (!raw) return '';
if (typeof raw === 'string') return raw;
@@ -864,10 +985,25 @@ function getModelPricing(model) {
// ── Compute real cost from session file token usage ────────
-function computeSessionCost(sessionId, project) {
+function computeSessionCost(sessionId, project, maxFileSize) {
const found = findSessionFile(sessionId, project);
if (!found) return { cost: 0, inputTokens: 0, outputTokens: 0, model: '' };
+ // Skip very large files in bulk operations (analytics)
+ if (maxFileSize) {
+ try {
+ const stat = fs.statSync(found.file);
+ if (stat.size > maxFileSize) {
+ // Estimate from file size instead
+ const tokens = stat.size / 4;
+ const pricing = getModelPricing('');
+ const inp = Math.round(tokens * 0.3);
+ const out = Math.round(tokens * 0.7);
+ return { cost: inp * pricing.input + out * pricing.output, inputTokens: inp, outputTokens: out, model: 'estimated' };
+ }
+ } catch {}
+ }
+
let totalCost = 0;
let totalInput = 0;
let totalOutput = 0;
@@ -919,7 +1055,13 @@ function computeSessionCost(sessionId, project) {
// ── Cost analytics ────────────────────────────────────────
+let analyticsCache = null;
+let analyticsCacheAt = 0;
+const ANALYTICS_TTL = 120000; // 2 minutes
+
function getCostAnalytics(sessions) {
+ const now = Date.now();
+ if (analyticsCache && (now - analyticsCacheAt) < ANALYTICS_TTL) return analyticsCache;
const byDay = {};
const byProject = {};
const byWeek = {};
@@ -928,7 +1070,7 @@ function getCostAnalytics(sessions) {
const sessionCosts = [];
for (const s of sessions) {
- const costData = computeSessionCost(s.id, s.project);
+ const costData = computeSessionCost(s.id, s.project, 10 * 1024 * 1024);
const cost = costData.cost;
const tokens = costData.inputTokens + costData.outputTokens;
if (cost === 0 && tokens === 0) continue;
@@ -966,7 +1108,7 @@ function getCostAnalytics(sessions) {
// Sort top sessions by cost
sessionCosts.sort((a, b) => b.cost - a.cost);
- return {
+ analyticsCache = {
totalCost,
totalTokens,
totalSessions: sessions.length,
@@ -975,6 +1117,8 @@ function getCostAnalytics(sessions) {
byProject,
topSessions: sessionCosts.slice(0, 10),
};
+ analyticsCacheAt = Date.now();
+ return analyticsCache;
}
// ── Active sessions detection ─────────────────────────────
diff --git a/src/frontend/app.js b/src/frontend/app.js
index be84c84..a1025d7 100644
--- a/src/frontend/app.js
+++ b/src/frontend/app.js
@@ -160,9 +160,11 @@ function toggleStar(id) {
// ── Data loading ───────────────────────────────────────────────
-async function loadSessions() {
+async function loadSessions(refresh) {
try {
- var resp = await fetch('/api/sessions');
+ var url = '/api/sessions';
+ if (refresh) url += '?refresh=1';
+ var resp = await fetch(url);
allSessions = await resp.json();
applyFilters();
} catch (e) {
@@ -171,10 +173,16 @@ async function loadSessions() {
}
function refreshData() {
- loadSessions();
+ loadSessions(true);
showToast('Refreshed');
}
+function clearSearch() {
+ var box = document.getElementById('searchBox');
+ if (box) box.value = '';
+ onSearch('');
+}
+
async function loadTerminals() {
try {
var resp = await fetch('/api/terminals');
@@ -378,6 +386,8 @@ function applyFilters() {
function onSearch(val) {
searchQuery = val;
+ var clearBtn = document.getElementById('searchClear');
+ if (clearBtn) clearBtn.style.display = val ? 'block' : 'none';
applyFilters();
// Trigger deep search after debounce
@@ -393,9 +403,177 @@ function onTagFilter(val) {
}
function onDateFilter() {
- dateFrom = document.getElementById('dateFrom').value || '';
- dateTo = document.getElementById('dateTo').value || '';
applyFilters();
+ updateDateBtn();
+}
+
+// ── Calendar ─────────────────────────────────────────────────
+
+var calYear = new Date().getFullYear();
+var calMonth = new Date().getMonth();
+var calStart = null;
+var calEnd = null;
+var calSelecting = false;
+
+function toggleCalendar() {
+ var popup = document.getElementById('calendarPopup');
+ if (!popup) return;
+ if (popup.classList.contains('open')) {
+ popup.classList.remove('open');
+ return;
+ }
+ renderCalendar();
+ popup.classList.add('open');
+ // Close on outside click
+ setTimeout(function() {
+ document.addEventListener('click', closeCalendarOutside, { once: true });
+ }, 0);
+}
+
+function closeCalendarOutside(e) {
+ var popup = document.getElementById('calendarPopup');
+ var btn = document.getElementById('dateBtn');
+ if (popup && !popup.contains(e.target) && btn && !btn.contains(e.target)) {
+ popup.classList.remove('open');
+ } else if (popup && popup.classList.contains('open')) {
+ document.addEventListener('click', closeCalendarOutside, { once: true });
+ }
+}
+
+function renderCalendar() {
+ var popup = document.getElementById('calendarPopup');
+ if (!popup) return;
+
+ var monthNames = ['January','February','March','April','May','June','July','August','September','October','November','December'];
+ var firstDay = new Date(calYear, calMonth, 1);
+ var lastDay = new Date(calYear, calMonth + 1, 0);
+ var startWeekday = (firstDay.getDay() + 6) % 7; // Monday=0
+ var daysInMonth = lastDay.getDate();
+ var today = new Date();
+ var todayStr = today.getFullYear() + '-' + String(today.getMonth()+1).padStart(2,'0') + '-' + String(today.getDate()).padStart(2,'0');
+
+ var html = '
';
+
+ html += 'Mo Tu We Th Fr Sa Su
';
+ html += '';
+
+ // Previous month padding
+ var prevLastDay = new Date(calYear, calMonth, 0).getDate();
+ for (var i = startWeekday - 1; i >= 0; i--) {
+ html += '
' + (prevLastDay - i) + '
';
+ }
+
+ // Current month days
+ for (var d = 1; d <= daysInMonth; d++) {
+ var dateStr = calYear + '-' + String(calMonth+1).padStart(2,'0') + '-' + String(d).padStart(2,'0');
+ var cls = 'cal-day';
+ if (dateStr === todayStr) cls += ' today';
+ if (calStart && calEnd) {
+ if (dateStr === calStart) cls += ' range-start';
+ if (dateStr === calEnd) cls += ' range-end';
+ if (dateStr > calStart && dateStr < calEnd) cls += ' in-range';
+ if (calStart === calEnd && dateStr === calStart) cls += ' range-start range-end';
+ } else if (calStart && dateStr === calStart) {
+ cls += ' range-start range-end';
+ }
+ html += '
' + d + '
';
+ }
+
+ // Next month padding
+ var totalCells = startWeekday + daysInMonth;
+ var remaining = (7 - (totalCells % 7)) % 7;
+ for (var n = 1; n <= remaining; n++) {
+ html += '
' + n + '
';
+ }
+ html += '
';
+
+ // Presets
+ html += '';
+ var presets = [['All',''],['Today','0'],['7d','7'],['30d','30'],['90d','90']];
+ presets.forEach(function(p) {
+ html += '' + p[0] + ' ';
+ });
+ html += '
';
+
+ popup.innerHTML = html;
+}
+
+function calNav(dir) {
+ calMonth += dir;
+ if (calMonth < 0) { calMonth = 11; calYear--; }
+ if (calMonth > 11) { calMonth = 0; calYear++; }
+ renderCalendar();
+}
+
+function calPickDay(dateStr) {
+ if (!calSelecting) {
+ // First click — set start
+ calStart = dateStr;
+ calEnd = null;
+ calSelecting = true;
+ } else {
+ // Second click — set end
+ if (dateStr < calStart) {
+ calEnd = calStart;
+ calStart = dateStr;
+ } else {
+ calEnd = dateStr;
+ }
+ calSelecting = false;
+ }
+ renderCalendar();
+ // Apply filter
+ dateFrom = calStart || '';
+ dateTo = calEnd || calStart || '';
+ onDateFilter();
+}
+
+function calPreset(days) {
+ calSelecting = false;
+ if (!days) {
+ calStart = null;
+ calEnd = null;
+ dateFrom = '';
+ dateTo = '';
+ } else {
+ var now = new Date();
+ calEnd = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0');
+ if (days === '0') {
+ calStart = calEnd;
+ } else {
+ var from = new Date(now.getTime() - parseInt(days) * 86400000);
+ calStart = from.getFullYear() + '-' + String(from.getMonth()+1).padStart(2,'0') + '-' + String(from.getDate()).padStart(2,'0');
+ }
+ dateFrom = calStart;
+ dateTo = calEnd;
+ }
+ renderCalendar();
+ onDateFilter();
+ // Close popup
+ var popup = document.getElementById('calendarPopup');
+ if (popup) popup.classList.remove('open');
+}
+
+function updateDateBtn() {
+ var btn = document.getElementById('dateBtn');
+ var label = document.getElementById('dateBtnLabel');
+ if (!btn || !label) return;
+ if (!dateFrom && !dateTo) {
+ label.textContent = 'All time';
+ btn.classList.remove('has-filter');
+ } else if (dateFrom === dateTo) {
+ label.textContent = dateFrom;
+ btn.classList.add('has-filter');
+ } else {
+ var f = dateFrom.slice(5) || '';
+ var t = dateTo.slice(5) || '';
+ label.textContent = f + ' \u2014 ' + t;
+ btn.classList.add('has-filter');
+ }
}
function toggleGroup() {
@@ -489,6 +667,21 @@ function renderCard(s, idx) {
html += '▾ ';
}
html += '';
+ // MCP/Skills footer
+ if ((s.mcp_servers && s.mcp_servers.length > 0) || (s.skills && s.skills.length > 0)) {
+ html += '';
+ if (s.mcp_servers) {
+ s.mcp_servers.forEach(function(m) {
+ html += '' + escHtml(m) + ' ';
+ });
+ }
+ if (s.skills) {
+ s.skills.forEach(function(sk) {
+ html += '' + escHtml(sk) + ' ';
+ });
+ }
+ html += '
';
+ }
// Expandable preview area (hidden by default)
html += '
';
html += '';
@@ -502,9 +695,7 @@ function toggleLayout() {
if (btn) btn.classList.toggle('active', layout === 'list');
var icon = document.getElementById('layoutIcon');
if (icon) {
- icon.innerHTML = layout === 'list'
- ? ' '
- : ' ';
+ icon.innerHTML = ' ';
}
render();
}
@@ -522,6 +713,16 @@ function renderListCard(s, idx) {
var html = '';
html += '
' + escHtml(s.tool) + ' ';
+ if (s.mcp_servers && s.mcp_servers.length > 0) {
+ s.mcp_servers.forEach(function(m) {
+ html += '
' + escHtml(m) + ' ';
+ });
+ }
+ if (s.skills && s.skills.length > 0) {
+ s.skills.forEach(function(sk) {
+ html += '
' + escHtml(sk) + ' ';
+ });
+ }
html += '
' + escHtml(projName) + ' ';
html += '
' + escHtml((s.first_message || '').slice(0, 80)) + ' ';
html += '
' + s.messages + ' msgs ';
@@ -743,6 +944,11 @@ function render() {
return;
}
+ if (currentView === 'settings') {
+ renderSettings(content);
+ return;
+ }
+
if (currentView === 'running') {
renderRunning(content, sessions);
return;
@@ -755,7 +961,9 @@ function render() {
return;
}
var idx = 0;
- content.innerHTML = starredSessions.map(function(s) { return renderCard(s, idx++); }).join('');
+ var renderFn = layout === 'list' ? renderListCard : renderCard;
+ var wrapClass = layout === 'list' ? 'list-view' : 'grid-view';
+ content.innerHTML = '
' + starredSessions.map(function(s) { return renderFn(s, idx++); }).join('') + '
';
return;
}
@@ -869,7 +1077,8 @@ function renderProjects(container, sessions) {
return;
}
- var html = '
';
+ var isList = layout === 'list';
+ var html = '
';
sorted.forEach(function(entry) {
var name = entry[0];
var info = entry[1];
@@ -878,18 +1087,29 @@ function renderProjects(container, sessions) {
var totalSize = info.sessions.reduce(function(sum, s) { return sum + (s.file_size || 0); }, 0);
var latest = info.sessions[0];
- html += '
';
- html += '';
- html += '
';
- html += '' + info.sessions.length + ' sessions ';
- html += '' + totalMsgs + ' msgs ';
- html += '' + formatBytes(totalSize) + ' ';
- html += '
';
- html += '
Last: ' + timeAgo(latest.last_ts) + '
';
- html += '
';
+ if (isList) {
+ html += '
';
+ html += ' ';
+ html += '' + escHtml(name) + ' ';
+ html += '' + info.sessions.length + ' sessions ';
+ html += '' + totalMsgs + ' msgs ';
+ html += '' + formatBytes(totalSize) + ' ';
+ html += '' + timeAgo(latest.last_ts) + ' ';
+ html += '
';
+ } else {
+ html += '
';
+ html += '';
+ html += '
';
+ html += '' + info.sessions.length + ' sessions ';
+ html += '' + totalMsgs + ' msgs ';
+ html += '' + formatBytes(totalSize) + ' ';
+ html += '
';
+ html += '
Last: ' + timeAgo(latest.last_ts) + '
';
+ html += '
';
+ }
});
html += '
';
container.innerHTML = html;
@@ -1087,6 +1307,22 @@ async function openDetail(s) {
});
infoHtml += '
+ ';
infoHtml += '
';
+ // MCP servers
+ if (s.mcp_servers && s.mcp_servers.length > 0) {
+ infoHtml += '
MCP ';
+ s.mcp_servers.forEach(function(m) {
+ infoHtml += '' + escHtml(m) + ' ';
+ });
+ infoHtml += '
';
+ }
+ // Skills
+ if (s.skills && s.skills.length > 0) {
+ infoHtml += '
Skills ';
+ s.skills.forEach(function(sk) {
+ infoHtml += '' + escHtml(sk) + ' ';
+ });
+ infoHtml += '
';
+ }
infoHtml += '
';
// Action buttons
@@ -1128,10 +1364,24 @@ async function openDetail(s) {
data.messages.forEach(function(m) {
var roleClass = m.role === 'user' ? 'msg-user' : 'msg-assistant';
var roleLabel = m.role === 'user' ? 'You' : 'Assistant';
- msgsHtml += '';
+ var hasTools = m.tools && m.tools.length > 0;
+ msgsHtml += '
';
});
msgContainer.innerHTML = msgsHtml;
} else {
@@ -1557,9 +1807,11 @@ function renderRunning(container, sessions) {
var recentInactive = sessions.filter(function(s) { return !activeSessions[s.id]; }).slice(0, 6);
if (recentInactive.length > 0) {
html += '
Recently Inactive ';
- html += '
';
+ var renderFn = layout === 'list' ? renderListCard : renderCard;
+ var wrapClass = layout === 'list' ? 'list-view' : 'grid-view';
+ html += '
';
var idx = 0;
- recentInactive.forEach(function(s) { html += renderCard(s, idx++); });
+ recentInactive.forEach(function(s) { html += renderFn(s, idx++); });
html += '
';
}
@@ -1791,6 +2043,47 @@ function focusSession(sessionId) {
// ── Changelog view ────────────────────────────────────────────
+function renderSettings(container) {
+ var savedTheme = localStorage.getItem('codedash-theme') || 'dark';
+ var savedTerminal = localStorage.getItem('codedash-terminal') || '';
+
+ var html = '
';
+ html += '
Settings ';
+
+ // Theme
+ html += '
';
+ html += '
Theme ';
+ html += '
';
+ ['dark', 'light', 'system'].forEach(function(t) {
+ var active = savedTheme === t ? ' active' : '';
+ html += '' + t.charAt(0).toUpperCase() + t.slice(1) + ' ';
+ });
+ html += '
';
+ html += '
';
+
+ // Terminal
+ html += '
';
+ html += 'Terminal ';
+ html += '';
+ availableTerminals.forEach(function(t) {
+ if (!t.available) return;
+ var sel = t.id === savedTerminal ? ' selected' : '';
+ html += '' + escHtml(t.name) + ' ';
+ });
+ html += ' ';
+ html += '
';
+
+ // Version
+ html += '
';
+ var badge = document.getElementById('versionBadge');
+ var ver = badge ? badge.textContent : '';
+ html += 'codedash ' + escHtml(ver) + ' ';
+ html += '
';
+
+ html += '
';
+ container.innerHTML = html;
+}
+
async function renderChangelog(container) {
container.innerHTML = '
Loading changelog...
';
try {
@@ -1987,6 +2280,30 @@ function dismissUpdate() {
// ── Initialization ─────────────────────────────────────────────
(function init() {
+ // Show skeleton preloader
+ var content = document.getElementById('content');
+ if (content) {
+ var skeletonHtml = '';
+ for (var i = 0; i < 6; i++) {
+ skeletonHtml += '
';
+ skeletonHtml += '
';
+ skeletonHtml += '
';
+ skeletonHtml += '
';
+ skeletonHtml += '
';
+ skeletonHtml += '
';
+ skeletonHtml += '
';
+ skeletonHtml += '
';
+ skeletonHtml += '
';
+ skeletonHtml += '
';
+ skeletonHtml += '
';
+ skeletonHtml += '
';
+ skeletonHtml += '
';
+ skeletonHtml += '
';
+ skeletonHtml += '
';
+ }
+ content.innerHTML = skeletonHtml;
+ }
+
// Load data
loadSessions();
loadTerminals();
diff --git a/src/frontend/index.html b/src/frontend/index.html
index 0687c18..5a4331d 100644
--- a/src/frontend/index.html
+++ b/src/frontend/index.html
@@ -8,105 +8,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+