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:
Yaojia Wang
2026-06-30 10:05:40 +02:00
parent f9964a517d
commit dc5d073374
16 changed files with 1073 additions and 23 deletions

View File

@@ -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)
}