feat(v0.6): per-project detail view — branch/worktrees + active sessions

Clicking a project's name opens an in-app detail view (back button returns to
the grid) with:
- header: name, path, branch, dirty
- Branch / Worktrees: each `git worktree list` entry (branch or detached@head,
  main/current/locked/prunable tags, path) — a single-worktree repo shows its
  branch; a multi-worktree repo shows them all
- Active sessions (N): detailed rows (status, devices watching, started-ago)
  with Open + Kill
- the Claude/Codex/VS Code launcher row

Backend: src/http/worktrees.ts (parseWorktrees pure + listWorktrees via
execFile, no shell, best-effort); buildProjectDetail(cfg, path, liveSessions)
in projects.ts (validates absolute existing dir, reuses branch/dirty/session
helpers, uncached — detail is on-demand and wants fresh state); read-only
GET /projects/detail?path= (no Origin guard, like /projects; 400 missing /
404 invalid). Types: WorktreeInfo, ProjectDetail.

Frontend: detail view + navigation in projects.ts (grid<->detail), project
name becomes a link, Kill via DELETE /live-sessions/:id.

Tests +21 (452 total): worktree parser, buildProjectDetail (validation,
non-git, session merge, real git-init worktree), renderProjectDetail.
worktrees.ts 100% / projects.ts 93.8% cov. Verified in-browser: opened a
Claude session, then the detail showed its branch+worktree and the live
session with Open/Kill.
This commit is contained in:
Yaojia Wang
2026-06-30 13:29:41 +02:00
parent a390130205
commit 99bc6fd9f6
10 changed files with 810 additions and 12 deletions

View File

@@ -1156,6 +1156,160 @@ body {
text-align: center;
}
/* ── Project detail view ─────────────────────────────────────────── */
.proj-name-link {
cursor: pointer;
}
.proj-name-link:hover {
text-decoration: underline;
text-underline-offset: 2px;
}
.proj-detail-inner {
max-width: 900px;
margin: 0 auto;
}
.proj-back {
background: none;
border: 1px solid var(--border);
color: var(--text-dim);
border-radius: 8px;
padding: 6px 12px;
font: inherit;
font-size: 13px;
cursor: pointer;
margin-bottom: 16px;
transition: background 0.12s ease, color 0.12s ease;
}
.proj-back:hover {
background: var(--surface-2);
color: var(--text);
}
.proj-detail-head {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.proj-detail-name {
margin: 0;
font-size: 22px;
font-weight: 700;
color: #fff;
}
.proj-detail-path {
margin: 4px 0 8px;
font-family: Menlo, Consolas, monospace;
font-size: 12px;
color: var(--text-faint);
word-break: break-all;
}
.proj-section-title {
margin: 22px 0 10px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-faint);
}
/* Worktrees */
.proj-wt-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.proj-wt-row {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 8px;
}
.proj-wt-branch {
font-weight: 600;
color: var(--text);
}
.proj-wt-tag {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 2px 7px;
border-radius: 999px;
background: var(--surface-3);
color: var(--text-dim);
}
.proj-wt-tag-current {
background: var(--accent);
color: var(--on-accent);
}
.proj-wt-path {
margin-left: auto;
font-family: Menlo, Consolas, monospace;
font-size: 11px;
color: var(--text-faint);
word-break: break-all;
}
/* Detailed session rows */
.proj-detail-sessions {
display: flex;
flex-direction: column;
gap: 8px;
}
.proj-dsession {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 8px;
}
.proj-dsession-main {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
flex: 1;
}
.proj-dsession-title {
font-weight: 600;
color: var(--text);
}
.proj-dsession-meta {
font-size: 12px;
color: var(--text-dim);
}
.proj-dsession-open {
background: var(--accent);
color: var(--on-accent);
border: none;
border-radius: 7px;
padding: 6px 14px;
font: inherit;
font-weight: 600;
cursor: pointer;
}
.proj-dsession-open:hover {
background: var(--accent-2);
}
.proj-dsession-kill {
background: none;
border: 1px solid var(--border);
color: var(--text-dim);
border-radius: 7px;
width: 30px;
height: 30px;
cursor: pointer;
font-size: 14px;
}
.proj-dsession-kill:hover {
background: var(--red, #e5484d);
border-color: var(--red, #e5484d);
color: #fff;
}
/* Empty state */
.proj-empty {
color: var(--text-dim);