clementine-agent 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli/dashboard.js +1258 -164
  2. package/package.json +1 -1
@@ -866,7 +866,13 @@ function getStatus() {
866
866
  try {
867
867
  const st = JSON.parse(readFileSync(statusFile, 'utf-8'));
868
868
  if (st.status === 'running') {
869
- currentActivity = 'Deep work: ' + (st.jobName || dir);
869
+ // If the unleashed run has a real jobName, use it; otherwise just say
870
+ // "Deep work in progress" — exposing the auto-generated dir id like
871
+ // "deep-1776118926610..." in the header reads as debug noise.
872
+ const friendly = st.jobName && !/^deep-\d{10,}$/.test(st.jobName)
873
+ ? st.jobName
874
+ : null;
875
+ currentActivity = friendly ? ('Deep work: ' + friendly) : 'Deep work in progress';
870
876
  break;
871
877
  }
872
878
  }
@@ -2240,6 +2246,493 @@ export async function cmdDashboard(opts) {
2240
2246
  writeFileSync(queueFile, JSON.stringify(queue, null, 2));
2241
2247
  res.json({ ok: true, id });
2242
2248
  });
2249
+ app.get('/api/home-digest', async (_req, res) => {
2250
+ try {
2251
+ const now = new Date();
2252
+ const dayName = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][now.getDay()];
2253
+ const dayLabel = now.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' });
2254
+ const hour = now.getHours();
2255
+ const greetingPart = hour < 5 ? 'Good evening' : hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
2256
+ // ── Resolve user name (USER_MODEL.md → MEMORY.md → env → OS user) ──
2257
+ let userName = null;
2258
+ const tryName = (v) => {
2259
+ if (typeof v === 'string' && v.trim()) {
2260
+ userName = v.trim().split(/\s+/)[0];
2261
+ return true;
2262
+ }
2263
+ return false;
2264
+ };
2265
+ try {
2266
+ const matter = (await import('gray-matter')).default;
2267
+ const userModelPath = path.join(VAULT_DIR, '00-System', 'USER_MODEL.md');
2268
+ if (!userName && existsSync(userModelPath)) {
2269
+ const um = matter(readFileSync(userModelPath, 'utf-8'));
2270
+ const d = um.data;
2271
+ tryName(d.preferred_name) || tryName(d.first_name) || tryName(d.name) || tryName(d.user_name);
2272
+ }
2273
+ const memoryPath = path.join(VAULT_DIR, '00-System', 'MEMORY.md');
2274
+ if (!userName && existsSync(memoryPath)) {
2275
+ const mm = matter(readFileSync(memoryPath, 'utf-8'));
2276
+ const d = mm.data;
2277
+ tryName(d.preferred_name) || tryName(d.first_name) || tryName(d.name) || tryName(d.user_name);
2278
+ }
2279
+ }
2280
+ catch { /* */ }
2281
+ if (!userName)
2282
+ tryName(process.env.CLEMENTINE_USER_NAME || process.env.USER_NAME);
2283
+ if (!userName) {
2284
+ try {
2285
+ const os = await import('os');
2286
+ const u = os.userInfo().username;
2287
+ if (u && u !== 'root')
2288
+ tryName(u.replace(/[._-].*$/, ''));
2289
+ }
2290
+ catch { /* */ }
2291
+ }
2292
+ if (!userName)
2293
+ userName = 'there';
2294
+ // Capitalize first letter so "nathan" → "Nathan"
2295
+ userName = userName.charAt(0).toUpperCase() + userName.slice(1);
2296
+ // ── KPIs ──
2297
+ const kpis = {
2298
+ activeRuns: 0,
2299
+ runsToday: 0,
2300
+ timeSavedMinutes: 0,
2301
+ pendingApprovals: 0,
2302
+ overdueTasks: 0,
2303
+ };
2304
+ // active unleashed runs
2305
+ try {
2306
+ const unleashedDir = path.join(BASE_DIR, 'unleashed');
2307
+ if (existsSync(unleashedDir)) {
2308
+ const dirs = readdirSync(unleashedDir, { withFileTypes: true }).filter((d) => d.isDirectory());
2309
+ for (const d of dirs) {
2310
+ const statusFile = path.join(unleashedDir, d.name, 'status.json');
2311
+ if (existsSync(statusFile)) {
2312
+ try {
2313
+ const s = JSON.parse(readFileSync(statusFile, 'utf-8'));
2314
+ if (s.status === 'running')
2315
+ kpis.activeRuns++;
2316
+ }
2317
+ catch { /* */ }
2318
+ }
2319
+ }
2320
+ }
2321
+ }
2322
+ catch { /* */ }
2323
+ // runs today + time saved (rough heuristic: 5 min per cron run)
2324
+ try {
2325
+ const cronRunsDir = path.join(BASE_DIR, 'cron', 'runs');
2326
+ if (existsSync(cronRunsDir)) {
2327
+ const today = now.toISOString().slice(0, 10);
2328
+ const files = readdirSync(cronRunsDir).filter(f => f.endsWith('.jsonl'));
2329
+ for (const f of files) {
2330
+ try {
2331
+ const lines = readFileSync(path.join(cronRunsDir, f), 'utf-8').trim().split('\n').filter(Boolean);
2332
+ for (const line of lines.slice(-30)) {
2333
+ try {
2334
+ const e = JSON.parse(line);
2335
+ const ts = e.completedAt || e.startedAt || e.timestamp;
2336
+ if (!ts)
2337
+ continue;
2338
+ if (String(ts).slice(0, 10) === today)
2339
+ kpis.runsToday++;
2340
+ }
2341
+ catch { /* */ }
2342
+ }
2343
+ }
2344
+ catch { /* */ }
2345
+ }
2346
+ // Time saved this week — count last 7 days × 5 min
2347
+ const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
2348
+ let weeklyRuns = 0;
2349
+ for (const f of files) {
2350
+ try {
2351
+ const lines = readFileSync(path.join(cronRunsDir, f), 'utf-8').trim().split('\n').filter(Boolean);
2352
+ for (const line of lines) {
2353
+ try {
2354
+ const e = JSON.parse(line);
2355
+ const ts = new Date(e.completedAt || e.startedAt || e.timestamp || 0).getTime();
2356
+ if (ts >= weekAgo)
2357
+ weeklyRuns++;
2358
+ }
2359
+ catch { /* */ }
2360
+ }
2361
+ }
2362
+ catch { /* */ }
2363
+ }
2364
+ kpis.timeSavedMinutes = weeklyRuns * 5;
2365
+ }
2366
+ }
2367
+ catch { /* */ }
2368
+ // pending approvals (self-improve)
2369
+ try {
2370
+ const siDir = path.join(BASE_DIR, 'self-improve');
2371
+ const proposalsFile = path.join(siDir, 'proposals.json');
2372
+ if (existsSync(proposalsFile)) {
2373
+ const data = JSON.parse(readFileSync(proposalsFile, 'utf-8'));
2374
+ const items = Array.isArray(data) ? data : (data.proposals || []);
2375
+ kpis.pendingApprovals = items.filter((p) => p.status === 'pending').length;
2376
+ }
2377
+ }
2378
+ catch { /* */ }
2379
+ // overdue tasks
2380
+ try {
2381
+ const tasksFile = path.join(VAULT_DIR, '05-Tasks', 'TASKS.md');
2382
+ if (existsSync(tasksFile)) {
2383
+ const content = readFileSync(tasksFile, 'utf-8');
2384
+ const todayStr = now.toISOString().slice(0, 10);
2385
+ // Match "- [ ] task @YYYY-MM-DD" pattern; overdue if date < today
2386
+ const lines = content.split('\n').filter(l => /^[-*]\s*\[ \]/.test(l));
2387
+ for (const line of lines) {
2388
+ const m = line.match(/@(\d{4}-\d{2}-\d{2})/);
2389
+ if (m && m[1] < todayStr)
2390
+ kpis.overdueTasks++;
2391
+ }
2392
+ }
2393
+ }
2394
+ catch { /* */ }
2395
+ // ── Today's runs ──
2396
+ const todayRuns = [];
2397
+ try {
2398
+ const { parseCronJobs, parseAgentCronJobs } = await import('../gateway/cron-scheduler.js');
2399
+ const all = [...parseCronJobs(), ...parseAgentCronJobs(path.join(VAULT_DIR, '00-System', 'agents'))];
2400
+ const cronParser = await import('cron-parser');
2401
+ const todayStart = new Date(now.toDateString()).getTime();
2402
+ const todayEnd = todayStart + 24 * 60 * 60 * 1000;
2403
+ for (const job of all) {
2404
+ if (!job.enabled)
2405
+ continue;
2406
+ let next = null;
2407
+ let firedToday = false;
2408
+ try {
2409
+ const interval = cronParser.CronExpressionParser?.parse?.(job.schedule, { currentDate: now }) ??
2410
+ cronParser.default?.parseExpression?.(job.schedule, { currentDate: now }) ??
2411
+ cronParser.parseExpression?.(job.schedule, { currentDate: now });
2412
+ const nextDate = typeof interval.next === 'function' ? interval.next().toDate() : interval.next();
2413
+ next = nextDate.toISOString();
2414
+ }
2415
+ catch { /* */ }
2416
+ // Did it fire today already? Look at cron runs.
2417
+ try {
2418
+ const runsFile = path.join(BASE_DIR, 'cron', 'runs', job.name + '.jsonl');
2419
+ if (existsSync(runsFile)) {
2420
+ const tail = readFileSync(runsFile, 'utf-8').trim().split('\n').filter(Boolean).slice(-12);
2421
+ for (const line of tail) {
2422
+ try {
2423
+ const e = JSON.parse(line);
2424
+ const ts = new Date(e.completedAt || e.startedAt || 0).getTime();
2425
+ if (ts >= todayStart && ts < todayEnd) {
2426
+ firedToday = true;
2427
+ break;
2428
+ }
2429
+ }
2430
+ catch { /* */ }
2431
+ }
2432
+ }
2433
+ }
2434
+ catch { /* */ }
2435
+ todayRuns.push({
2436
+ name: job.name,
2437
+ nextRun: next,
2438
+ lastRun: null,
2439
+ firedToday,
2440
+ agentSlug: job.agentSlug ?? null,
2441
+ });
2442
+ }
2443
+ }
2444
+ catch { /* */ }
2445
+ // Sort: fired today first (so user sees what already happened), then upcoming by nextRun
2446
+ todayRuns.sort((a, b) => {
2447
+ if (a.firedToday !== b.firedToday)
2448
+ return a.firedToday ? -1 : 1;
2449
+ if (!a.nextRun)
2450
+ return 1;
2451
+ if (!b.nextRun)
2452
+ return -1;
2453
+ return new Date(a.nextRun).getTime() - new Date(b.nextRun).getTime();
2454
+ });
2455
+ // ── Briefing: parse today's daily note ──
2456
+ const briefing = {
2457
+ highlights: [],
2458
+ needsReview: [],
2459
+ };
2460
+ const todayStr = now.toISOString().slice(0, 10);
2461
+ const dailyNotePath = path.join(VAULT_DIR, '01-Daily-Notes', todayStr + '.md');
2462
+ try {
2463
+ if (existsSync(dailyNotePath)) {
2464
+ const note = readFileSync(dailyNotePath, 'utf-8');
2465
+ // Parse Sends (cron job result lines)
2466
+ const sendsMatch = note.match(/##\s+([\w-]+)\s+\d{4}-\d{2}-\d{2}\s*\n\s*\n###\s+Sends\s*\n([\s\S]*?)(?:\n###|\n##|$)/g);
2467
+ if (sendsMatch) {
2468
+ for (const block of sendsMatch.slice(0, 3)) {
2469
+ const titleM = block.match(/##\s+([\w-]+)\s/);
2470
+ const title = titleM ? titleM[1] : 'cron';
2471
+ const sendLines = block.split('\n').filter(l => l.trim().startsWith('- ')).slice(0, 5);
2472
+ if (sendLines.length > 0) {
2473
+ briefing.highlights.push({
2474
+ text: title + ' sent ' + sendLines.length + (sendLines.length === 1 ? ' message' : ' messages'),
2475
+ source: 'daily-note',
2476
+ });
2477
+ }
2478
+ }
2479
+ }
2480
+ // Parse one-line summary lines like "<job-name> 2026-04-28: T2: 0 sent | T3: 0 ... | Replies: 0 | Skipped: ..."
2481
+ const summaryLines = note.split('\n').filter(l => /\d{4}-\d{2}-\d{2}.*Replies/i.test(l)).slice(0, 3);
2482
+ for (const s of summaryLines) {
2483
+ briefing.highlights.push({ text: s.trim().replace(/^.*?\d{4}-\d{2}-\d{2}:?\s*/, ''), source: 'daily-note' });
2484
+ }
2485
+ // Parse "## Interactions" entries
2486
+ const interactionsMatch = note.match(/## Interactions\s*\n([\s\S]*?)(?:\n##|$)/);
2487
+ if (interactionsMatch) {
2488
+ const lines = interactionsMatch[1].split('\n').filter(l => l.trim().startsWith('- **'));
2489
+ // Take the most informative lines (longest contentful body)
2490
+ const interactiveOnes = lines
2491
+ .map(l => {
2492
+ const stripped = l.replace(/^- \*\*[^*]+\*\*[^:]*:\s*/, '');
2493
+ return { line: l, body: stripped, len: stripped.length };
2494
+ })
2495
+ .filter(x => x.len > 40)
2496
+ .sort((a, b) => b.len - a.len)
2497
+ .slice(0, 4);
2498
+ for (const i of interactiveOnes) {
2499
+ const titleM = i.line.match(/\*\*([^*]+)\*\*/);
2500
+ const title = titleM ? titleM[1] : 'agent';
2501
+ briefing.highlights.push({
2502
+ text: '[' + title + '] ' + i.body.slice(0, 200),
2503
+ source: 'interactions',
2504
+ });
2505
+ }
2506
+ }
2507
+ }
2508
+ }
2509
+ catch { /* */ }
2510
+ // Fallback: if briefing.highlights is sparse, pull from /api/activity-style events
2511
+ if (briefing.highlights.length < 3) {
2512
+ try {
2513
+ const cronRunsDir = path.join(BASE_DIR, 'cron', 'runs');
2514
+ if (existsSync(cronRunsDir)) {
2515
+ const todayMs = new Date(todayStr + 'T00:00:00Z').getTime();
2516
+ const recent = [];
2517
+ for (const f of readdirSync(cronRunsDir).filter(x => x.endsWith('.jsonl')).slice(0, 30)) {
2518
+ try {
2519
+ const lines = readFileSync(path.join(cronRunsDir, f), 'utf-8').trim().split('\n').filter(Boolean).slice(-5);
2520
+ for (const line of lines) {
2521
+ try {
2522
+ const e = JSON.parse(line);
2523
+ const ts = new Date(e.completedAt || e.startedAt || 0).getTime();
2524
+ if (ts < todayMs)
2525
+ continue;
2526
+ const body = (e.responsePreview || e.summary || e.output || '').slice(0, 180);
2527
+ if (!body)
2528
+ continue;
2529
+ recent.push({ ts, title: f.replace('.jsonl', ''), body });
2530
+ }
2531
+ catch { /* */ }
2532
+ }
2533
+ }
2534
+ catch { /* */ }
2535
+ }
2536
+ recent.sort((a, b) => b.ts - a.ts);
2537
+ for (const r of recent.slice(0, 4 - briefing.highlights.length)) {
2538
+ briefing.highlights.push({ text: '[' + r.title + '] ' + r.body, source: 'activity' });
2539
+ }
2540
+ }
2541
+ }
2542
+ catch { /* */ }
2543
+ }
2544
+ // Needs review: pending approvals + overdue + broken jobs + active unleashed
2545
+ if (kpis.pendingApprovals > 0) {
2546
+ briefing.needsReview.push({
2547
+ text: kpis.pendingApprovals + ' self-improve proposal' + (kpis.pendingApprovals === 1 ? '' : 's') + ' awaiting review',
2548
+ href: '#brain/learning',
2549
+ });
2550
+ }
2551
+ if (kpis.overdueTasks > 0) {
2552
+ briefing.needsReview.push({
2553
+ text: kpis.overdueTasks + ' overdue task' + (kpis.overdueTasks === 1 ? '' : 's'),
2554
+ href: '#team/goals',
2555
+ });
2556
+ }
2557
+ if (kpis.activeRuns > 0) {
2558
+ briefing.needsReview.push({
2559
+ text: kpis.activeRuns + ' unleashed task' + (kpis.activeRuns === 1 ? '' : 's') + ' running',
2560
+ href: '#build/workflows',
2561
+ });
2562
+ }
2563
+ // Broken jobs (consecutive failures)
2564
+ try {
2565
+ const cronRunsDir = path.join(BASE_DIR, 'cron', 'runs');
2566
+ if (existsSync(cronRunsDir)) {
2567
+ for (const f of readdirSync(cronRunsDir).filter(x => x.endsWith('.jsonl'))) {
2568
+ try {
2569
+ const tail = readFileSync(path.join(cronRunsDir, f), 'utf-8').trim().split('\n').filter(Boolean).slice(-3);
2570
+ const failures = tail.filter(l => {
2571
+ try {
2572
+ const e = JSON.parse(l);
2573
+ return e.status === 'error' || e.error;
2574
+ }
2575
+ catch {
2576
+ return false;
2577
+ }
2578
+ });
2579
+ if (failures.length >= 2) {
2580
+ briefing.needsReview.push({
2581
+ text: f.replace('.jsonl', '') + ' failing — last ' + failures.length + ' runs errored',
2582
+ href: '#build/crons',
2583
+ });
2584
+ }
2585
+ }
2586
+ catch { /* */ }
2587
+ }
2588
+ }
2589
+ }
2590
+ catch { /* */ }
2591
+ res.json({
2592
+ greeting: { phrase: greetingPart, name: userName, dayLabel, dayName },
2593
+ kpis,
2594
+ briefing,
2595
+ todayRuns: todayRuns.slice(0, 6),
2596
+ });
2597
+ }
2598
+ catch (err) {
2599
+ res.status(500).json({ error: String(err) });
2600
+ }
2601
+ });
2602
+ app.get('/api/vault-files', async (req, res) => {
2603
+ try {
2604
+ const limit = Math.min(parseInt(String(req.query.limit ?? '120'), 10) || 120, 500);
2605
+ const sinceDays = Math.max(parseInt(String(req.query.sinceDays ?? '30'), 10) || 30, 1);
2606
+ const agentFilter = typeof req.query.agent === 'string' ? req.query.agent : '';
2607
+ const folderFilter = typeof req.query.folder === 'string' ? req.query.folder : '';
2608
+ const search = typeof req.query.q === 'string' ? req.query.q.toLowerCase() : '';
2609
+ const includeAuto = req.query.includeAuto === '1';
2610
+ const cutoffMs = Date.now() - sinceDays * 24 * 60 * 60 * 1000;
2611
+ const vaultRoot = path.join(BASE_DIR, 'vault');
2612
+ const matter = (await import('gray-matter')).default;
2613
+ const files = [];
2614
+ function walk(dir) {
2615
+ let entries = [];
2616
+ try {
2617
+ entries = readdirSync(dir);
2618
+ }
2619
+ catch {
2620
+ return;
2621
+ }
2622
+ for (const e of entries) {
2623
+ if (e.startsWith('.'))
2624
+ continue;
2625
+ const full = path.join(dir, e);
2626
+ let stat;
2627
+ try {
2628
+ stat = statSync(full);
2629
+ }
2630
+ catch {
2631
+ continue;
2632
+ }
2633
+ if (stat.isDirectory()) {
2634
+ walk(full);
2635
+ continue;
2636
+ }
2637
+ if (!e.endsWith('.md'))
2638
+ continue;
2639
+ if (e.endsWith('.md.bak'))
2640
+ continue;
2641
+ if (stat.mtimeMs < cutoffMs)
2642
+ continue;
2643
+ const rel = path.relative(vaultRoot, full);
2644
+ // Skip auto-generated MCP/skill wrappers unless explicitly requested.
2645
+ // Path patterns: 00-System/skills/auto/* (generated tool wrappers).
2646
+ if (!includeAuto && rel.startsWith('00-System/skills/auto/'))
2647
+ continue;
2648
+ if (!includeAuto && rel.startsWith('00-System/agents/') && /\/(MEMORY|HEARTBEAT|TASKS|CRON)\.md$/.test(rel))
2649
+ continue;
2650
+ const folder = path.dirname(rel).split(path.sep)[0] || '';
2651
+ let agentSlug = null;
2652
+ if (rel.startsWith('00-System/agents/')) {
2653
+ const m = rel.match(/^00-System\/agents\/([^/]+)\//);
2654
+ if (m)
2655
+ agentSlug = m[1];
2656
+ }
2657
+ // Skip system housekeeping files (their author will surface via mtime in agent's own dir)
2658
+ let title = path.basename(rel, '.md');
2659
+ let typeTag = null;
2660
+ try {
2661
+ const head = readFileSync(full, 'utf-8').slice(0, 4000);
2662
+ const parsed = matter(head);
2663
+ const data = parsed.data;
2664
+ if (typeof data.title === 'string')
2665
+ title = data.title;
2666
+ else if (typeof data.name === 'string')
2667
+ title = data.name;
2668
+ else {
2669
+ const h1 = (parsed.content || '').match(/^#\s+(.+)$/m);
2670
+ if (h1)
2671
+ title = h1[1].trim();
2672
+ }
2673
+ if (typeof data.type === 'string')
2674
+ typeTag = data.type;
2675
+ }
2676
+ catch { /* */ }
2677
+ files.push({
2678
+ path: full,
2679
+ relPath: rel,
2680
+ title,
2681
+ folder,
2682
+ agentSlug,
2683
+ mtime: new Date(stat.mtimeMs).toISOString(),
2684
+ sizeBytes: stat.size,
2685
+ type: typeTag,
2686
+ });
2687
+ }
2688
+ }
2689
+ walk(vaultRoot);
2690
+ let filtered = files
2691
+ .sort((a, b) => b.mtime.localeCompare(a.mtime))
2692
+ .filter(f => {
2693
+ if (agentFilter === '__shared__' && f.agentSlug != null)
2694
+ return false;
2695
+ if (agentFilter && agentFilter !== '__shared__' && f.agentSlug !== agentFilter)
2696
+ return false;
2697
+ if (folderFilter && f.folder !== folderFilter)
2698
+ return false;
2699
+ if (search) {
2700
+ const hay = (f.title + ' ' + f.relPath).toLowerCase();
2701
+ if (!hay.includes(search))
2702
+ return false;
2703
+ }
2704
+ return true;
2705
+ })
2706
+ .slice(0, limit);
2707
+ // Compute folder counts for filter chips
2708
+ const folderCounts = {};
2709
+ for (const f of files)
2710
+ folderCounts[f.folder] = (folderCounts[f.folder] || 0) + 1;
2711
+ res.json({ files: filtered, total: files.length, folderCounts });
2712
+ }
2713
+ catch (err) {
2714
+ res.status(500).json({ error: String(err) });
2715
+ }
2716
+ });
2717
+ app.get('/api/vault-file', async (req, res) => {
2718
+ try {
2719
+ const relPath = typeof req.query.path === 'string' ? req.query.path : '';
2720
+ if (!relPath || relPath.includes('..')) {
2721
+ res.status(400).json({ error: 'Bad path' });
2722
+ return;
2723
+ }
2724
+ const full = path.join(BASE_DIR, 'vault', relPath);
2725
+ if (!existsSync(full)) {
2726
+ res.status(404).json({ error: 'Not found' });
2727
+ return;
2728
+ }
2729
+ const content = readFileSync(full, 'utf-8');
2730
+ res.json({ path: relPath, content });
2731
+ }
2732
+ catch (err) {
2733
+ res.status(500).json({ error: String(err) });
2734
+ }
2735
+ });
2243
2736
  app.get('/api/memory', async (_req, res) => {
2244
2737
  res.json(await getMemory());
2245
2738
  });
@@ -8212,6 +8705,22 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
8212
8705
  --motion-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
8213
8706
  /* ── Focus ring ── */
8214
8707
  --ring: 0 0 0 2px var(--clementine), 0 0 0 4px rgba(255, 140, 33, 0.18);
8708
+ /* ── 0-10 numerical bg/text scales (additive — existing semantic tokens still work) ── */
8709
+ --bg-0: #ffffff;
8710
+ --bg-1: #fcfdfe;
8711
+ --bg-2: #f8fafc;
8712
+ --bg-3: #f1f5f9;
8713
+ --bg-4: #e2e8f0;
8714
+ --bg-5: #cbd5e1;
8715
+ --bg-6: #94a3b8;
8716
+ --bg-7: #64748b;
8717
+ --bg-8: #475569;
8718
+ --bg-9: #1e293b;
8719
+ --bg-10: #0f172a;
8720
+ --text-1: #0f172a;
8721
+ --text-2: #334155;
8722
+ --text-3: #64748b;
8723
+ --text-4: #94a3b8;
8215
8724
  }
8216
8725
  [data-theme="dark"] {
8217
8726
  --bg-primary: #0b0f17;
@@ -8243,6 +8752,22 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
8243
8752
  --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5), 0 2px 4px rgba(0, 0, 0, 0.3);
8244
8753
  --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.6), 0 4px 8px rgba(0, 0, 0, 0.4);
8245
8754
  --ring: 0 0 0 2px var(--clementine), 0 0 0 4px rgba(255, 165, 79, 0.22);
8755
+ /* Dark variants of the 0-10 scale */
8756
+ --bg-0: #0b0f17;
8757
+ --bg-1: #0f1421;
8758
+ --bg-2: #131923;
8759
+ --bg-3: #1a212d;
8760
+ --bg-4: #1f2733;
8761
+ --bg-5: #2d3748;
8762
+ --bg-6: #475569;
8763
+ --bg-7: #64748b;
8764
+ --bg-8: #94a3b8;
8765
+ --bg-9: #cbd5e1;
8766
+ --bg-10: #f1f5f9;
8767
+ --text-1: #e2e8f0;
8768
+ --text-2: #cbd5e1;
8769
+ --text-3: #94a3b8;
8770
+ --text-4: #64748b;
8246
8771
  }
8247
8772
  /* OS-preference dark mode unless user has explicitly chosen a theme */
8248
8773
  @media (prefers-color-scheme: dark) {
@@ -8282,6 +8807,17 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
8282
8807
  box-shadow: var(--ring);
8283
8808
  border-radius: var(--radius-xs);
8284
8809
  }
8810
+ /* Honor user's reduced-motion preference: kill every animation/transition. */
8811
+ @media (prefers-reduced-motion: reduce) {
8812
+ *,
8813
+ *::before,
8814
+ *::after {
8815
+ animation-duration: 0.001ms !important;
8816
+ animation-iteration-count: 1 !important;
8817
+ transition-duration: 0.001ms !important;
8818
+ scroll-behavior: auto !important;
8819
+ }
8820
+ }
8285
8821
  body {
8286
8822
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
8287
8823
  font-feature-settings: 'cv02', 'cv11', 'ss01', 'ss03';
@@ -8653,66 +9189,279 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
8653
9189
  .icn-lg { width: 22px; height: 22px; stroke-width: 1.75; }
8654
9190
  .icn-xl { width: 28px; height: 28px; stroke-width: 1.5; }
8655
9191
 
8656
- /* ── Home layout — chat-first daily driver ─── */
8657
- .home-layout {
9192
+ /* ── Home (v1.5 daily-driver dashboard) ─────── */
9193
+ .home-shell {
9194
+ max-width: 1180px;
9195
+ margin: 0 auto;
9196
+ padding: 24px 28px 120px;
9197
+ display: flex;
9198
+ flex-direction: column;
9199
+ gap: 22px;
9200
+ }
9201
+ .home-greeting {
9202
+ display: flex;
9203
+ align-items: flex-end;
9204
+ justify-content: space-between;
9205
+ gap: 16px;
9206
+ margin-top: 4px;
9207
+ }
9208
+ .home-greeting-text h1 {
9209
+ font-size: 26px;
9210
+ font-weight: 600;
9211
+ letter-spacing: -0.025em;
9212
+ color: var(--text-primary);
9213
+ margin: 0;
9214
+ line-height: 1.15;
9215
+ }
9216
+ .home-greeting-day {
9217
+ font-size: var(--text-base);
9218
+ color: var(--text-muted);
9219
+ margin: 4px 0 0;
9220
+ }
9221
+ .home-greeting-actions { display: flex; gap: 8px; }
9222
+
9223
+ /* KPI strip */
9224
+ .kpi-strip {
8658
9225
  display: grid;
8659
- grid-template-columns: 1fr 320px;
8660
- gap: 18px;
8661
- height: calc(100vh - var(--header-h));
8662
- padding: 18px;
8663
- box-sizing: border-box;
9226
+ grid-template-columns: repeat(5, minmax(0, 1fr));
9227
+ gap: 10px;
8664
9228
  }
8665
- .home-main {
9229
+ .kpi-tile {
9230
+ background: var(--bg-card);
9231
+ border: 1px solid var(--border);
9232
+ border-radius: var(--radius-md);
9233
+ padding: 12px 14px;
8666
9234
  display: flex;
8667
9235
  flex-direction: column;
9236
+ gap: 4px;
9237
+ transition: background var(--motion), border-color var(--motion), transform var(--motion-fast);
9238
+ position: relative;
9239
+ min-width: 0;
9240
+ }
9241
+ .kpi-tile[onclick] { cursor: pointer; }
9242
+ .kpi-tile[onclick]:hover { background: var(--bg-hover); border-color: var(--clementine); transform: translateY(-1px); }
9243
+ .kpi-tile .kpi-icon {
9244
+ width: 22px;
9245
+ height: 22px;
9246
+ display: inline-flex;
9247
+ align-items: center;
9248
+ justify-content: center;
9249
+ color: var(--text-muted);
9250
+ margin-bottom: 2px;
9251
+ }
9252
+ .kpi-tile .kpi-icon .icn { width: 16px; height: 16px; }
9253
+ .kpi-tile .kpi-value {
9254
+ font-size: 22px;
9255
+ font-weight: 600;
9256
+ color: var(--text-primary);
9257
+ line-height: 1;
9258
+ letter-spacing: -0.01em;
9259
+ }
9260
+ .kpi-tile .kpi-value.muted { color: var(--text-muted); font-weight: 500; }
9261
+ .kpi-tile .kpi-label {
9262
+ font-size: var(--text-xs);
9263
+ color: var(--text-muted);
9264
+ text-transform: uppercase;
9265
+ letter-spacing: 0.05em;
9266
+ font-weight: 500;
9267
+ }
9268
+ .kpi-tile.alert .kpi-value { color: var(--clementine); }
9269
+ .kpi-tile.alert::before {
9270
+ content: '';
9271
+ position: absolute;
9272
+ top: 0; right: 0;
9273
+ width: 6px; height: 6px;
9274
+ border-radius: 50%;
9275
+ background: var(--clementine);
9276
+ transform: translate(50%, -50%);
9277
+ box-shadow: 0 0 0 3px var(--bg-primary);
9278
+ }
9279
+
9280
+ /* Two-column home grid (briefing + today's runs) */
9281
+ .home-grid-2 {
9282
+ display: grid;
9283
+ grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
8668
9284
  gap: 14px;
8669
- min-height: 0;
8670
- overflow-y: auto;
9285
+ align-items: start;
9286
+ }
9287
+ .briefing-card .card-body { padding: 14px 18px; line-height: 1.55; }
9288
+ .briefing-section-title {
9289
+ font-size: var(--text-xs);
9290
+ text-transform: uppercase;
9291
+ letter-spacing: 0.06em;
9292
+ color: var(--text-muted);
9293
+ font-weight: 600;
9294
+ margin: 4px 0 8px;
8671
9295
  }
8672
- .home-chat {
9296
+ .briefing-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 8px; }
9297
+ .briefing-item {
8673
9298
  display: flex;
8674
- flex-direction: column;
8675
- flex: 1;
8676
- min-height: 320px;
8677
- background: var(--bg-card);
9299
+ align-items: flex-start;
9300
+ gap: 10px;
9301
+ font-size: var(--text-base);
9302
+ color: var(--text-primary);
9303
+ line-height: 1.45;
9304
+ }
9305
+ .briefing-item::before {
9306
+ content: '';
9307
+ width: 4px;
9308
+ height: 4px;
9309
+ border-radius: 50%;
9310
+ background: var(--clementine);
9311
+ margin-top: 8px;
9312
+ flex-shrink: 0;
9313
+ }
9314
+ .briefing-item.review {
9315
+ background: var(--clementine-bg);
9316
+ padding: 8px 10px;
9317
+ border-radius: var(--radius-sm);
9318
+ cursor: pointer;
9319
+ transition: background var(--motion);
9320
+ }
9321
+ .briefing-item.review::before { background: var(--clementine); }
9322
+ .briefing-item.review:hover { background: rgba(255,140,33,0.14); }
9323
+ .briefing-item.review .arrow { margin-left: auto; color: var(--clementine); flex-shrink: 0; }
9324
+
9325
+ .runs-card .card-body { padding: 8px 0; }
9326
+ .run-row {
9327
+ display: flex;
9328
+ align-items: center;
9329
+ gap: 10px;
9330
+ padding: 8px 16px;
9331
+ font-size: var(--text-base);
9332
+ border-left: 2px solid transparent;
9333
+ transition: background var(--motion), border-color var(--motion);
9334
+ }
9335
+ .run-row.fired {
9336
+ border-left-color: var(--green);
9337
+ color: var(--text-muted);
9338
+ }
9339
+ .run-row.upcoming { border-left-color: var(--clementine); }
9340
+ .run-row .run-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
9341
+ .run-row .run-time { font-size: var(--text-xs); color: var(--text-muted); flex-shrink: 0; }
9342
+ .run-row .run-icon { width: 14px; height: 14px; flex-shrink: 0; }
9343
+ .run-row.fired .run-icon { color: var(--green); }
9344
+ .run-row.upcoming .run-icon { color: var(--clementine); }
9345
+
9346
+ .home-activity { margin: 0; }
9347
+
9348
+ @media (max-width: 900px) {
9349
+ .kpi-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
9350
+ .home-grid-2 { grid-template-columns: 1fr; }
9351
+ .home-greeting-text h1 { font-size: 22px; }
9352
+ }
9353
+
9354
+ /* ── Floating chat FAB + panel ─────────────── */
9355
+ .home-chat-fab {
9356
+ position: fixed;
9357
+ right: 22px;
9358
+ bottom: 22px;
9359
+ z-index: 200;
9360
+ background: var(--clementine);
9361
+ color: #fff;
9362
+ border: none;
9363
+ border-radius: 999px;
9364
+ padding: 12px 18px;
9365
+ display: inline-flex;
9366
+ align-items: center;
9367
+ gap: 8px;
9368
+ font-size: var(--text-base);
9369
+ font-weight: 500;
9370
+ box-shadow: var(--shadow-md);
9371
+ cursor: pointer;
9372
+ transition: transform var(--motion-fast), box-shadow var(--motion);
9373
+ }
9374
+ .home-chat-fab:hover { transform: translateY(-1px); box-shadow: var(--shadow-lg); }
9375
+ .home-chat-fab .icn { width: 16px; height: 16px; }
9376
+ .home-chat-fab-label { white-space: nowrap; }
9377
+ .home-chat-fab.compact .home-chat-fab-label { display: none; }
9378
+ .home-chat-fab.hidden { display: none; }
9379
+
9380
+ .home-chat-panel {
9381
+ position: fixed;
9382
+ right: 22px;
9383
+ bottom: 22px;
9384
+ z-index: 199;
9385
+ width: 420px;
9386
+ max-width: calc(100vw - 44px);
9387
+ height: 560px;
9388
+ max-height: calc(100vh - var(--header-h) - 44px);
9389
+ background: var(--bg-secondary);
8678
9390
  border: 1px solid var(--border);
8679
- border-radius: 12px;
9391
+ border-radius: var(--radius-lg);
9392
+ box-shadow: var(--shadow-lg);
9393
+ display: flex;
9394
+ flex-direction: column;
8680
9395
  overflow: hidden;
8681
- box-shadow: 0 2px 8px rgba(0,0,0,0.06);
9396
+ transform: translateY(20px) scale(0.96);
9397
+ opacity: 0;
9398
+ pointer-events: none;
9399
+ transition: transform var(--motion), opacity var(--motion);
8682
9400
  }
8683
- .home-chat-messages {
9401
+ .home-chat-panel.open {
9402
+ transform: translateY(0) scale(1);
9403
+ opacity: 1;
9404
+ pointer-events: auto;
9405
+ }
9406
+ .home-chat-panel-header {
9407
+ display: flex;
9408
+ align-items: center;
9409
+ gap: 8px;
9410
+ padding: 12px 14px;
9411
+ border-bottom: 1px solid var(--border);
9412
+ font-size: var(--text-base);
9413
+ font-weight: 600;
9414
+ background: var(--bg-secondary);
9415
+ }
9416
+ .home-chat-panel-header .icn { width: 16px; height: 16px; color: var(--clementine); }
9417
+ .home-chat-panel-header select {
9418
+ padding: 4px 8px;
9419
+ font-size: var(--text-xs);
9420
+ border: 1px solid var(--border);
9421
+ border-radius: var(--radius-xs);
9422
+ background: var(--bg-input);
9423
+ color: var(--text-primary);
9424
+ }
9425
+ .home-chat-panel-messages {
8684
9426
  flex: 1;
8685
9427
  overflow-y: auto;
8686
- padding: 18px 20px;
8687
- min-height: 280px;
9428
+ padding: 14px 16px;
9429
+ background: var(--bg-primary);
8688
9430
  }
8689
- .home-chat-input-row {
9431
+ .home-chat-panel-input-row {
8690
9432
  display: flex;
8691
9433
  gap: 8px;
8692
- padding: 12px 16px;
9434
+ padding: 10px 12px;
8693
9435
  border-top: 1px solid var(--border);
8694
9436
  background: var(--bg-secondary);
8695
9437
  align-items: center;
8696
9438
  }
8697
- .home-chat-input-row input[type="text"] {
9439
+ .home-chat-panel-input-row input[type="text"] {
8698
9440
  flex: 1;
8699
- padding: 10px 14px;
9441
+ padding: 8px 12px;
8700
9442
  border: 1px solid var(--border);
8701
- border-radius: 8px;
9443
+ border-radius: var(--radius-sm);
8702
9444
  background: var(--bg-input);
8703
9445
  color: var(--text-primary);
8704
- font-size: 14px;
9446
+ font-size: var(--text-base);
9447
+ font-family: inherit;
8705
9448
  }
8706
- .home-chat-input-row select {
8707
- padding: 6px 10px;
8708
- border: 1px solid var(--border);
8709
- border-radius: 6px;
8710
- background: var(--bg-secondary);
8711
- color: var(--text-primary);
8712
- font-size: 12px;
9449
+ .home-chat-panel-input-row .btn-icon { padding: 6px; }
9450
+ .home-chat-panel-input-row .icn { width: 14px; height: 14px; }
9451
+
9452
+ @media (max-width: 600px) {
9453
+ .home-chat-panel {
9454
+ right: 8px;
9455
+ bottom: 8px;
9456
+ width: calc(100vw - 16px);
9457
+ height: calc(100vh - var(--header-h) - 16px);
9458
+ }
9459
+ .home-chat-fab {
9460
+ right: 12px;
9461
+ bottom: 12px;
9462
+ padding: 10px 14px;
9463
+ }
8713
9464
  }
8714
- .home-chat-input-row button { padding: 9px 18px; border-radius: 8px; }
8715
- .home-activity { margin: 0; }
8716
9465
 
8717
9466
  /* Right rail */
8718
9467
  .home-rail {
@@ -10610,6 +11359,26 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
10610
11359
  /* Hide chat profile selector when default — the row gets cleaner */
10611
11360
  .home-chat-input-row .chat-profile-spacer { display: none; }
10612
11361
 
11362
+ /* Vault file folder chips */
11363
+ .vault-folder-chip {
11364
+ padding: 4px 12px;
11365
+ border: 1px solid var(--border);
11366
+ border-radius: 16px;
11367
+ font-size: var(--text-sm);
11368
+ color: var(--text-secondary);
11369
+ cursor: pointer;
11370
+ transition: all var(--motion);
11371
+ background: var(--bg-secondary);
11372
+ user-select: none;
11373
+ }
11374
+ .vault-folder-chip:hover { background: var(--bg-hover); color: var(--text-primary); }
11375
+ .vault-folder-chip.active {
11376
+ background: var(--clementine-bg);
11377
+ color: var(--clementine);
11378
+ border-color: var(--clementine);
11379
+ font-weight: 500;
11380
+ }
11381
+
10613
11382
  /* ── Task Cards ─────────────────────────── */
10614
11383
  .task-grid {
10615
11384
  display: grid;
@@ -11372,11 +12141,12 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
11372
12141
  function run() {
11373
12142
  document.querySelectorAll('[data-icon]').forEach(function(el) {
11374
12143
  if (el.dataset._iconHydrated) return;
12144
+ if (!window.LUCIDE || !window.lucide) return;
11375
12145
  el.dataset._iconHydrated = '1';
11376
12146
  var name = el.getAttribute('data-icon');
11377
- if (!window.LUCIDE || !window.lucide) return;
11378
12147
  var slot = el.querySelector('.nav-icon, .hire-plus, .icon-slot');
11379
- if (slot) slot.innerHTML = window.lucide(name, 'icn-md');
12148
+ var target = slot || el;
12149
+ target.innerHTML = window.lucide(name, 'icn-md');
11380
12150
  });
11381
12151
  }
11382
12152
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run);
@@ -11392,143 +12162,140 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
11392
12162
  <!-- Content -->
11393
12163
  <div class="content">
11394
12164
 
11395
- <!-- ═══ Home — chat-first daily driver ═══ -->
12165
+ <!-- ═══ Home — daily-driver dashboard ═══ -->
11396
12166
  <div class="page active" id="page-home">
11397
- <div class="home-layout">
11398
- <main class="home-main">
11399
- <!-- Getting Started (shown for new users only) -->
11400
- <div id="getting-started" class="getting-started" style="display:none">
11401
- <div class="gs-header">
11402
- <div class="gs-title">Get Started</div>
11403
- <div class="gs-subtitle">Set up your AI assistant in 5 steps</div>
11404
- <button class="btn-ghost btn-sm gs-dismiss" onclick="dismissGettingStarted()" title="Dismiss">&times;</button>
12167
+ <div class="home-shell">
12168
+ <!-- Getting Started (shown for new users only) -->
12169
+ <div id="getting-started" class="getting-started" style="display:none">
12170
+ <div class="gs-header">
12171
+ <div class="gs-title">Get Started</div>
12172
+ <div class="gs-subtitle">Set up your AI assistant in 5 steps</div>
12173
+ <button class="btn-ghost btn-sm gs-dismiss" onclick="dismissGettingStarted()" title="Dismiss">&times;</button>
12174
+ </div>
12175
+ <div class="gs-grid">
12176
+ <div class="gs-card" id="gs-step-auth">
12177
+ <div class="gs-step-num">1</div>
12178
+ <div class="gs-card-icon">&#128274;</div>
12179
+ <div class="gs-card-title">Login with Anthropic</div>
12180
+ <div class="gs-card-desc" id="gs-auth-desc">Connect your Anthropic account to power your agents.</div>
12181
+ <button class="btn btn-sm btn-primary" id="gs-auth-btn" onclick="startAnthropicOAuth()">Login with Claude</button>
11405
12182
  </div>
11406
- <div class="gs-grid">
11407
- <div class="gs-card" id="gs-step-auth">
11408
- <div class="gs-step-num">1</div>
11409
- <div class="gs-card-icon">&#128274;</div>
11410
- <div class="gs-card-title">Login with Anthropic</div>
11411
- <div class="gs-card-desc" id="gs-auth-desc">Connect your Anthropic account to power your agents.</div>
11412
- <button class="btn btn-sm btn-primary" id="gs-auth-btn" onclick="startAnthropicOAuth()">Login with Claude</button>
11413
- </div>
11414
- <div class="gs-card" id="gs-step-agent">
11415
- <div class="gs-step-num">2</div>
11416
- <div class="gs-card-icon">&#128101;</div>
11417
- <div class="gs-card-title">Create an Agent</div>
11418
- <div class="gs-card-desc">Hire your first AI team member with a role, tools, and a channel.</div>
11419
- <button class="btn btn-sm btn-primary" onclick="navigateTo('team')">Go to The Office</button>
11420
- </div>
11421
- <div class="gs-card" id="gs-step-channel">
11422
- <div class="gs-step-num">3</div>
11423
- <div class="gs-card-icon">&#128172;</div>
11424
- <div class="gs-card-title">Connect a Channel</div>
11425
- <div class="gs-card-desc">Link Discord or Slack so your agents can communicate.</div>
11426
- <button class="btn btn-sm" onclick="navigateTo('settings', { tab: 'channels' })">Open Settings</button>
11427
- </div>
11428
- <div class="gs-card" id="gs-step-task">
11429
- <div class="gs-step-num">4</div>
11430
- <div class="gs-card-icon">&#9200;</div>
11431
- <div class="gs-card-title">Schedule a Task</div>
11432
- <div class="gs-card-desc">Set up cron jobs so agents work on autopilot.</div>
11433
- <button class="btn btn-sm" onclick="navigateTo('build', { tab: 'crons' })">Add a Task</button>
11434
- </div>
11435
- <div class="gs-card" id="gs-step-project">
11436
- <div class="gs-step-num">5</div>
11437
- <div class="gs-card-icon">&#128194;</div>
11438
- <div class="gs-card-title">Link a Project</div>
11439
- <div class="gs-card-desc">Give agents context about your codebases and tools.</div>
11440
- <button class="btn btn-sm" onclick="navigateTo('settings', { tab: 'projects' })">Browse Projects</button>
11441
- </div>
12183
+ <div class="gs-card" id="gs-step-agent">
12184
+ <div class="gs-step-num">2</div>
12185
+ <div class="gs-card-icon">&#128101;</div>
12186
+ <div class="gs-card-title">Create an Agent</div>
12187
+ <div class="gs-card-desc">Hire your first AI team member with a role, tools, and a channel.</div>
12188
+ <button class="btn btn-sm btn-primary" onclick="navigateTo('team')">Go to The Office</button>
11442
12189
  </div>
11443
- </div>
11444
-
11445
- <!-- Chat panel — primary surface -->
11446
- <div class="home-chat">
11447
- <div id="chat-messages" class="home-chat-messages">
11448
- <div class="empty-state" style="margin-top:32px">
11449
- <p style="margin-bottom:14px;color:var(--text-muted)">What can I help with?</p>
11450
- <div style="display:flex;flex-wrap:wrap;gap:8px;justify-content:center">
11451
- <button class="btn btn-sm quick-pill" onclick="quickChat(&quot;What&apos;s on my schedule?&quot;)">What's on my schedule?</button>
11452
- <button class="btn btn-sm quick-pill" onclick="quickChat('Check my email')">Check my email</button>
11453
- <button class="btn btn-sm quick-pill" onclick="quickChat('Run morning briefing')">Run morning briefing</button>
11454
- <button class="btn btn-sm quick-pill" onclick="quickChat('What did you do today?')">What did you do today?</button>
11455
- </div>
11456
- </div>
12190
+ <div class="gs-card" id="gs-step-channel">
12191
+ <div class="gs-step-num">3</div>
12192
+ <div class="gs-card-icon">&#128172;</div>
12193
+ <div class="gs-card-title">Connect a Channel</div>
12194
+ <div class="gs-card-desc">Link Discord or Slack so your agents can communicate.</div>
12195
+ <button class="btn btn-sm" onclick="navigateTo('settings', { tab: 'channels' })">Open Settings</button>
11457
12196
  </div>
11458
- <div class="home-chat-input-row">
11459
- <input type="text" id="chat-input" placeholder="Ask Clementine anything..." onkeydown="if(event.key==='Enter'&amp;&amp;!event.shiftKey){event.preventDefault();sendChat()}">
11460
- <select id="chat-profile-select" onchange="switchProfile(this.value)" title="Active profile">
11461
- <option value="">Default</option>
11462
- </select>
11463
- <button class="btn-primary" id="chat-send-btn" onclick="sendChat()">Send</button>
12197
+ <div class="gs-card" id="gs-step-task">
12198
+ <div class="gs-step-num">4</div>
12199
+ <div class="gs-card-icon">&#9200;</div>
12200
+ <div class="gs-card-title">Schedule a Task</div>
12201
+ <div class="gs-card-desc">Set up cron jobs so agents work on autopilot.</div>
12202
+ <button class="btn btn-sm" onclick="navigateTo('build', { tab: 'crons' })">Add a Task</button>
12203
+ </div>
12204
+ <div class="gs-card" id="gs-step-project">
12205
+ <div class="gs-step-num">5</div>
12206
+ <div class="gs-card-icon">&#128194;</div>
12207
+ <div class="gs-card-title">Link a Project</div>
12208
+ <div class="gs-card-desc">Give agents context about your codebases and tools.</div>
12209
+ <button class="btn btn-sm" onclick="navigateTo('settings', { tab: 'projects' })">Browse Projects</button>
11464
12210
  </div>
11465
12211
  </div>
12212
+ </div>
11466
12213
 
11467
- <!-- Activity feed -->
11468
- <div class="card home-activity">
11469
- <div class="card-header" style="display:flex;justify-content:space-between;align-items:center">
11470
- <span>Live activity</span>
11471
- <div style="display:flex;gap:6px;align-items:center">
11472
- <select id="activity-source-filter" onchange="refreshActivity()" style="font-size:11px;padding:2px 6px;border:1px solid var(--border);border-radius:4px;background:var(--bg-secondary);color:var(--text-primary)">
11473
- <option value="">All Sources</option>
11474
- <option value="cron">Cron</option>
11475
- <option value="activity">Activities</option>
11476
- <option value="send">Emails</option>
11477
- <option value="approval">Approvals</option>
11478
- <option value="memory">Memory</option>
11479
- </select>
11480
- <select id="activity-agent-filter" onchange="refreshActivity()" style="font-size:11px;padding:2px 6px;border:1px solid var(--border);border-radius:4px;background:var(--bg-secondary);color:var(--text-primary)">
11481
- <option value="">All Agents</option>
11482
- </select>
11483
- </div>
12214
+ <!-- Greeting -->
12215
+ <header class="home-greeting">
12216
+ <div class="home-greeting-text">
12217
+ <h1 id="home-greet-line"><span id="home-greet-phrase">Hello</span><span id="home-greet-comma">,</span> <span id="home-greet-name">there</span></h1>
12218
+ <p id="home-greet-day" class="home-greeting-day">…</p>
12219
+ </div>
12220
+ <div class="home-greeting-actions">
12221
+ <button class="btn-sm btn-ghost" onclick="refreshHomeDigest()" title="Refresh dashboard">
12222
+ <span class="icon-slot" data-icon="refresh"></span>
12223
+ </button>
12224
+ </div>
12225
+ </header>
12226
+
12227
+ <!-- KPI strip -->
12228
+ <section class="kpi-strip" id="home-kpis">
12229
+ <div class="kpi-tile" data-kpi="activeRuns" onclick="navigateTo('build',{tab:'workflows'})">
12230
+ <div class="kpi-icon" data-icon="zap"></div>
12231
+ <div class="kpi-value" id="kpi-active-runs">--</div>
12232
+ <div class="kpi-label">Active runs</div>
12233
+ </div>
12234
+ <div class="kpi-tile" data-kpi="runsToday">
12235
+ <div class="kpi-icon" data-icon="clock"></div>
12236
+ <div class="kpi-value" id="kpi-runs-today">--</div>
12237
+ <div class="kpi-label">Runs today</div>
12238
+ </div>
12239
+ <div class="kpi-tile" data-kpi="timeSaved">
12240
+ <div class="kpi-icon" data-icon="sparkles"></div>
12241
+ <div class="kpi-value" id="kpi-time-saved">--</div>
12242
+ <div class="kpi-label">Saved this week</div>
12243
+ </div>
12244
+ <div class="kpi-tile" data-kpi="approvals" onclick="navigateTo('brain',{tab:'learning'})">
12245
+ <div class="kpi-icon" data-icon="check"></div>
12246
+ <div class="kpi-value" id="kpi-approvals">--</div>
12247
+ <div class="kpi-label">Need approval</div>
12248
+ </div>
12249
+ <div class="kpi-tile" data-kpi="overdue" onclick="navigateTo('team',{tab:'goals'})">
12250
+ <div class="kpi-icon" data-icon="target"></div>
12251
+ <div class="kpi-value" id="kpi-overdue">--</div>
12252
+ <div class="kpi-label">Overdue</div>
12253
+ </div>
12254
+ </section>
12255
+
12256
+ <!-- Today's briefing + Today's runs side-by-side -->
12257
+ <section class="home-grid-2">
12258
+ <div class="card briefing-card">
12259
+ <div class="card-header">
12260
+ <span><span class="icon-slot" data-icon="fileText"></span> Today's briefing</span>
12261
+ <span style="font-size:var(--text-xs);color:var(--text-muted)" id="briefing-source-hint"></span>
12262
+ </div>
12263
+ <div class="card-body" id="briefing-body">
12264
+ <div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>
11484
12265
  </div>
11485
- <div class="card-body" id="panel-activity"><div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div></div>
11486
12266
  </div>
11487
- </main>
11488
-
11489
- <!-- Right rail: Today / Upcoming / Active / Time-saved / Approvals -->
11490
- <aside class="home-rail" id="home-rail">
11491
- <button class="rail-collapse-btn" onclick="toggleHomeRail()" title="Hide rail">&times;</button>
11492
12267
 
11493
- <section class="rail-card">
11494
- <div class="rail-header">
11495
- <span><span class="status-pip green"></span>Daemon</span>
11496
- <span id="rail-daemon-uptime" style="font-size:11px;color:var(--text-muted)">--</span>
12268
+ <div class="card runs-card">
12269
+ <div class="card-header">
12270
+ <span><span class="icon-slot" data-icon="clock"></span> Today's runs</span>
12271
+ <button class="btn-sm btn-ghost" onclick="navigateTo('build',{tab:'crons'})" style="font-size:var(--text-xs);padding:2px 8px">View all</button>
11497
12272
  </div>
11498
- <div class="rail-body" id="rail-daemon-body">
11499
- <div class="agent-activity" id="agent-activity"><span class="agent-activity-dot"></span><span>Loading...</span></div>
12273
+ <div class="card-body" id="today-runs-body">
12274
+ <div class="skel-block"><div class="skel-row med"></div><div class="skel-row short"></div></div>
11500
12275
  </div>
11501
- </section>
11502
-
11503
- <section class="rail-card">
11504
- <div class="rail-header">
11505
- <span>Today</span>
11506
- <input type="date" id="plan-date-picker" style="padding:2px 6px;font-size:10px;background:var(--bg-input);border:1px solid var(--border);border-radius:3px;color:var(--text-primary)">
12276
+ </div>
12277
+ </section>
12278
+
12279
+ <!-- Recent activity -->
12280
+ <section class="card home-activity">
12281
+ <div class="card-header" style="display:flex;justify-content:space-between;align-items:center">
12282
+ <span><span class="icon-slot" data-icon="list"></span> Recent activity</span>
12283
+ <div style="display:flex;gap:6px;align-items:center">
12284
+ <select id="activity-source-filter" onchange="refreshActivity()" style="font-size:var(--text-xs);padding:3px 8px;border:1px solid var(--border);border-radius:var(--radius-xs);background:var(--bg-secondary);color:var(--text-primary)">
12285
+ <option value="">All sources</option>
12286
+ <option value="cron">Cron</option>
12287
+ <option value="activity">Activities</option>
12288
+ <option value="send">Emails</option>
12289
+ <option value="approval">Approvals</option>
12290
+ <option value="memory">Memory</option>
12291
+ </select>
12292
+ <select id="activity-agent-filter" onchange="refreshActivity()" style="font-size:var(--text-xs);padding:3px 8px;border:1px solid var(--border);border-radius:var(--radius-xs);background:var(--bg-secondary);color:var(--text-primary)">
12293
+ <option value="">All agents</option>
12294
+ </select>
11507
12295
  </div>
11508
- <div class="rail-body" id="home-plan-content"><div class="skel-row med"></div><div class="skel-row"></div></div>
11509
- </section>
11510
-
11511
- <section class="rail-card">
11512
- <div class="rail-header"><span>Upcoming runs</span><span id="rail-upcoming-count" class="rail-badge">0</span></div>
11513
- <div class="rail-body" id="rail-upcoming"><div class="skel-row short"></div></div>
11514
- </section>
11515
-
11516
- <section class="rail-card">
11517
- <div class="rail-header"><span>Active runs</span><span id="rail-active-count" class="rail-badge" style="display:none">0</span></div>
11518
- <div class="rail-body" id="rail-active"><div style="font-size:12px;color:var(--text-muted)">Nothing running.</div></div>
11519
- </section>
11520
-
11521
- <section class="rail-card">
11522
- <div class="rail-header"><span>Time saved this week</span></div>
11523
- <div class="rail-body" id="rail-time-saved"><div class="skel-row short"></div></div>
11524
- </section>
11525
-
11526
- <section class="rail-card">
11527
- <div class="rail-header"><span>Approvals</span><span id="rail-approvals-count" class="rail-badge" style="display:none">0</span></div>
11528
- <div class="rail-body" id="rail-approvals"><div style="font-size:12px;color:var(--text-muted)">Nothing pending.</div></div>
11529
- </section>
11530
- </aside>
11531
- <button class="home-rail-toggle" onclick="toggleHomeRail()" title="Open status rail">&#9776;</button>
12296
+ </div>
12297
+ <div class="card-body" id="panel-activity"><div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div></div>
12298
+ </section>
11532
12299
  </div>
11533
12300
  </div>
11534
12301
 
@@ -11765,6 +12532,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
11765
12532
  <div class="tab-bar" id="intelligence-tabs" style="margin:0 0 0 18px">
11766
12533
  <button class="active" data-icon="database" onclick="switchTab('intelligence','search')"><span class="icon-slot"></span> Memory</button>
11767
12534
  <button data-icon="sparkles" onclick="switchTab('intelligence','graph')"><span class="icon-slot"></span> Knowledge</button>
12535
+ <button data-icon="fileText" onclick="switchTab('intelligence','files')"><span class="icon-slot"></span> Files</button>
11768
12536
  <button data-icon="folder" onclick="switchTab('intelligence','sources')"><span class="icon-slot"></span> Ingestion</button>
11769
12537
  <button data-icon="zap" onclick="switchTab('intelligence','health')"><span class="icon-slot"></span> Health <span class="tab-badge" id="brain-health-badge" style="display:none;background:#ef4444;color:#fff">0</span></button>
11770
12538
  <button data-icon="users" onclick="switchTab('intelligence','user-model')"><span class="icon-slot"></span> User Model</button>
@@ -11973,6 +12741,26 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
11973
12741
  <div class="tab-pane" id="tab-intelligence-runs">
11974
12742
  <div id="brain-runs-list"></div>
11975
12743
  </div>
12744
+ <div class="tab-pane" id="tab-intelligence-files">
12745
+ <div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;flex-wrap:wrap">
12746
+ <input type="text" id="vault-files-search" placeholder="Search title or path..." style="flex:1;min-width:200px;padding:7px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-input);color:var(--text-primary);font-size:13px" oninput="refreshVaultFiles()">
12747
+ <select id="vault-files-agent-filter" onchange="refreshVaultFiles()" style="padding:7px 10px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-secondary);color:var(--text-primary);font-size:12px">
12748
+ <option value="">All authors</option>
12749
+ <option value="__shared__">Shared (vault root)</option>
12750
+ </select>
12751
+ <select id="vault-files-since" onchange="refreshVaultFiles()" style="padding:7px 10px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-secondary);color:var(--text-primary);font-size:12px">
12752
+ <option value="7">Past 7 days</option>
12753
+ <option value="30" selected>Past 30 days</option>
12754
+ <option value="90">Past 90 days</option>
12755
+ <option value="365">Past year</option>
12756
+ </select>
12757
+ <button class="btn-sm" onclick="refreshVaultFiles()">Refresh</button>
12758
+ </div>
12759
+ <div id="vault-files-folder-chips" style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px"></div>
12760
+ <div id="vault-files-list">
12761
+ <div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>
12762
+ </div>
12763
+ </div>
11976
12764
  <div class="tab-pane" id="tab-intelligence-health">
11977
12765
  <div style="display:flex;align-items:center;gap:8px;margin-bottom:14px;flex-wrap:wrap">
11978
12766
  <button class="btn-sm" onclick="memoryHealthAction('janitor')" title="Run the janitor cleanup pass now">Run cleanup</button>
@@ -12989,6 +13777,41 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
12989
13777
  </script>
12990
13778
  </div>
12991
13779
 
13780
+ <!-- Floating chat FAB + panel — anchored bottom-right.
13781
+ Pass 3 styles this; structure is here so existing chat handlers
13782
+ (sendChat, switchProfile, quickChat) keep finding the IDs they target. -->
13783
+ <button id="home-chat-fab" class="home-chat-fab" onclick="toggleHomeChat()" title="Ask Clementine (?)">
13784
+ <span class="icon-slot" data-icon="messageSquare"></span>
13785
+ <span class="home-chat-fab-label">Ask Clementine</span>
13786
+ </button>
13787
+ <div id="home-chat-panel" class="home-chat-panel" aria-hidden="true">
13788
+ <div class="home-chat-panel-header">
13789
+ <span><span class="icon-slot" data-icon="messageSquare"></span> Chat</span>
13790
+ <span style="flex:1"></span>
13791
+ <select id="chat-profile-select" onchange="switchProfile(this.value)" title="Active profile">
13792
+ <option value="">Default</option>
13793
+ </select>
13794
+ <button class="btn-icon btn-sm" onclick="toggleHomeChat()" title="Close chat">×</button>
13795
+ </div>
13796
+ <div id="chat-messages" class="home-chat-panel-messages">
13797
+ <div class="empty-state" style="margin-top:24px">
13798
+ <p style="margin-bottom:12px;color:var(--text-muted);font-size:var(--text-base)">What can I help with?</p>
13799
+ <div style="display:flex;flex-wrap:wrap;gap:6px;justify-content:center">
13800
+ <button class="btn btn-sm quick-pill" onclick="quickChat(&quot;What&apos;s on my schedule?&quot;)">Schedule?</button>
13801
+ <button class="btn btn-sm quick-pill" onclick="quickChat('Check my email')">Email</button>
13802
+ <button class="btn btn-sm quick-pill" onclick="quickChat('Run morning briefing')">Briefing</button>
13803
+ <button class="btn btn-sm quick-pill" onclick="quickChat('What did you do today?')">Today's work</button>
13804
+ </div>
13805
+ </div>
13806
+ </div>
13807
+ <div class="home-chat-panel-input-row">
13808
+ <input type="text" id="chat-input" placeholder="Ask anything..." onkeydown="if(event.key==='Enter'&amp;&amp;!event.shiftKey){event.preventDefault();sendChat()}">
13809
+ <button class="btn-primary btn-sm" id="chat-send-btn" onclick="sendChat()" title="Send">
13810
+ <span class="icon-slot" data-icon="send"></span>
13811
+ </button>
13812
+ </div>
13813
+ </div>
13814
+
12992
13815
  <!-- Sessions, Trust & Claims, Logs, Chat, Metrics, Daily Plan pages
12993
13816
  removed in Session 2 — content lives on Home or migrates to other
12994
13817
  destinations in Sessions 3-4. Page references for these IDs are
@@ -14182,6 +15005,7 @@ function navigateTo(page, opts) {
14182
15005
  switch (page) {
14183
15006
  case 'home':
14184
15007
  refreshAll();
15008
+ if (typeof refreshHomeDigest === 'function') refreshHomeDigest();
14185
15009
  // tab is a soft hint on Home (one cohesive layout): focus the relevant area.
14186
15010
  var t = opts.tab || 'chat';
14187
15011
  setTimeout(function() {
@@ -14565,6 +15389,7 @@ function switchTab(group, tab) {
14565
15389
  if (group === 'intelligence') {
14566
15390
  if (tab === 'graph') refreshGraph();
14567
15391
  if (tab === 'memory') refreshMemory();
15392
+ if (tab === 'files' && typeof refreshVaultFiles === 'function') refreshVaultFiles();
14568
15393
  if (tab === 'health') {
14569
15394
  if (typeof refreshMemoryHealth === 'function') refreshMemoryHealth();
14570
15395
  if (typeof refreshClaims === 'function') refreshClaims();
@@ -19816,6 +20641,134 @@ async function memoryHealthAction(action) {
19816
20641
  }
19817
20642
  }
19818
20643
 
20644
+ // ── Vault Files (Brain → Files tab) ──────────────────────────────
20645
+ var _vaultFilesCache = null;
20646
+ var _vaultFilesFolder = ''; // current folder filter
20647
+
20648
+ async function refreshVaultFiles() {
20649
+ var listEl = document.getElementById('vault-files-list');
20650
+ if (!listEl) return;
20651
+ var q = document.getElementById('vault-files-search')?.value || '';
20652
+ var agent = document.getElementById('vault-files-agent-filter')?.value || '';
20653
+ var since = document.getElementById('vault-files-since')?.value || '30';
20654
+ // Show skeleton while loading
20655
+ listEl.innerHTML = '<div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div>';
20656
+ try {
20657
+ var url = '/api/vault-files?sinceDays=' + encodeURIComponent(since)
20658
+ + (q ? '&q=' + encodeURIComponent(q) : '')
20659
+ + (agent ? '&agent=' + encodeURIComponent(agent) : '')
20660
+ + (_vaultFilesFolder ? '&folder=' + encodeURIComponent(_vaultFilesFolder) : '');
20661
+ var r = await apiFetch(url);
20662
+ var d = await r.json();
20663
+ var files = d.files || [];
20664
+ _vaultFilesCache = files;
20665
+ // Populate agent filter from ALL files response (use server's full set, not filtered)
20666
+ var agentSel = document.getElementById('vault-files-agent-filter');
20667
+ if (agentSel && agentSel.options.length <= 2) {
20668
+ var slugs = [...new Set(files.map(function(f) { return f.agentSlug; }).filter(Boolean))].sort();
20669
+ slugs.forEach(function(slug) {
20670
+ var opt = document.createElement('option');
20671
+ opt.value = slug;
20672
+ opt.textContent = slug;
20673
+ agentSel.appendChild(opt);
20674
+ });
20675
+ }
20676
+ // Render folder filter chips (using folderCounts from server)
20677
+ var chipsEl = document.getElementById('vault-files-folder-chips');
20678
+ if (chipsEl && d.folderCounts) {
20679
+ var folders = Object.entries(d.folderCounts).sort(function(a, b) { return b[1] - a[1]; });
20680
+ var totalCount = folders.reduce(function(s, p) { return s + p[1]; }, 0);
20681
+ var chipHtml = '<div class="vault-folder-chip' + (_vaultFilesFolder === '' ? ' active' : '') + '" data-folder="" onclick="setVaultFolderFilter(\\x27\\x27)">All <span style="opacity:0.6">' + totalCount + '</span></div>';
20682
+ folders.forEach(function(p) {
20683
+ var folder = p[0]; var count = p[1];
20684
+ if (!folder) return;
20685
+ chipHtml += '<div class="vault-folder-chip' + (_vaultFilesFolder === folder ? ' active' : '') + '" data-folder="' + esc(folder) + '" onclick="setVaultFolderFilter(\\x27' + esc(folder) + '\\x27)">' + esc(folder) + ' <span style="opacity:0.6">' + count + '</span></div>';
20686
+ });
20687
+ chipsEl.innerHTML = chipHtml;
20688
+ }
20689
+ if (files.length === 0) {
20690
+ listEl.innerHTML = '<div class="empty-cta"><div class="label">No recent files</div><div class="hint">Try a wider time window or different filter.</div></div>';
20691
+ return;
20692
+ }
20693
+ var html = '<div style="font-size:11px;color:var(--text-muted);margin-bottom:10px">Showing ' + files.length + ' of ' + d.total + ' files modified in the last ' + since + ' days.</div>';
20694
+ html += '<div style="display:flex;flex-direction:column;gap:1px;border:1px solid var(--border);border-radius:var(--radius-md);overflow:hidden;background:var(--bg-card)">';
20695
+ for (var i = 0; i < files.length; i++) {
20696
+ var f = files[i];
20697
+ var agentBadge = f.agentSlug
20698
+ ? '<span style="font-size:10px;background:var(--clementine-bg);color:var(--clementine);padding:2px 7px;border-radius:var(--radius-xs);font-weight:500">' + esc(f.agentSlug) + '</span>'
20699
+ : '<span style="font-size:10px;background:var(--bg-tertiary);color:var(--text-muted);padding:2px 7px;border-radius:var(--radius-xs)">shared</span>';
20700
+ var typeBadge = f.type ? '<span style="font-size:10px;color:var(--text-muted);margin-right:6px">' + esc(f.type) + '</span>' : '';
20701
+ html += '<div class="vault-file-row clickable-row" data-path="' + esc(f.relPath) + '" style="display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--bg-secondary);border-bottom:1px solid var(--border-light);font-size:13px">'
20702
+ + '<div style="flex:1;min-width:0">'
20703
+ + '<div style="font-weight:500;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + esc(f.title) + '</div>'
20704
+ + '<div style="font-size:11px;color:var(--text-muted);font-family:\\x27JetBrains Mono\\x27,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px">' + esc(f.relPath) + '</div>'
20705
+ + '</div>'
20706
+ + '<div style="display:flex;align-items:center;gap:8px;flex-shrink:0">'
20707
+ + typeBadge + agentBadge
20708
+ + '<span style="font-size:11px;color:var(--text-muted);min-width:60px;text-align:right">' + esc(timeAgo(f.mtime)) + '</span>'
20709
+ + '</div>'
20710
+ + '</div>';
20711
+ }
20712
+ html += '</div>';
20713
+ listEl.innerHTML = html;
20714
+ // Wire row clicks
20715
+ listEl.querySelectorAll('.vault-file-row').forEach(function(row) {
20716
+ row.onclick = function() { openVaultFile(row.getAttribute('data-path')); };
20717
+ });
20718
+ } catch (err) {
20719
+ listEl.innerHTML = '<div style="padding:24px;color:var(--red);font-size:13px">Failed to load: ' + esc(String(err)) + '</div>';
20720
+ }
20721
+ }
20722
+
20723
+ async function openVaultFile(relPath) {
20724
+ if (!relPath) return;
20725
+ // Build/reuse a slide-out drawer for content preview
20726
+ var drawer = document.getElementById('vault-file-drawer');
20727
+ if (!drawer) {
20728
+ drawer = document.createElement('div');
20729
+ drawer.id = 'vault-file-drawer';
20730
+ drawer.style.cssText = 'position:fixed;right:0;top:0;bottom:0;width:560px;max-width:92vw;background:var(--bg-secondary);border-left:1px solid var(--border);box-shadow:-8px 0 32px rgba(0,0,0,0.18);z-index:200;display:flex;flex-direction:column;transform:translateX(100%);transition:transform 200ms ease';
20731
+ drawer.innerHTML =
20732
+ '<div style="display:flex;align-items:center;gap:10px;padding:14px 18px;border-bottom:1px solid var(--border);flex-shrink:0">'
20733
+ + '<div style="flex:1;min-width:0">'
20734
+ + '<div id="vault-file-drawer-title" style="font-weight:600;font-size:15px;letter-spacing:-0.01em"></div>'
20735
+ + '<div id="vault-file-drawer-path" style="font-size:11px;color:var(--text-muted);font-family:\\x27JetBrains Mono\\x27,monospace;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></div>'
20736
+ + '</div>'
20737
+ + '<button class="btn-icon btn-sm" onclick="closeVaultFileDrawer()" title="Close">' + lucide('x', 'icn-sm') + '</button>'
20738
+ + '</div>'
20739
+ + '<div id="vault-file-drawer-body" style="flex:1;overflow-y:auto;padding:18px 22px;font-size:13px;line-height:1.55"></div>';
20740
+ document.body.appendChild(drawer);
20741
+ }
20742
+ var titleEl = document.getElementById('vault-file-drawer-title');
20743
+ var pathEl = document.getElementById('vault-file-drawer-path');
20744
+ var body = document.getElementById('vault-file-drawer-body');
20745
+ if (titleEl) titleEl.textContent = relPath.split('/').pop().replace(/\\.md$/, '');
20746
+ if (pathEl) pathEl.textContent = relPath;
20747
+ if (body) body.innerHTML = '<div class="skel-block"><div class="skel-row"></div><div class="skel-row med"></div><div class="skel-row short"></div></div>';
20748
+ drawer.style.transform = 'translateX(0)';
20749
+ try {
20750
+ var r = await apiFetch('/api/vault-file?path=' + encodeURIComponent(relPath));
20751
+ var d = await r.json();
20752
+ if (d.error) {
20753
+ body.innerHTML = '<div style="color:var(--red)">' + esc(d.error) + '</div>';
20754
+ return;
20755
+ }
20756
+ body.innerHTML = renderMd(d.content);
20757
+ } catch (err) {
20758
+ body.innerHTML = '<div style="color:var(--red)">Failed: ' + esc(String(err)) + '</div>';
20759
+ }
20760
+ }
20761
+
20762
+ function closeVaultFileDrawer() {
20763
+ var drawer = document.getElementById('vault-file-drawer');
20764
+ if (drawer) drawer.style.transform = 'translateX(100%)';
20765
+ }
20766
+
20767
+ function setVaultFolderFilter(folder) {
20768
+ _vaultFilesFolder = folder || '';
20769
+ refreshVaultFiles();
20770
+ }
20771
+
19819
20772
  // ── Goals: inline create form ────────────────────────────────────
19820
20773
  function openNewGoalForm() {
19821
20774
  var el = document.getElementById('new-goal-form');
@@ -20585,6 +21538,147 @@ async function saveDiscordSetup() {
20585
21538
  } catch(e) { toast(String(e), 'error'); }
20586
21539
  }
20587
21540
 
21541
+ // ── Home digest (v1.5 daily-driver) ────────────────────────────
21542
+ async function refreshHomeDigest() {
21543
+ try {
21544
+ var r = await apiFetch('/api/home-digest');
21545
+ var d = await r.json();
21546
+ if (!d || d.error) return;
21547
+
21548
+ // Greeting
21549
+ var greetPhraseEl = document.getElementById('home-greet-phrase');
21550
+ var greetNameEl = document.getElementById('home-greet-name');
21551
+ var greetDayEl = document.getElementById('home-greet-day');
21552
+ if (d.greeting) {
21553
+ if (greetPhraseEl) greetPhraseEl.textContent = d.greeting.phrase || 'Hello';
21554
+ if (greetNameEl) greetNameEl.textContent = d.greeting.name || 'there';
21555
+ if (greetDayEl) greetDayEl.textContent = d.greeting.dayLabel || '';
21556
+ }
21557
+
21558
+ // KPIs
21559
+ var k = d.kpis || {};
21560
+ var setKpi = function(id, value, alertWhen) {
21561
+ var el = document.getElementById(id);
21562
+ if (!el) return;
21563
+ el.textContent = (value == null) ? '0' : String(value);
21564
+ var tile = el.closest('.kpi-tile');
21565
+ if (tile) {
21566
+ tile.classList.toggle('alert', alertWhen);
21567
+ tile.classList.toggle('muted', !value);
21568
+ }
21569
+ el.classList.toggle('muted', !value);
21570
+ };
21571
+ setKpi('kpi-active-runs', k.activeRuns || 0, (k.activeRuns || 0) > 0);
21572
+ setKpi('kpi-runs-today', k.runsToday || 0, false);
21573
+ var minutes = k.timeSavedMinutes || 0;
21574
+ var savedEl = document.getElementById('kpi-time-saved');
21575
+ if (savedEl) {
21576
+ savedEl.textContent = minutes >= 60 ? (minutes / 60).toFixed(1) + 'h' : (minutes + 'm');
21577
+ savedEl.classList.toggle('muted', minutes === 0);
21578
+ }
21579
+ setKpi('kpi-approvals', k.pendingApprovals || 0, (k.pendingApprovals || 0) > 0);
21580
+ setKpi('kpi-overdue', k.overdueTasks || 0, (k.overdueTasks || 0) > 0);
21581
+
21582
+ // Briefing
21583
+ var briefBody = document.getElementById('briefing-body');
21584
+ var briefHint = document.getElementById('briefing-source-hint');
21585
+ if (briefBody) {
21586
+ var b = d.briefing || { highlights: [], needsReview: [] };
21587
+ var html = '';
21588
+ if (b.highlights.length) {
21589
+ html += '<div class="briefing-section-title">What\\x27s happened today</div>';
21590
+ html += '<ul class="briefing-list">';
21591
+ for (var i = 0; i < b.highlights.length; i++) {
21592
+ html += '<li class="briefing-item">' + esc(b.highlights[i].text) + '</li>';
21593
+ }
21594
+ html += '</ul>';
21595
+ } else {
21596
+ html += '<div style="color:var(--text-muted);font-size:var(--text-base);padding:6px 0">No notable activity in today\\x27s daily note yet. Your team\\x27s morning briefing fires at 8:00am.</div>';
21597
+ }
21598
+ if (b.needsReview.length) {
21599
+ html += '<div class="briefing-section-title" style="margin-top:18px">Needs your review</div>';
21600
+ html += '<ul class="briefing-list">';
21601
+ for (var j = 0; j < b.needsReview.length; j++) {
21602
+ var item = b.needsReview[j];
21603
+ var hrefAttr = item.href ? 'onclick="briefingNeedsReviewClick(\\x27' + esc(item.href) + '\\x27)"' : '';
21604
+ html += '<li class="briefing-item review" ' + hrefAttr + '>' + esc(item.text) + (item.href ? '<span class="arrow">&rarr;</span>' : '') + '</li>';
21605
+ }
21606
+ html += '</ul>';
21607
+ }
21608
+ briefBody.innerHTML = html;
21609
+ if (briefHint) {
21610
+ var sourceCount = b.highlights.filter(function(h) { return h.source === 'daily-note'; }).length;
21611
+ briefHint.textContent = sourceCount > 0 ? ('from today\\x27s daily note') : (b.highlights.length > 0 ? 'from recent activity' : '');
21612
+ }
21613
+ }
21614
+
21615
+ // Today's runs
21616
+ var runsBody = document.getElementById('today-runs-body');
21617
+ if (runsBody) {
21618
+ var runs = d.todayRuns || [];
21619
+ if (!runs.length) {
21620
+ runsBody.innerHTML = '<div style="padding:14px 18px;color:var(--text-muted);font-size:var(--text-sm)">No scheduled runs configured. <a href="#" onclick="navigateTo(\\x27build\\x27,{tab:\\x27crons\\x27});return false" style="color:var(--clementine)">Add one in Build &rarr; Crons</a>.</div>';
21621
+ } else {
21622
+ var html2 = '';
21623
+ for (var n = 0; n < runs.length; n++) {
21624
+ var run = runs[n];
21625
+ var status = run.firedToday ? 'fired' : 'upcoming';
21626
+ var label = run.firedToday ? 'fired today' : (run.nextRun ? timeUntil(run.nextRun) : 'manual');
21627
+ html2 += '<div class="run-row ' + status + '" onclick="navigateTo(\\x27build\\x27,{tab:\\x27crons\\x27})">'
21628
+ + '<span class="run-icon">' + (status === 'fired' ? lucide('check', 'icn-sm') : lucide('clock', 'icn-sm')) + '</span>'
21629
+ + '<span class="run-name">' + esc(run.name) + '</span>'
21630
+ + '<span class="run-time">' + esc(label) + '</span>'
21631
+ + '</div>';
21632
+ }
21633
+ runsBody.innerHTML = html2;
21634
+ }
21635
+ }
21636
+ } catch (err) {
21637
+ console.warn('refreshHomeDigest failed:', err);
21638
+ }
21639
+ }
21640
+
21641
+ // ── Home chat FAB + panel ────────────────────────────────────────
21642
+ function toggleHomeChat(forceOpen) {
21643
+ var fab = document.getElementById('home-chat-fab');
21644
+ var panel = document.getElementById('home-chat-panel');
21645
+ if (!panel || !fab) return;
21646
+ var willOpen = forceOpen != null ? !!forceOpen : !panel.classList.contains('open');
21647
+ panel.classList.toggle('open', willOpen);
21648
+ panel.setAttribute('aria-hidden', willOpen ? 'false' : 'true');
21649
+ fab.classList.toggle('hidden', willOpen);
21650
+ if (willOpen) {
21651
+ setTimeout(function() {
21652
+ var input = document.getElementById('chat-input');
21653
+ if (input) input.focus();
21654
+ }, 80);
21655
+ if (typeof loadProfiles === 'function') loadProfiles();
21656
+ }
21657
+ }
21658
+
21659
+ // Keyboard shortcut: ? toggles chat (when not typing in another input)
21660
+ document.addEventListener('keydown', function(e) {
21661
+ if (e.key === '?' && !e.metaKey && !e.ctrlKey) {
21662
+ var t = e.target;
21663
+ if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
21664
+ e.preventDefault();
21665
+ toggleHomeChat();
21666
+ }
21667
+ // Esc closes chat panel
21668
+ if (e.key === 'Escape') {
21669
+ var panel = document.getElementById('home-chat-panel');
21670
+ if (panel && panel.classList.contains('open')) toggleHomeChat(false);
21671
+ }
21672
+ });
21673
+
21674
+ function briefingNeedsReviewClick(href) {
21675
+ if (!href) return;
21676
+ if (href.startsWith('#')) {
21677
+ var parts = href.replace(/^#/, '').split('/');
21678
+ if (parts[0]) navigateTo(parts[0], parts[1] ? { tab: parts[1] } : {});
21679
+ }
21680
+ }
21681
+
20588
21682
  function toggleHomeRail() {
20589
21683
  var rail = document.getElementById('home-rail');
20590
21684
  if (!rail) return;
@@ -24039,8 +25133,8 @@ async function refreshSalesforce() {
24039
25133
  if (d.status) { try { refreshStatus(d.status); } catch(e) { console.warn('init: status', e); } }
24040
25134
  if (d.activity) { try { refreshActivity(false, d.activity); } catch(e) { console.warn('init: activity', e); } }
24041
25135
  else { try { refreshActivity(); } catch(e) { console.warn('init: activity fallback', e); } }
24042
- // Populate the home right rail (daemon, plan, runs, time-saved, approvals)
24043
- if (typeof refreshHomeRail === 'function') { try { refreshHomeRail(); } catch(e) { console.warn('init: rail', e); } }
25136
+ // Populate the home digest (greeting, KPIs, briefing, today's runs)
25137
+ if (typeof refreshHomeDigest === 'function') { try { refreshHomeDigest(); } catch(e) { console.warn('init: digest', e); } }
24044
25138
  if (d.office) { try { refreshTeamNav(d.office); refreshTeamPulse(d.office); } catch(e) { console.warn('init: office', e); } }
24045
25139
  if (d.plan) { try { refreshHomePlan(d.plan); } catch(e) { console.warn('init: plan', e); } }
24046
25140
  if (d.version) { try { _loadedHash = d.version.started; } catch(e) { /* ignore */ } }