chore(v0.6): checkpoint foundation — Amber theme, P1 types, P3 config, design docs
Prior-session prep for the v0.6 Project Manager, checkpointed on the v0.6-projects branch as a clean base for the parallel builders: - P1 (src/types.ts): ProjectInfo + ProjectSessionRef contracts; Config gains projectRoots/projectScanDepth/projectScanTtlMs/projectDirtyCheck. - P3 (src/config.ts): parseBool + parseProjectRoots helpers; parse the 4 PROJECT_* env vars with defaults ([homeDir]/4/10000/true). - UI: public/style.css indigo -> Amber (#e3a64a) accent theme. - Docs: FEATURE_PROJECT_MANAGER.md (1:N session design), THEME.md, docs/mockups/ (final-amber.png is the locked visual). Typecheck (backend + web) green. No behavior change to runtime code yet; P2/P4/P5/P6 implement the feature on top of this.
This commit is contained in:
@@ -31,6 +31,8 @@ const DEFAULT_MAX_MSGS_PER_SEC = 2000
|
||||
const DEFAULT_PERM_TIMEOUT_MS = 5 * 60_000 // H3: hold a PermissionRequest for 5 min
|
||||
const DEFAULT_REAP_INTERVAL_MS = 60_000 // idle reaper sweeps every minute
|
||||
const DEFAULT_PREVIEW_BYTES = 24 * 1024 // manage-page preview: tail of scrollback
|
||||
const DEFAULT_PROJECT_SCAN_DEPTH = 4 // v0.6: how deep to scan roots for .git
|
||||
const DEFAULT_PROJECT_SCAN_TTL_MS = 10_000 // v0.6: /projects discovery cache TTL
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -50,6 +52,27 @@ function parseNonNegativeInt(
|
||||
return n
|
||||
}
|
||||
|
||||
/** Parse a boolean env value ('1'/'true'/'on' → true, '0'/'false'/'off' → false), else fallback. */
|
||||
function parseBool(raw: string | undefined, fallback: boolean): boolean {
|
||||
const v = raw?.trim().toLowerCase()
|
||||
if (v === undefined || v === '') return fallback
|
||||
if (v === '1' || v === 'true' || v === 'on') return true
|
||||
if (v === '0' || v === 'false' || v === 'off') return false
|
||||
return fallback
|
||||
}
|
||||
|
||||
/** Parse PROJECT_ROOTS (comma-separated absolute paths); expand a leading '~' to homeDir.
|
||||
* Empty / unset → [homeDir]. */
|
||||
function parseProjectRoots(raw: string | undefined, homeDir: string): readonly string[] {
|
||||
if (raw === undefined || raw.trim() === '') return Object.freeze([homeDir])
|
||||
const roots = raw
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s !== '')
|
||||
.map((s) => (s === '~' ? homeDir : s.startsWith('~/') ? homeDir + s.slice(1) : s))
|
||||
return Object.freeze(roots.length > 0 ? roots : [homeDir])
|
||||
}
|
||||
|
||||
function parsePort(raw: string | undefined): number {
|
||||
if (raw === undefined) return DEFAULT_PORT
|
||||
const n = Number(raw)
|
||||
@@ -195,6 +218,20 @@ export function loadConfig(env: EnvLike): Config {
|
||||
|
||||
const allowedOrigins = deriveAllowedOrigins(port, env['ALLOWED_ORIGINS'])
|
||||
|
||||
// v0.6 Project Manager — discovery config
|
||||
const projectRoots = parseProjectRoots(env['PROJECT_ROOTS'], homeDir)
|
||||
const projectScanDepth = parseNonNegativeInt(
|
||||
env['PROJECT_SCAN_DEPTH'],
|
||||
'PROJECT_SCAN_DEPTH',
|
||||
DEFAULT_PROJECT_SCAN_DEPTH,
|
||||
)
|
||||
const projectScanTtlMs = parseNonNegativeInt(
|
||||
env['PROJECT_SCAN_TTL'],
|
||||
'PROJECT_SCAN_TTL',
|
||||
DEFAULT_PROJECT_SCAN_TTL_MS,
|
||||
)
|
||||
const projectDirtyCheck = parseBool(env['PROJECT_DIRTY_CHECK'], true)
|
||||
|
||||
return Object.freeze({
|
||||
port,
|
||||
bindHost,
|
||||
@@ -211,5 +248,9 @@ export function loadConfig(env: EnvLike): Config {
|
||||
previewBytes,
|
||||
useTmux,
|
||||
allowedOrigins,
|
||||
projectRoots,
|
||||
projectScanDepth,
|
||||
projectScanTtlMs,
|
||||
projectDirtyCheck,
|
||||
} satisfies Config)
|
||||
}
|
||||
|
||||
30
src/types.ts
30
src/types.ts
@@ -34,6 +34,11 @@ export interface Config {
|
||||
readonly previewBytes: number; // manage-page preview: bytes of scrollback tail rendered
|
||||
readonly useTmux: boolean; // H1: spawn the shell inside tmux so it survives a server restart
|
||||
readonly allowedOrigins: readonly string[]; // derived from NIC IPs, NOT bindHost (M1)
|
||||
// v0.6 Project Manager — project discovery (impl: src/config.ts T-PM3)
|
||||
readonly projectRoots: readonly string[]; // PROJECT_ROOTS, default [homeDir]
|
||||
readonly projectScanDepth: number; // PROJECT_SCAN_DEPTH, default 4
|
||||
readonly projectScanTtlMs: number; // PROJECT_SCAN_TTL, default 10000 (discovery cache)
|
||||
readonly projectDirtyCheck: boolean; // PROJECT_DIRTY_CHECK, default true (git status per repo)
|
||||
}
|
||||
|
||||
/** process.env is structurally assignable to this; keeps the file free of `NodeJS.*`. */
|
||||
@@ -192,6 +197,31 @@ export interface LiveSessionInfo {
|
||||
rows: number;
|
||||
}
|
||||
|
||||
/* ───────────────── project manager (v0.6, §4.3 FEATURE doc) ──────────────── */
|
||||
|
||||
/** One running session belonging to a project (live-sessions归并 by cwd).
|
||||
* Mirrors LiveSessionInfo fields so buildProjects can map directly. */
|
||||
export interface ProjectSessionRef {
|
||||
id: string;
|
||||
title?: string; // tab title / derived label (e.g. 'claude', 'shell'); optional
|
||||
status: ClaudeStatus; // reuse hook status (working|waiting|idle|unknown)
|
||||
clientCount: number; // mirror devices currently attached
|
||||
createdAt: number;
|
||||
exited: boolean;
|
||||
}
|
||||
|
||||
/** A discovered project (git repo or recently-used cwd) for the Projects panel.
|
||||
* impl: src/http/projects.ts buildProjects(cfg, liveSessions) — T-PM2. */
|
||||
export interface ProjectInfo {
|
||||
name: string; // repo directory name
|
||||
path: string; // absolute path
|
||||
isGit: boolean;
|
||||
branch?: string; // current branch (git repos only)
|
||||
dirty?: boolean; // uncommitted changes (when projectDirtyCheck)
|
||||
lastActiveMs?: number; // newest ~/.claude/projects mtime for this cwd; sort key
|
||||
sessions: ProjectSessionRef[]; // running sessions in this project (1:N; may be empty)
|
||||
}
|
||||
|
||||
export interface SessionManager {
|
||||
handleAttach(
|
||||
ws: WebSocketLike,
|
||||
|
||||
Reference in New Issue
Block a user