diff --git a/src/data.js b/src/data.js
index d99a787..cb4d260 100644
--- a/src/data.js
+++ b/src/data.js
@@ -807,6 +807,44 @@ function resolveGitRoot(projectPath) {
}
}
+const _gitInfoCache = {};
+const GIT_INFO_CACHE_TTL = 30000; // 30 seconds
+
+function getProjectGitInfo(projectPath) {
+ if (!projectPath || !fs.existsSync(projectPath)) return null;
+ if (process.platform === 'win32') return null;
+
+ const now = Date.now();
+ const cached = _gitInfoCache[projectPath];
+ if (cached && (now - cached._ts) < GIT_INFO_CACHE_TTL) return cached;
+
+ const gitRoot = resolveGitRoot(projectPath);
+ if (!gitRoot) return null;
+
+ const cwd = gitRoot;
+ const opts = { encoding: 'utf8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] };
+ const info = { gitRoot, branch: '', remoteUrl: '', lastCommit: '', lastCommitDate: '', isDirty: false, _ts: now };
+
+ try { info.branch = execSync(`git -C "${cwd}" rev-parse --abbrev-ref HEAD 2>/dev/null`, opts).trim(); } catch {}
+ try { info.remoteUrl = execSync(`git -C "${cwd}" config --get remote.origin.url 2>/dev/null`, opts).trim(); } catch {}
+ try {
+ const log = execSync(`git -C "${cwd}" log -1 --format="%h %s" 2>/dev/null`, opts).trim();
+ if (log) {
+ const sp = log.indexOf(' ');
+ info.lastCommit = sp > 0 ? log.slice(sp + 1).slice(0, 80) : log;
+ info.lastCommitHash = sp > 0 ? log.slice(0, sp) : '';
+ }
+ } catch {}
+ try { info.lastCommitDate = execSync(`git -C "${cwd}" log -1 --format="%ci" 2>/dev/null`, opts).trim(); } catch {}
+ try {
+ const status = execSync(`git -C "${cwd}" status --porcelain 2>/dev/null`, opts).trim();
+ info.isDirty = status.length > 0;
+ } catch {}
+
+ _gitInfoCache[projectPath] = info;
+ return info;
+}
+
// ── Public API ─────────────────────────────────────────────
let _sessionsCache = null;
@@ -1958,6 +1996,7 @@ function getActiveSessions() {
module.exports = {
loadSessions,
loadSessionDetail,
+ getProjectGitInfo,
deleteSession,
getGitCommits,
exportSessionMarkdown,
diff --git a/src/frontend/app.js b/src/frontend/app.js
index 9f7d4f5..e43dd2c 100644
--- a/src/frontend/app.js
+++ b/src/frontend/app.js
@@ -1488,6 +1488,7 @@ async function openDetail(s) {
var detailToolLabel = s.tool === 'claude-ext' ? 'claude ext' : s.tool;
infoHtml += '
Tool' + escHtml(detailToolLabel) + '
';
infoHtml += 'Project' + escHtml(s.project_short || s.project || '') + '
';
+ infoHtml += '';
infoHtml += 'Session ID' + escHtml(s.id) + '
';
infoHtml += 'First seen' + escHtml(s.first_time || '') + '
';
infoHtml += 'Last seen' + escHtml(s.last_time || '') + ' (' + timeAgo(s.last_ts) + ')
';
@@ -1583,6 +1584,32 @@ async function openDetail(s) {
if (estBadge) estBadge.style.opacity = '0.5';
});
+ // Load git info
+ if (s.project) {
+ fetch('/api/git-info?project=' + encodeURIComponent(s.project))
+ .then(function(r) { return r.json(); })
+ .then(function(git) {
+ var el = document.getElementById('detail-git-info');
+ if (!el || git.error) return;
+ var html = '';
+ if (git.branch) {
+ html += 'Branch' + escHtml(git.branch);
+ if (git.isDirty) html += ' *';
+ html += '
';
+ }
+ if (git.lastCommit) {
+ html += 'Last commit';
+ if (git.lastCommitHash) html += '' + escHtml(git.lastCommitHash) + ' ';
+ html += escHtml(git.lastCommit) + '
';
+ }
+ if (git.remoteUrl) {
+ var displayUrl = git.remoteUrl.replace(/\.git$/, '').replace(/^https?:\/\//, '').replace(/^git@([^:]+):/, '$1/');
+ html += 'Remote' + escHtml(displayUrl) + '
';
+ }
+ el.innerHTML = html;
+ }).catch(function() {});
+ }
+
// Load git commits
if (s.project) {
var commits = await loadGitCommits(s.project, s.first_ts, s.last_ts);
diff --git a/src/frontend/styles.css b/src/frontend/styles.css
index 3f9ba90..9ed8b40 100644
--- a/src/frontend/styles.css
+++ b/src/frontend/styles.css
@@ -1377,6 +1377,15 @@ body {
font-size: 12px;
}
+.git-branch {
+ font-weight: 600;
+ color: var(--accent-purple);
+}
+.git-dirty {
+ color: var(--accent-orange);
+ font-weight: 700;
+}
+
.detail-messages {
margin-top: 16px;
}
diff --git a/src/server.js b/src/server.js
index 44978ae..690189d 100644
--- a/src/server.js
+++ b/src/server.js
@@ -3,7 +3,7 @@ const http = require('http');
const https = require('https');
const { URL } = require('url');
const { exec } = require('child_process');
-const { loadSessions, loadSessionDetail, deleteSession, getGitCommits, exportSessionMarkdown, getSessionPreview, searchFullText, getActiveSessions, getSessionReplay, getCostAnalytics, computeSessionCost } = require('./data');
+const { loadSessions, loadSessionDetail, deleteSession, getGitCommits, exportSessionMarkdown, getSessionPreview, searchFullText, getActiveSessions, getSessionReplay, getCostAnalytics, computeSessionCost, getProjectGitInfo } = require('./data');
const { detectTerminals, openInTerminal, focusTerminalByPid } = require('./terminals');
const { convertSession } = require('./convert');
const { generateHandoff } = require('./handoff');
@@ -155,6 +155,13 @@ function startServer(host, port, openBrowser = true) {
json(res, commits);
}
+ // ── Project git info ────────────────────
+ else if (req.method === 'GET' && pathname === '/api/git-info') {
+ const project = parsed.searchParams.get('project') || '';
+ const info = getProjectGitInfo(project);
+ json(res, info || { error: 'No git repo found' });
+ }
+
// ── Active sessions ─────────────────────
else if (req.method === 'GET' && pathname === '/api/active') {
const active = getActiveSessions();