- {" PROJECT BRANCH LAST USE"}
+ {" PROJECT BRANCH LAST USE"}
- [✓]
+ ●
+ [✓]
{" "}
acme-api{" "}
@@ -414,19 +475,24 @@ export default function Home() {
{" "}25m ago
+ ◉
+ 2m
[✓]
quantum-dashboard{" "}
feat/cha
{" "}1h ago
- [✓]
+ ●
+ [✓]
ml-pipeline{" "}
exp/bert
{" "}just now
-
- [ ] pixel-engine{" "}develop{" "}3h ago
+
+ ○
+ [ ]
+ pixel-engine{" "}develop{" "}3h ago
diff --git a/site/app/terminal-cascade.tsx b/site/app/terminal-cascade.tsx
index 6ef619e..88263cd 100644
--- a/site/app/terminal-cascade.tsx
+++ b/site/app/terminal-cascade.tsx
@@ -4,9 +4,9 @@ import { useEffect, useState, useCallback } from "react";
import Image from "next/image";
const projects = [
- { name: "acme-api", branch: "main", time: "25m ago" },
- { name: "quantum-dash", branch: "feat/charts", time: "1h ago" },
- { name: "ml-pipeline", branch: "exp/bert", time: "just now" },
+ { name: "acme-api", branch: "main", time: "25m ago", status: "busy" as const },
+ { name: "quantum-dash", branch: "feat/charts", time: "1h ago", status: "idle" as const, elapsed: "4m" },
+ { name: "ml-pipeline", branch: "exp/bert", time: "just now", status: "busy" as const },
];
type Phase =
@@ -94,6 +94,12 @@ export function TerminalCascade() {
{projects.map((proj, i) => {
const checked = i < selectedCount;
const isActive = i === selectedCount - 1 && phase === "selecting";
+ const dot = proj.status === "busy"
+ ?
●
+ :
◉;
+ const tag = proj.status === "idle" && proj.elapsed
+ ?
{proj.elapsed.padEnd(2)}
+ :
;
return (
+ {dot}{tag}
{checked ? "[✓]" : "[ ]"}
- {proj.name.padEnd(20)}
+ {proj.name.padEnd(18)}
{proj.branch.padEnd(13)}
{proj.time}
);
})}
- [ ] pixel-engine{" "}develop{" "}3h ago
+ ○ [ ] pixel-engine{" "}develop{" "}3h ago
{/* Enter hint */}
diff --git a/src/data/history.ts b/src/data/history.ts
index a1425ef..3d9cf64 100644
--- a/src/data/history.ts
+++ b/src/data/history.ts
@@ -64,6 +64,9 @@ export async function discoverProjects(): Promise
{
sessionCount,
totalMessages: info.msgs,
tags: getTags(path),
+ activeSessions: 0,
+ busySessions: 0,
+ lastActivityMs: 0,
expanded: false,
sessions: null,
branches: null,
diff --git a/src/data/mock.ts b/src/data/mock.ts
index 0bc337e..61dfab3 100644
--- a/src/data/mock.ts
+++ b/src/data/mock.ts
@@ -21,6 +21,9 @@ export function generateMockProjects(): Project[] {
sessionCount: 14,
totalMessages: 342,
tags: "ts bun hono",
+ activeSessions: 0,
+ busySessions: 0,
+ lastActivityMs: 0,
expanded: false,
sessions: null,
branches: null,
@@ -40,6 +43,9 @@ export function generateMockProjects(): Project[] {
sessionCount: 8,
totalMessages: 187,
tags: "ts react vite",
+ activeSessions: 0,
+ busySessions: 0,
+ lastActivityMs: 0,
expanded: false,
sessions: null,
branches: null,
@@ -59,6 +65,9 @@ export function generateMockProjects(): Project[] {
sessionCount: 22,
totalMessages: 891,
tags: "rust wgpu",
+ activeSessions: 0,
+ busySessions: 0,
+ lastActivityMs: 0,
expanded: false,
sessions: null,
branches: null,
@@ -78,6 +87,9 @@ export function generateMockProjects(): Project[] {
sessionCount: 5,
totalMessages: 78,
tags: "go docker k8s",
+ activeSessions: 0,
+ busySessions: 0,
+ lastActivityMs: 0,
expanded: false,
sessions: null,
branches: null,
@@ -97,6 +109,9 @@ export function generateMockProjects(): Project[] {
sessionCount: 11,
totalMessages: 256,
tags: "ts rn expo",
+ activeSessions: 0,
+ busySessions: 0,
+ lastActivityMs: 0,
expanded: false,
sessions: null,
branches: null,
@@ -116,6 +131,9 @@ export function generateMockProjects(): Project[] {
sessionCount: 3,
totalMessages: 45,
tags: "ts astro mdx",
+ activeSessions: 0,
+ busySessions: 0,
+ lastActivityMs: 0,
expanded: false,
sessions: null,
branches: null,
@@ -135,6 +153,9 @@ export function generateMockProjects(): Project[] {
sessionCount: 19,
totalMessages: 523,
tags: "py torch",
+ activeSessions: 0,
+ busySessions: 0,
+ lastActivityMs: 0,
expanded: false,
sessions: null,
branches: null,
@@ -154,6 +175,9 @@ export function generateMockProjects(): Project[] {
sessionCount: 2,
totalMessages: 31,
tags: "ts express pg",
+ activeSessions: 0,
+ busySessions: 0,
+ lastActivityMs: 0,
expanded: false,
sessions: null,
branches: null,
@@ -399,6 +423,22 @@ const mockBranchData: Record = {
],
}
+export function generateMockBusySessions(projects: Project[]): void {
+ const now = Date.now()
+ for (const p of projects) {
+ if (p.activeSessions > 0) {
+ const isBusy = Math.random() > 0.4
+ p.busySessions = isBusy ? Math.min(p.activeSessions, 1 + Math.floor(Math.random() * p.activeSessions)) : 0
+ p.lastActivityMs = isBusy
+ ? now - Math.floor(Math.random() * 3000)
+ : now - (10_000 + Math.floor(Math.random() * 600_000))
+ } else {
+ p.busySessions = 0
+ p.lastActivityMs = 0
+ }
+ }
+}
+
export function generateMockSessions(projectPath: string): SessionInfo[] {
const name = projectPath.split("/").pop() || ""
return mockSessionData[name] || []
diff --git a/src/data/monitor.ts b/src/data/monitor.ts
new file mode 100644
index 0000000..b71cd39
--- /dev/null
+++ b/src/data/monitor.ts
@@ -0,0 +1,226 @@
+import { readdirSync, statSync } from "node:fs"
+import { join } from "node:path"
+import type { Project } from "../lib/types"
+
+const PROJECTS_DIR = `${Bun.env.HOME}/.claude/projects`
+const BUSY_THRESHOLD_MS = 5000
+
+export interface ActiveSession {
+ pid: string
+ cwd: string
+ tty: string
+ sessionFile: string | null
+ busy: boolean
+ lastActivityMs: number
+}
+
+// path → list of active sessions with tty info
+const sessionsByPath = new Map()
+
+function cwdToProjectKey(cwd: string): string {
+ return cwd.replaceAll("/", "-")
+}
+
+function findActiveJsonl(projectKey: string): { path: string; mtime: number } | null {
+ const projDir = join(PROJECTS_DIR, projectKey)
+ try {
+ const files = readdirSync(projDir).filter(f => f.endsWith(".jsonl"))
+ let best: { path: string; mtime: number } | null = null
+ for (const f of files) {
+ const full = join(projDir, f)
+ try {
+ const st = statSync(full)
+ const mt = st.mtimeMs
+ if (!best || mt > best.mtime) best = { path: full, mtime: mt }
+ } catch {}
+ }
+ return best
+ } catch {
+ return null
+ }
+}
+
+export async function detectActiveSessions(): Promise