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:
154
public/style.css
154
public/style.css
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user