clementine-agent 1.4.3 → 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 +954 -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,359 @@ 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
+ });
2243
2602
  app.get('/api/vault-files', async (req, res) => {
2244
2603
  try {
2245
2604
  const limit = Math.min(parseInt(String(req.query.limit ?? '120'), 10) || 120, 500);
@@ -8346,6 +8705,22 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
8346
8705
  --motion-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
8347
8706
  /* ── Focus ring ── */
8348
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;
8349
8724
  }
8350
8725
  [data-theme="dark"] {
8351
8726
  --bg-primary: #0b0f17;
@@ -8377,6 +8752,22 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
8377
8752
  --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5), 0 2px 4px rgba(0, 0, 0, 0.3);
8378
8753
  --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.6), 0 4px 8px rgba(0, 0, 0, 0.4);
8379
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;
8380
8771
  }
8381
8772
  /* OS-preference dark mode unless user has explicitly chosen a theme */
8382
8773
  @media (prefers-color-scheme: dark) {
@@ -8416,6 +8807,17 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
8416
8807
  box-shadow: var(--ring);
8417
8808
  border-radius: var(--radius-xs);
8418
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
+ }
8419
8821
  body {
8420
8822
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
8421
8823
  font-feature-settings: 'cv02', 'cv11', 'ss01', 'ss03';
@@ -8787,66 +9189,279 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
8787
9189
  .icn-lg { width: 22px; height: 22px; stroke-width: 1.75; }
8788
9190
  .icn-xl { width: 28px; height: 28px; stroke-width: 1.5; }
8789
9191
 
8790
- /* ── Home layout — chat-first daily driver ─── */
8791
- .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 {
8792
9225
  display: grid;
8793
- grid-template-columns: 1fr 320px;
8794
- gap: 18px;
8795
- height: calc(100vh - var(--header-h));
8796
- padding: 18px;
8797
- box-sizing: border-box;
9226
+ grid-template-columns: repeat(5, minmax(0, 1fr));
9227
+ gap: 10px;
8798
9228
  }
8799
- .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;
8800
9234
  display: flex;
8801
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);
8802
9284
  gap: 14px;
8803
- min-height: 0;
8804
- overflow-y: auto;
9285
+ align-items: start;
8805
9286
  }
8806
- .home-chat {
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;
9295
+ }
9296
+ .briefing-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 8px; }
9297
+ .briefing-item {
8807
9298
  display: flex;
8808
- flex-direction: column;
8809
- flex: 1;
8810
- min-height: 320px;
8811
- 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);
8812
9390
  border: 1px solid var(--border);
8813
- border-radius: 12px;
9391
+ border-radius: var(--radius-lg);
9392
+ box-shadow: var(--shadow-lg);
9393
+ display: flex;
9394
+ flex-direction: column;
8814
9395
  overflow: hidden;
8815
- 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);
9400
+ }
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);
8816
9415
  }
8817
- .home-chat-messages {
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 {
8818
9426
  flex: 1;
8819
9427
  overflow-y: auto;
8820
- padding: 18px 20px;
8821
- min-height: 280px;
9428
+ padding: 14px 16px;
9429
+ background: var(--bg-primary);
8822
9430
  }
8823
- .home-chat-input-row {
9431
+ .home-chat-panel-input-row {
8824
9432
  display: flex;
8825
9433
  gap: 8px;
8826
- padding: 12px 16px;
9434
+ padding: 10px 12px;
8827
9435
  border-top: 1px solid var(--border);
8828
9436
  background: var(--bg-secondary);
8829
9437
  align-items: center;
8830
9438
  }
8831
- .home-chat-input-row input[type="text"] {
9439
+ .home-chat-panel-input-row input[type="text"] {
8832
9440
  flex: 1;
8833
- padding: 10px 14px;
9441
+ padding: 8px 12px;
8834
9442
  border: 1px solid var(--border);
8835
- border-radius: 8px;
9443
+ border-radius: var(--radius-sm);
8836
9444
  background: var(--bg-input);
8837
9445
  color: var(--text-primary);
8838
- font-size: 14px;
9446
+ font-size: var(--text-base);
9447
+ font-family: inherit;
8839
9448
  }
8840
- .home-chat-input-row select {
8841
- padding: 6px 10px;
8842
- border: 1px solid var(--border);
8843
- border-radius: 6px;
8844
- background: var(--bg-secondary);
8845
- color: var(--text-primary);
8846
- 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
+ }
8847
9464
  }
8848
- .home-chat-input-row button { padding: 9px 18px; border-radius: 8px; }
8849
- .home-activity { margin: 0; }
8850
9465
 
8851
9466
  /* Right rail */
8852
9467
  .home-rail {
@@ -11526,11 +12141,12 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
11526
12141
  function run() {
11527
12142
  document.querySelectorAll('[data-icon]').forEach(function(el) {
11528
12143
  if (el.dataset._iconHydrated) return;
12144
+ if (!window.LUCIDE || !window.lucide) return;
11529
12145
  el.dataset._iconHydrated = '1';
11530
12146
  var name = el.getAttribute('data-icon');
11531
- if (!window.LUCIDE || !window.lucide) return;
11532
12147
  var slot = el.querySelector('.nav-icon, .hire-plus, .icon-slot');
11533
- if (slot) slot.innerHTML = window.lucide(name, 'icn-md');
12148
+ var target = slot || el;
12149
+ target.innerHTML = window.lucide(name, 'icn-md');
11534
12150
  });
11535
12151
  }
11536
12152
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run);
@@ -11546,143 +12162,140 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
11546
12162
  <!-- Content -->
11547
12163
  <div class="content">
11548
12164
 
11549
- <!-- ═══ Home — chat-first daily driver ═══ -->
12165
+ <!-- ═══ Home — daily-driver dashboard ═══ -->
11550
12166
  <div class="page active" id="page-home">
11551
- <div class="home-layout">
11552
- <main class="home-main">
11553
- <!-- Getting Started (shown for new users only) -->
11554
- <div id="getting-started" class="getting-started" style="display:none">
11555
- <div class="gs-header">
11556
- <div class="gs-title">Get Started</div>
11557
- <div class="gs-subtitle">Set up your AI assistant in 5 steps</div>
11558
- <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>
11559
12182
  </div>
11560
- <div class="gs-grid">
11561
- <div class="gs-card" id="gs-step-auth">
11562
- <div class="gs-step-num">1</div>
11563
- <div class="gs-card-icon">&#128274;</div>
11564
- <div class="gs-card-title">Login with Anthropic</div>
11565
- <div class="gs-card-desc" id="gs-auth-desc">Connect your Anthropic account to power your agents.</div>
11566
- <button class="btn btn-sm btn-primary" id="gs-auth-btn" onclick="startAnthropicOAuth()">Login with Claude</button>
11567
- </div>
11568
- <div class="gs-card" id="gs-step-agent">
11569
- <div class="gs-step-num">2</div>
11570
- <div class="gs-card-icon">&#128101;</div>
11571
- <div class="gs-card-title">Create an Agent</div>
11572
- <div class="gs-card-desc">Hire your first AI team member with a role, tools, and a channel.</div>
11573
- <button class="btn btn-sm btn-primary" onclick="navigateTo('team')">Go to The Office</button>
11574
- </div>
11575
- <div class="gs-card" id="gs-step-channel">
11576
- <div class="gs-step-num">3</div>
11577
- <div class="gs-card-icon">&#128172;</div>
11578
- <div class="gs-card-title">Connect a Channel</div>
11579
- <div class="gs-card-desc">Link Discord or Slack so your agents can communicate.</div>
11580
- <button class="btn btn-sm" onclick="navigateTo('settings', { tab: 'channels' })">Open Settings</button>
11581
- </div>
11582
- <div class="gs-card" id="gs-step-task">
11583
- <div class="gs-step-num">4</div>
11584
- <div class="gs-card-icon">&#9200;</div>
11585
- <div class="gs-card-title">Schedule a Task</div>
11586
- <div class="gs-card-desc">Set up cron jobs so agents work on autopilot.</div>
11587
- <button class="btn btn-sm" onclick="navigateTo('build', { tab: 'crons' })">Add a Task</button>
11588
- </div>
11589
- <div class="gs-card" id="gs-step-project">
11590
- <div class="gs-step-num">5</div>
11591
- <div class="gs-card-icon">&#128194;</div>
11592
- <div class="gs-card-title">Link a Project</div>
11593
- <div class="gs-card-desc">Give agents context about your codebases and tools.</div>
11594
- <button class="btn btn-sm" onclick="navigateTo('settings', { tab: 'projects' })">Browse Projects</button>
11595
- </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>
11596
12189
  </div>
11597
- </div>
11598
-
11599
- <!-- Chat panel — primary surface -->
11600
- <div class="home-chat">
11601
- <div id="chat-messages" class="home-chat-messages">
11602
- <div class="empty-state" style="margin-top:32px">
11603
- <p style="margin-bottom:14px;color:var(--text-muted)">What can I help with?</p>
11604
- <div style="display:flex;flex-wrap:wrap;gap:8px;justify-content:center">
11605
- <button class="btn btn-sm quick-pill" onclick="quickChat(&quot;What&apos;s on my schedule?&quot;)">What's on my schedule?</button>
11606
- <button class="btn btn-sm quick-pill" onclick="quickChat('Check my email')">Check my email</button>
11607
- <button class="btn btn-sm quick-pill" onclick="quickChat('Run morning briefing')">Run morning briefing</button>
11608
- <button class="btn btn-sm quick-pill" onclick="quickChat('What did you do today?')">What did you do today?</button>
11609
- </div>
11610
- </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>
11611
12196
  </div>
11612
- <div class="home-chat-input-row">
11613
- <input type="text" id="chat-input" placeholder="Ask Clementine anything..." onkeydown="if(event.key==='Enter'&amp;&amp;!event.shiftKey){event.preventDefault();sendChat()}">
11614
- <select id="chat-profile-select" onchange="switchProfile(this.value)" title="Active profile">
11615
- <option value="">Default</option>
11616
- </select>
11617
- <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>
11618
12210
  </div>
11619
12211
  </div>
12212
+ </div>
11620
12213
 
11621
- <!-- Activity feed -->
11622
- <div class="card home-activity">
11623
- <div class="card-header" style="display:flex;justify-content:space-between;align-items:center">
11624
- <span>Live activity</span>
11625
- <div style="display:flex;gap:6px;align-items:center">
11626
- <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)">
11627
- <option value="">All Sources</option>
11628
- <option value="cron">Cron</option>
11629
- <option value="activity">Activities</option>
11630
- <option value="send">Emails</option>
11631
- <option value="approval">Approvals</option>
11632
- <option value="memory">Memory</option>
11633
- </select>
11634
- <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)">
11635
- <option value="">All Agents</option>
11636
- </select>
11637
- </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>
11638
12265
  </div>
11639
- <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>
11640
12266
  </div>
11641
- </main>
11642
12267
 
11643
- <!-- Right rail: Today / Upcoming / Active / Time-saved / Approvals -->
11644
- <aside class="home-rail" id="home-rail">
11645
- <button class="rail-collapse-btn" onclick="toggleHomeRail()" title="Hide rail">&times;</button>
11646
-
11647
- <section class="rail-card">
11648
- <div class="rail-header">
11649
- <span><span class="status-pip green"></span>Daemon</span>
11650
- <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>
11651
12272
  </div>
11652
- <div class="rail-body" id="rail-daemon-body">
11653
- <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>
11654
12275
  </div>
11655
- </section>
11656
-
11657
- <section class="rail-card">
11658
- <div class="rail-header">
11659
- <span>Today</span>
11660
- <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>
11661
12295
  </div>
11662
- <div class="rail-body" id="home-plan-content"><div class="skel-row med"></div><div class="skel-row"></div></div>
11663
- </section>
11664
-
11665
- <section class="rail-card">
11666
- <div class="rail-header"><span>Upcoming runs</span><span id="rail-upcoming-count" class="rail-badge">0</span></div>
11667
- <div class="rail-body" id="rail-upcoming"><div class="skel-row short"></div></div>
11668
- </section>
11669
-
11670
- <section class="rail-card">
11671
- <div class="rail-header"><span>Active runs</span><span id="rail-active-count" class="rail-badge" style="display:none">0</span></div>
11672
- <div class="rail-body" id="rail-active"><div style="font-size:12px;color:var(--text-muted)">Nothing running.</div></div>
11673
- </section>
11674
-
11675
- <section class="rail-card">
11676
- <div class="rail-header"><span>Time saved this week</span></div>
11677
- <div class="rail-body" id="rail-time-saved"><div class="skel-row short"></div></div>
11678
- </section>
11679
-
11680
- <section class="rail-card">
11681
- <div class="rail-header"><span>Approvals</span><span id="rail-approvals-count" class="rail-badge" style="display:none">0</span></div>
11682
- <div class="rail-body" id="rail-approvals"><div style="font-size:12px;color:var(--text-muted)">Nothing pending.</div></div>
11683
- </section>
11684
- </aside>
11685
- <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>
11686
12299
  </div>
11687
12300
  </div>
11688
12301
 
@@ -13164,6 +13777,41 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
13164
13777
  </script>
13165
13778
  </div>
13166
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
+
13167
13815
  <!-- Sessions, Trust & Claims, Logs, Chat, Metrics, Daily Plan pages
13168
13816
  removed in Session 2 — content lives on Home or migrates to other
13169
13817
  destinations in Sessions 3-4. Page references for these IDs are
@@ -14357,6 +15005,7 @@ function navigateTo(page, opts) {
14357
15005
  switch (page) {
14358
15006
  case 'home':
14359
15007
  refreshAll();
15008
+ if (typeof refreshHomeDigest === 'function') refreshHomeDigest();
14360
15009
  // tab is a soft hint on Home (one cohesive layout): focus the relevant area.
14361
15010
  var t = opts.tab || 'chat';
14362
15011
  setTimeout(function() {
@@ -20889,6 +21538,147 @@ async function saveDiscordSetup() {
20889
21538
  } catch(e) { toast(String(e), 'error'); }
20890
21539
  }
20891
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
+
20892
21682
  function toggleHomeRail() {
20893
21683
  var rail = document.getElementById('home-rail');
20894
21684
  if (!rail) return;
@@ -24343,8 +25133,8 @@ async function refreshSalesforce() {
24343
25133
  if (d.status) { try { refreshStatus(d.status); } catch(e) { console.warn('init: status', e); } }
24344
25134
  if (d.activity) { try { refreshActivity(false, d.activity); } catch(e) { console.warn('init: activity', e); } }
24345
25135
  else { try { refreshActivity(); } catch(e) { console.warn('init: activity fallback', e); } }
24346
- // Populate the home right rail (daemon, plan, runs, time-saved, approvals)
24347
- 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); } }
24348
25138
  if (d.office) { try { refreshTeamNav(d.office); refreshTeamPulse(d.office); } catch(e) { console.warn('init: office', e); } }
24349
25139
  if (d.plan) { try { refreshHomePlan(d.plan); } catch(e) { console.warn('init: plan', e); } }
24350
25140
  if (d.version) { try { _loadedHash = d.version.started; } catch(e) { /* ignore */ } }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.4.3",
3
+ "version": "1.5.0",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",