feat(v0.6): show CLAUDE.md on the project detail page + /init generate/update button

The detail page now reads <repo>/CLAUDE.md and shows it in a scrollable
monospace box, with one smart button: "↻ Update" when it exists / " Generate"
when it doesn't. The button opens an interactive Claude session in the repo
running /init (claude "/init") so you review what it writes — reuses the project
launcher, no new backend orchestration, stays a byte-shuttle.

- ProjectDetail += hasClaudeMd / claudeMd; buildProjectDetail reads CLAUDE.md
  (truncated 64KB, best-effort, in the existing Promise.all)
- renderProjectDetail: CLAUDE.md section (content box or empty state) + button
- style.css: .proj-section-row / .proj-claudemd-btn / .proj-claudemd

Tests +5 (458): backend read present/absent; frontend content+Update,
empty+Generate, button opens `claude "/init"`. Verified: tsc clean, full suite
green, build OK, curl returns the content, in-browser shows the box + button.
This commit is contained in:
Yaojia Wang
2026-06-30 14:51:42 +02:00
parent 67b0a43b39
commit 88b960bac5
7 changed files with 143 additions and 1 deletions

View File

@@ -361,6 +361,20 @@ export async function buildProjects(
return sortProjects(withSessions).slice(0, MAX_PROJECTS)
}
const CLAUDE_MD_MAX_BYTES = 64 * 1024 // cap CLAUDE.md content sent for display
/** Read <repo>/CLAUDE.md (truncated); undefined when absent/unreadable. */
async function readClaudeMd(projectPath: string): Promise<string | undefined> {
try {
const text = await fs.readFile(path.join(projectPath, 'CLAUDE.md'), 'utf8')
return text.length > CLAUDE_MD_MAX_BYTES
? text.slice(0, CLAUDE_MD_MAX_BYTES) + '\n\n…(truncated)'
: text
} catch {
return undefined
}
}
/**
* Detail for one project: branch + dirty + worktrees + its running sessions.
* `projectPath` must be an absolute, existing directory (else null — 404). Not
@@ -383,10 +397,11 @@ export async function buildProjectDetail(
if (!stat.isDirectory()) return null
const isGit = await hasGitEntry(projectPath)
const [branch, dirty, worktrees] = await Promise.all([
const [branch, dirty, worktrees, claudeMd] = await Promise.all([
isGit ? readBranch(projectPath) : Promise.resolve(undefined),
isGit && cfg.projectDirtyCheck ? readDirty(projectPath) : Promise.resolve(undefined),
isGit ? listWorktrees(projectPath) : Promise.resolve([]),
readClaudeMd(projectPath),
])
return {
@@ -397,5 +412,7 @@ export async function buildProjectDetail(
dirty,
worktrees,
sessions: matchSessions(projectPath, liveSessions),
hasClaudeMd: claudeMd !== undefined,
claudeMd,
}
}