bloby-bot 0.51.4 → 0.52.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 (32) hide show
  1. package/dist-bloby/assets/{bloby-vi0Xitb-.js → bloby-CjvuL1QI.js} +77 -77
  2. package/dist-bloby/assets/{globals-DNO3ilRx.js → globals-UaNdQXf5.js} +3 -3
  3. package/dist-bloby/assets/globals-ZdFBsH0Q.css +2 -0
  4. package/dist-bloby/assets/{highlighted-body-OFNGDK62-DMeCY5Rc.js → highlighted-body-OFNGDK62-D0QQRXpE.js} +1 -1
  5. package/dist-bloby/assets/mermaid-GHXKKRXX-D9GshF2B.js +1 -0
  6. package/dist-bloby/assets/{onboard-D8sRPjz2.js → onboard-DU3OfA5h.js} +1 -1
  7. package/dist-bloby/bloby.html +3 -3
  8. package/dist-bloby/onboard.html +3 -3
  9. package/package.json +5 -3
  10. package/supervisor/chat/OnboardWizard.tsx +16 -16
  11. package/supervisor/chat/bloby-main.tsx +40 -7
  12. package/supervisor/chat/src/components/Chat/MessageBubble.tsx +17 -3
  13. package/supervisor/chat/src/components/Chat/MessageList.tsx +7 -1
  14. package/supervisor/chat/src/components/Chat/NotchCard.tsx +70 -0
  15. package/supervisor/chat/src/components/LoginScreen.tsx +3 -3
  16. package/supervisor/chat/src/styles/globals.css +18 -18
  17. package/supervisor/harnesses/claude.ts +31 -1
  18. package/supervisor/harnesses/codex.ts +15 -1
  19. package/supervisor/index.ts +48 -19
  20. package/worker/prompts/bloby-system-prompt.txt +1 -1
  21. package/workspace/client/src/components/Dashboard/DashboardPage.tsx +4 -4
  22. package/workspace/client/src/components/Layout/Sidebar.tsx +1 -1
  23. package/workspace/client/src/components/deleteme_onboarding/tour-theme.css +1 -1
  24. package/workspace/client/src/styles/globals.css +18 -18
  25. package/workspace/skills/mac/SKILL.md +302 -0
  26. package/workspace/skills/mac/frequentSnippets/calendar-today.html +22 -0
  27. package/workspace/skills/mac/frequentSnippets/info-card.html +27 -0
  28. package/workspace/skills/mac/frequentSnippets/single-stat.html +13 -0
  29. package/workspace/skills/mac/presets/PRESETS.md +226 -0
  30. package/workspace/skills/mac/skill.json +15 -0
  31. package/dist-bloby/assets/globals-D60b-8LY.css +0 -2
  32. package/dist-bloby/assets/mermaid-GHXKKRXX-BOqNyL14.js +0 -1
@@ -22,7 +22,7 @@
22
22
  --color-accent-foreground: #EBEBEB;
23
23
 
24
24
  /* ── Primary ── */
25
- --color-primary: #4C89F2;
25
+ --color-primary: #0069FE;
26
26
  --color-primary-foreground: #ffffff;
27
27
 
28
28
  /* ── Danger ── */
@@ -32,10 +32,10 @@
32
32
  /* ── Borders & inputs ── */
33
33
  --color-border: #333333;
34
34
  --color-input: #333333;
35
- --color-ring: #4C89F2;
35
+ --color-ring: #0069FE;
36
36
 
37
37
  /* ── Charts ── */
38
- --color-chart-1: #4C89F2;
38
+ --color-chart-1: #0069FE;
39
39
  --color-chart-2: #F04D68;
40
40
  --color-chart-3: #F59E0B;
41
41
  --color-chart-4: #8B5CF6;
@@ -44,12 +44,12 @@
44
44
  /* ── Code blocks (Streamdown uses bg-sidebar / bg-background) ── */
45
45
  --color-sidebar: #222222;
46
46
  --color-sidebar-foreground: #EBEBEB;
47
- --color-sidebar-primary: #4C89F2;
47
+ --color-sidebar-primary: #0069FE;
48
48
  --color-sidebar-primary-foreground: #ffffff;
49
49
  --color-sidebar-accent: #2A2A2A;
50
50
  --color-sidebar-accent-foreground: #EBEBEB;
51
51
  --color-sidebar-border: #333333;
52
- --color-sidebar-ring: #4C89F2;
52
+ --color-sidebar-ring: #0069FE;
53
53
 
54
54
  --radius: 0.75rem;
55
55
  }
@@ -68,7 +68,7 @@ body {
68
68
  }
69
69
 
70
70
  ::selection {
71
- background-color: rgba(175, 39, 227, 0.25);
71
+ background-color: rgba(0, 105, 254, 0.25);
72
72
  }
73
73
 
74
74
  ::-webkit-scrollbar { width: 6px; }
@@ -81,16 +81,16 @@ body {
81
81
  -webkit-background-clip: text;
82
82
  color: transparent;
83
83
  -webkit-text-fill-color: transparent;
84
- background-image: linear-gradient(135deg, #04D1FE, #AF27E3, #FB4072);
84
+ background-image: linear-gradient(135deg, #0166FF, #009AFE, #4AEEFF);
85
85
  }
86
86
 
87
87
  .bg-gradient-brand {
88
- background-image: linear-gradient(135deg, #04D1FE, #AF27E3, #FB4072);
88
+ background-image: linear-gradient(135deg, #0166FF, #009AFE, #4AEEFF);
89
89
  }
90
90
 
91
91
  .glow-border {
92
- box-shadow: 0 0 0 1px rgba(175, 39, 227, 0.1),
93
- 0 0 20px -5px rgba(175, 39, 227, 0.15);
92
+ box-shadow: 0 0 0 1px rgba(0, 105, 254, 0.1),
93
+ 0 0 20px -5px rgba(0, 105, 254, 0.15);
94
94
  }
95
95
 
96
96
  .animated-border {
@@ -103,10 +103,10 @@ body {
103
103
  inset: -150%;
104
104
  background: conic-gradient(
105
105
  from 0deg,
106
- #04D1FE,
107
- #AF27E3,
108
- #FB4072,
109
- #04D1FE
106
+ #0166FF,
107
+ #009AFE,
108
+ #4AEEFF,
109
+ #0166FF
110
110
  );
111
111
  animation: border-spin 3s linear infinite;
112
112
  }
@@ -120,10 +120,10 @@ body {
120
120
  }
121
121
 
122
122
  .input-glow:focus {
123
- border-color: rgba(175, 39, 227, 0.4);
124
- box-shadow: 0 0 0 1px rgba(175, 39, 227, 0.15),
125
- 0 0 20px -5px rgba(175, 39, 227, 0.25),
126
- 0 0 4px -1px rgba(4, 209, 254, 0.1);
123
+ border-color: rgba(0, 105, 254, 0.4);
124
+ box-shadow: 0 0 0 1px rgba(0, 105, 254, 0.15),
125
+ 0 0 20px -5px rgba(0, 105, 254, 0.25),
126
+ 0 0 4px -1px rgba(74, 238, 255, 0.1);
127
127
  }
128
128
 
129
129
  @keyframes border-spin {
@@ -77,6 +77,9 @@ interface LiveConversation {
77
77
  onMessage: (type: string, data: any) => void;
78
78
  /** True while the model is actively processing (between message push and result) */
79
79
  busy: boolean;
80
+ /** Messages pushed but not yet completed (1 result per message). Used to know when
81
+ * the session is truly idle — i.e. no queued message — so it's safe to recycle. */
82
+ pendingCount: number;
80
83
  }
81
84
 
82
85
  const liveConversations = new Map<string, LiveConversation>();
@@ -217,6 +220,12 @@ async function buildConversationOptions(
217
220
  mcpServers,
218
221
  agents,
219
222
  agentProgressSummaries: true,
223
+ // Auto-compaction: the live conversation is a single long-lived query() whose
224
+ // context grows every turn (messages + tool results + sub-agent transcripts).
225
+ // Enable it explicitly via inline settings so it does NOT depend on filesystem
226
+ // settings.json being present — when context fills, the SDK summarizes older
227
+ // history and continues instead of hitting the hard context wall.
228
+ settings: { autoCompactEnabled: true },
220
229
  env: {
221
230
  ...process.env as Record<string, string>,
222
231
  CLAUDE_CODE_OAUTH_TOKEN: oauthToken,
@@ -297,6 +306,7 @@ export async function startConversation(
297
306
  queryHandle: null,
298
307
  onMessage,
299
308
  busy: false,
309
+ pendingCount: 0,
300
310
  };
301
311
  liveConversations.set(conversationId, conv);
302
312
 
@@ -369,7 +379,26 @@ export async function startConversation(
369
379
  // Signal turn complete — backend restart + UI update
370
380
  const FILE_TOOLS = ['Write', 'Edit'];
371
381
  const usedFileTools = FILE_TOOLS.some((t) => usedTools.has(t));
372
- onMessage('bot:turn-complete', { conversationId, usedFileTools });
382
+
383
+ // Context-size signal for the orchestrator's proactive session recycling.
384
+ // Prefer modelUsage (carries the per-model contextWindow); fall back to raw usage.
385
+ let contextTokens = 0;
386
+ let contextWindow = 0;
387
+ const modelUsage = (msg as any).modelUsage as Record<string, any> | undefined;
388
+ if (modelUsage) {
389
+ for (const mu of Object.values(modelUsage)) {
390
+ const used = (mu?.inputTokens || 0) + (mu?.cacheReadInputTokens || 0) + (mu?.cacheCreationInputTokens || 0);
391
+ if (used > contextTokens) { contextTokens = used; contextWindow = mu?.contextWindow || 0; }
392
+ }
393
+ }
394
+ if (!contextTokens) {
395
+ const u = (msg as any).usage || {};
396
+ contextTokens = (u.input_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0);
397
+ }
398
+ // One result per pushed message → decrement; idle when nothing else is queued.
399
+ conv.pendingCount = Math.max(0, (conv.pendingCount || 0) - 1);
400
+ const idle = conv.pendingCount === 0;
401
+ onMessage('bot:turn-complete', { conversationId, usedFileTools, contextTokens, contextWindow, idle });
373
402
 
374
403
  // Reset per-turn state
375
404
  usedTools.clear();
@@ -482,6 +511,7 @@ export function pushMessage(
482
511
 
483
512
  const userMessage = buildUserMessage(content, attachments, savedFiles);
484
513
  conv.busy = true;
514
+ conv.pendingCount = (conv.pendingCount || 0) + 1;
485
515
  conv.inputQueue.push(userMessage);
486
516
 
487
517
  // Emit typing indicator
@@ -483,7 +483,16 @@ function handleNotification(conv: CodexConversation, n: { method: string; params
483
483
  conv.onMessage('bot:done', { conversationId: conv.id, usedFileTools: conv.usedFileTools });
484
484
  teardownConversation(conv.id);
485
485
  } else {
486
- conv.onMessage('bot:turn-complete', { conversationId: conv.id, usedFileTools: conv.usedFileTools });
486
+ // Context-size signal for the orchestrator's proactive session recycling.
487
+ // The codex app-server reports token usage on turn/completed; field names vary
488
+ // across versions, so probe defensively (0 if absent → falls back to codex's
489
+ // own built-in auto-compaction).
490
+ const tu: any = p.turn?.usage || p.usage || {};
491
+ const contextTokens = tu.input_tokens ?? tu.inputTokens ?? tu.total_tokens ?? tu.totalTokens ?? tu.tokens ?? 0;
492
+ const contextWindow = tu.context_window ?? tu.contextWindow ?? 0;
493
+ // idle = no message queued behind this turn (the drain happens just below).
494
+ const idle = conv.pendingInputs.length === 0;
495
+ conv.onMessage('bot:turn-complete', { conversationId: conv.id, usedFileTools: conv.usedFileTools, contextTokens, contextWindow, idle });
487
496
 
488
497
  // Drain any messages that were submitted while we were busy.
489
498
  const next = conv.pendingInputs.shift();
@@ -558,6 +567,11 @@ async function spawnAndInitialize(
558
567
  log.info(`[codex] init conversation ${conversationId} (model=${modelId}${effort ? `, effort=${effort}` : ''})`);
559
568
  await rpc.request('initialize', { clientInfo: CLIENT_INFO });
560
569
  rpc.notify('initialized', {});
570
+ // Context auto-compaction is ON by default in the codex app-server: when the
571
+ // thread's token count crosses the model's threshold it compacts history in
572
+ // place (emitting a `contextCompaction` item) and continues — no flag needed
573
+ // here. A manual trigger also exists (`thread/compact/start`) if we ever want
574
+ // to force it from the UI.
561
575
  const startResult = await rpc.request<{ thread: { id: string } }>('thread/start', {
562
576
  cwd: WORKSPACE_DIR,
563
577
  model: modelId,
@@ -31,6 +31,19 @@ import { ChannelManager } from './channels/manager.js';
31
31
  const DIST_BLOBY = path.join(PKG_DIR, 'dist-bloby');
32
32
  const SUPERVISOR_PUBLIC = path.join(PKG_DIR, 'supervisor', 'public');
33
33
 
34
+ // Proactive context recycling. The chat runs as one long-lived agent session per
35
+ // conversation (so the user can keep talking while the agent works). That session's
36
+ // context grows every turn and would eventually hit the wall. But continuity does NOT
37
+ // live in that session — every fresh session re-injects the recent messages + memory
38
+ // files — so when the context grows large AND the agent is idle, we end the session;
39
+ // the next message starts a clean one. This keeps heavy/long chats off the wall
40
+ // without lossy compaction, while preserving mid-turn responsiveness (we only recycle
41
+ // between turns). When the harness reports the model's context window we recycle at
42
+ // RECYCLE_FRACTION of it (adaptive to 200k vs 1M windows); otherwise we fall back to a
43
+ // fixed token budget. Auto-compaction stays on as the in-turn safety net.
44
+ const CONTEXT_RECYCLE_TOKENS = Number(process.env.BLOBY_CONTEXT_RECYCLE_TOKENS) || 140_000;
45
+ const CONTEXT_RECYCLE_FRACTION = Number(process.env.BLOBY_CONTEXT_RECYCLE_FRACTION) || 0.7;
46
+
34
47
  // Platform assets that must survive workspace swaps — served directly by supervisor
35
48
  const PLATFORM_ASSETS = new Set([
36
49
  '/spritesheet.webp',
@@ -502,7 +515,7 @@ export async function startSupervisor() {
502
515
  const connected = status?.connected || false;
503
516
  res.writeHead(200);
504
517
  const confettiHTML = Array.from({ length: 30 }, (_, i) => {
505
- const colors = ['#04D1FE', '#AF27E3', '#FB4072', '#4ade80', '#facc15', '#818cf8'];
518
+ const colors = ['#0166FF', '#009AFE', '#4AEEFF', '#4ade80', '#facc15', '#818cf8'];
506
519
  const color = colors[Math.floor(Math.random() * colors.length)];
507
520
  const left = Math.random() * 100;
508
521
  const delay = i * 0.04;
@@ -521,7 +534,7 @@ export async function startSupervisor() {
521
534
  body{background:#212121;color:#f5f5f5;font-family:'Inter',system-ui,-apple-system,sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100dvh;margin:0;overflow-x:hidden}
522
535
  .container{display:flex;flex-direction:column;align-items:center;max-width:360px;width:100%;padding:20px}
523
536
 
524
- .qr-card{background:#2a2a2a;border:1px solid rgba(255,255,255,0.08);border-radius:20px;padding:28px;width:100%;box-shadow:0 0 0 1px rgba(175,39,227,0.1),0 0 20px -5px rgba(175,39,227,0.15);animation:fade-up .5s ease-out both}
537
+ .qr-card{background:#2a2a2a;border:1px solid rgba(255,255,255,0.08);border-radius:20px;padding:28px;width:100%;box-shadow:0 0 0 1px rgba(0, 105, 254,0.1),0 0 20px -5px rgba(0, 105, 254,0.15);animation:fade-up .5s ease-out both}
525
538
  .qr-inner{background:#fff;border-radius:12px;padding:16px}
526
539
  .qr-inner svg{width:100%;height:auto;display:block}
527
540
 
@@ -533,22 +546,22 @@ export async function startSupervisor() {
533
546
 
534
547
  .phone-section{width:100%;animation:fade-up .5s ease-out .4s both}
535
548
  .phone-toggle{background:none;border:none;color:#888;font-size:13px;cursor:pointer;font-family:inherit;padding:4px 0;transition:color .2s;width:100%;text-align:center}
536
- .phone-toggle:hover{color:#AF27E3}
549
+ .phone-toggle:hover{color:#0069FE}
537
550
 
538
551
  .phone-form{display:none;width:100%;margin-top:16px;animation:fade-up .3s ease-out both}
539
552
  .phone-form.visible{display:flex;flex-direction:column;align-items:center;gap:12px}
540
553
  .phone-input-wrap{display:flex;gap:8px;width:100%}
541
554
  .phone-input{flex:1;background:#2a2a2a;border:1px solid rgba(255,255,255,0.1);border-radius:10px;padding:12px 14px;color:#f5f5f5;font-size:15px;font-family:inherit;outline:none;transition:border-color .2s}
542
- .phone-input:focus{border-color:#AF27E3}
555
+ .phone-input:focus{border-color:#0069FE}
543
556
  .phone-input::placeholder{color:#555}
544
- .phone-btn{background:linear-gradient(135deg,#AF27E3,#FB4072);border:none;border-radius:10px;padding:12px 20px;color:#fff;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;white-space:nowrap;transition:opacity .2s}
557
+ .phone-btn{background:linear-gradient(135deg, #0166FF, #009AFE);border:none;border-radius:10px;padding:12px 20px;color:#fff;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;white-space:nowrap;transition:opacity .2s}
545
558
  .phone-btn:hover{opacity:.9}
546
559
  .phone-btn:disabled{opacity:.5;cursor:not-allowed}
547
560
  .phone-hint{font-size:12px;color:#555;text-align:center}
548
561
 
549
562
  .code-display{display:none;width:100%;margin-top:16px;text-align:center;animation:fade-up .3s ease-out both}
550
563
  .code-display.visible{display:block}
551
- .code-value{font-family:'Space Grotesk',monospace;font-size:32px;font-weight:700;letter-spacing:6px;background:linear-gradient(135deg,#04D1FE,#AF27E3);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin:12px 0}
564
+ .code-value{font-family:'Space Grotesk',monospace;font-size:32px;font-weight:700;letter-spacing:6px;background:linear-gradient(135deg, #0166FF, #009AFE);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin:12px 0}
552
565
  .code-steps{font-size:12px;color:#666;line-height:1.8;text-align:left;margin-top:12px;padding:0 8px}
553
566
  .code-steps li{margin-bottom:2px}
554
567
  .code-error{color:#FB4072;font-size:13px;margin-top:8px}
@@ -562,7 +575,7 @@ export async function startSupervisor() {
562
575
  @keyframes pop-in{0%{transform:scale(0);opacity:0}100%{transform:scale(1);opacity:1}}
563
576
 
564
577
  .text-wrap{text-align:center;animation:fade-up .5s ease-out .3s both}
565
- .title{font-family:'Space Grotesk',sans-serif;font-size:22px;font-weight:700;background:linear-gradient(135deg,#04D1FE,#AF27E3,#FB4072);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin-bottom:8px}
578
+ .title{font-family:'Space Grotesk',sans-serif;font-size:22px;font-weight:700;background:linear-gradient(135deg, #0166FF, #009AFE, #4AEEFF);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin-bottom:8px}
566
579
  .subtitle{font-size:14px;color:#999;line-height:1.5}
567
580
 
568
581
  .loading{font-size:14px;color:#999;animation:pulse 2s ease-in-out infinite}
@@ -574,7 +587,7 @@ ${connected
574
587
  ? `<div class="confetti-wrap">${confettiHTML}</div>
575
588
  <div class="video-wrap"><video autoplay muted playsinline><source src="/bloby_happy_reappearing.mov" type='video/mp4; codecs="hvc1"'><source src="/bloby_happy_reappearing.webm" type="video/webm"></video></div>
576
589
  <div class="text-wrap"><div class="title">Connected!</div><p class="subtitle">WhatsApp is linked. You can close this page.</p>
577
- <button onclick="relink()" style="margin-top:20px;padding:10px 24px;background:#2a2a2a;border:1px solid rgba(255,255,255,0.15);border-radius:10px;color:#999;font-size:13px;cursor:pointer;font-family:inherit;transition:all .2s" onmouseover="this.style.borderColor='#AF27E3';this.style.color='#f5f5f5'" onmouseout="this.style.borderColor='rgba(255,255,255,0.15)';this.style.color='#999'">Relink to a different number</button>
590
+ <button onclick="relink()" style="margin-top:20px;padding:10px 24px;background:#2a2a2a;border:1px solid rgba(255,255,255,0.15);border-radius:10px;color:#999;font-size:13px;cursor:pointer;font-family:inherit;transition:all .2s" onmouseover="this.style.borderColor='#0069FE';this.style.color='#f5f5f5'" onmouseout="this.style.borderColor='rgba(255,255,255,0.15)';this.style.color='#999'">Relink to a different number</button>
578
591
  </div>
579
592
  <script>async function relink(){await fetch('/api/channels/whatsapp/logout',{method:'POST'});await fetch('/api/channels/whatsapp/connect',{method:'POST'});setTimeout(()=>location.reload(),2000)}</script>`
580
593
  : qr
@@ -980,7 +993,7 @@ ${!connected ? `<script>
980
993
  const alexaStatus = channelManager.getStatus('alexa');
981
994
  const alreadyLinked = !!(alexaStatus?.info as any)?.linked;
982
995
  const confettiHTML = Array.from({ length: 30 }, (_, i) => {
983
- const colors = ['#04D1FE', '#AF27E3', '#FB4072', '#4ade80', '#facc15', '#818cf8'];
996
+ const colors = ['#0166FF', '#009AFE', '#4AEEFF', '#4ade80', '#facc15', '#818cf8'];
984
997
  const color = colors[Math.floor(Math.random() * colors.length)];
985
998
  const left = Math.random() * 100;
986
999
  const delay = i * 0.04;
@@ -998,17 +1011,17 @@ ${!connected ? `<script>
998
1011
  body{background:#212121;color:#f5f5f5;font-family:'Inter',system-ui,-apple-system,sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100dvh;margin:0;overflow-x:hidden}
999
1012
  .container{display:flex;flex-direction:column;align-items:center;max-width:380px;width:100%;padding:20px}
1000
1013
 
1001
- .card{background:#2a2a2a;border:1px solid rgba(255,255,255,0.08);border-radius:20px;padding:28px 24px;width:100%;box-shadow:0 0 0 1px rgba(175,39,227,0.1),0 0 20px -5px rgba(175,39,227,0.15);animation:fade-up .5s ease-out both;text-align:center}
1014
+ .card{background:#2a2a2a;border:1px solid rgba(255,255,255,0.08);border-radius:20px;padding:28px 24px;width:100%;box-shadow:0 0 0 1px rgba(0, 105, 254,0.1),0 0 20px -5px rgba(0, 105, 254,0.15);animation:fade-up .5s ease-out both;text-align:center}
1002
1015
 
1003
1016
  .header{display:flex;flex-direction:column;align-items:center;gap:6px;margin-bottom:18px}
1004
1017
  .badge-alexa{display:inline-flex;align-items:center;gap:6px;background:#1a1a1a;border:1px solid rgba(255,255,255,0.08);border-radius:9999px;padding:4px 10px;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:0.6px}
1005
- .badge-alexa::before{content:'';width:8px;height:8px;border-radius:50%;background:linear-gradient(135deg,#04D1FE,#AF27E3);box-shadow:0 0 8px rgba(4,209,254,0.5)}
1006
- .title{font-family:'Space Grotesk',sans-serif;font-size:22px;font-weight:700;background:linear-gradient(135deg,#04D1FE,#AF27E3,#FB4072);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin-top:6px}
1018
+ .badge-alexa::before{content:'';width:8px;height:8px;border-radius:50%;background:linear-gradient(135deg, #0166FF, #009AFE);box-shadow:0 0 8px rgba(74, 238, 255,0.5)}
1019
+ .title{font-family:'Space Grotesk',sans-serif;font-size:22px;font-weight:700;background:linear-gradient(135deg, #0166FF, #009AFE, #4AEEFF);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin-top:6px}
1007
1020
  .sub{font-size:13px;color:#999;line-height:1.6;margin-top:6px}
1008
1021
 
1009
1022
  .code-block{margin:18px 0 6px;animation:fade-up .5s ease-out .15s both}
1010
1023
  .code-label{font-size:11px;color:#666;text-transform:uppercase;letter-spacing:0.8px;margin-bottom:8px}
1011
- .code-value{font-family:'Space Grotesk',monospace;font-size:36px;font-weight:700;letter-spacing:8px;background:linear-gradient(135deg,#04D1FE,#AF27E3);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;line-height:1.1}
1024
+ .code-value{font-family:'Space Grotesk',monospace;font-size:36px;font-weight:700;letter-spacing:8px;background:linear-gradient(135deg, #0166FF, #009AFE);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;line-height:1.1}
1012
1025
  .countdown{font-size:12px;color:#666;margin-top:10px;display:inline-flex;align-items:center;gap:6px}
1013
1026
  .countdown.warn{color:#FB4072}
1014
1027
  .countdown .dot{width:6px;height:6px;border-radius:50%;background:currentColor;animation:pulse 1.6s ease-in-out infinite;opacity:.6}
@@ -1016,8 +1029,8 @@ ${!connected ? `<script>
1016
1029
  .quote-card{background:#1a1a1a;border:1px solid rgba(255,255,255,0.06);border-radius:12px;padding:14px 16px;margin:18px 0 6px;animation:fade-up .5s ease-out .25s both}
1017
1030
  .quote-label{font-size:11px;color:#666;text-transform:uppercase;letter-spacing:0.6px;margin-bottom:6px}
1018
1031
  .quote{font-size:15px;color:#f5f5f5;line-height:1.5;font-style:italic}
1019
- .quote .invocation{color:#04D1FE;font-style:normal;font-weight:600}
1020
- .quote .num{background:linear-gradient(135deg,#04D1FE,#AF27E3);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;font-style:normal;font-weight:700;letter-spacing:2px}
1032
+ .quote .invocation{color:#0069FE;font-style:normal;font-weight:600}
1033
+ .quote .num{background:linear-gradient(135deg, #0166FF, #009AFE);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;font-style:normal;font-weight:700;letter-spacing:2px}
1021
1034
 
1022
1035
  .steps{text-align:left;margin-top:20px;animation:fade-up .5s ease-out .35s both}
1023
1036
  .steps-title{font-size:11px;color:#666;text-transform:uppercase;letter-spacing:0.6px;margin-bottom:10px;text-align:center}
@@ -1027,11 +1040,11 @@ ${!connected ? `<script>
1027
1040
 
1028
1041
  .btn-row{display:flex;gap:10px;margin-top:20px;width:100%;animation:fade-up .5s ease-out .45s both}
1029
1042
  .btn{flex:1;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:10px;padding:11px 16px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;transition:all .2s}
1030
- .btn-primary{background:linear-gradient(135deg,#AF27E3,#FB4072);color:#fff}
1043
+ .btn-primary{background:linear-gradient(135deg, #0166FF, #009AFE);color:#fff}
1031
1044
  .btn-primary:hover{opacity:.9}
1032
1045
  .btn-primary:disabled{opacity:.5;cursor:not-allowed}
1033
1046
  .btn-ghost{background:#1a1a1a;border:1px solid rgba(255,255,255,0.1);color:#999}
1034
- .btn-ghost:hover{border-color:rgba(175,39,227,0.4);color:#f5f5f5}
1047
+ .btn-ghost:hover{border-color:rgba(0, 105, 254,0.4);color:#f5f5f5}
1035
1048
 
1036
1049
  .err{color:#FB4072;font-size:13px;margin-top:14px;min-height:1.2em}
1037
1050
 
@@ -1429,7 +1442,7 @@ mint();
1429
1442
  try {
1430
1443
  const [status, recentRaw] = await Promise.all([
1431
1444
  workerApi('/api/onboard/status'),
1432
- workerApi(`/api/conversations/${convId}/messages/recent?limit=20`),
1445
+ workerApi(`/api/conversations/${convId}/messages/recent?limit=30`),
1433
1446
  ]);
1434
1447
  botName = status.agentName || 'Bloby';
1435
1448
  humanName = status.userName || 'Human';
@@ -1894,6 +1907,22 @@ mint();
1894
1907
  endConversation(convId);
1895
1908
  runDeferredUpdate();
1896
1909
  }
1910
+
1911
+ // Proactive session recycling (see CONTEXT_RECYCLE_TOKENS). Only when the
1912
+ // harness reports the session idle (no queued message) — and this handler runs
1913
+ // synchronously from the harness callback, so nothing can be pushed between the
1914
+ // idle check and endConversation. Recycling here therefore never drops a queued
1915
+ // message; the next user message re-injects recent history + memory.
1916
+ if (eventData.idle && hasConversation(convId)) {
1917
+ const used = typeof eventData.contextTokens === 'number' ? eventData.contextTokens : 0;
1918
+ const window = typeof eventData.contextWindow === 'number' ? eventData.contextWindow : 0;
1919
+ const recycleAt = window > 0 ? Math.floor(window * CONTEXT_RECYCLE_FRACTION) : CONTEXT_RECYCLE_TOKENS;
1920
+ if (used > recycleAt) {
1921
+ log.info(`[orchestrator] Context ~${used} tok > ${recycleAt} (window=${window || 'n/a'}) — recycling session ${convId}; next message re-injects recent + memory.`);
1922
+ endConversation(convId);
1923
+ }
1924
+ }
1925
+
1897
1926
  broadcastBloby('bot:idle', { conversationId: convId });
1898
1927
  return;
1899
1928
  }
@@ -2227,7 +2256,7 @@ mint();
2227
2256
  try {
2228
2257
  const [status, recentRaw] = await Promise.all([
2229
2258
  workerApi('/api/onboard/status') as Promise<any>,
2230
- workerApi(`/api/conversations/${convId}/messages/recent?limit=20`) as Promise<any[]>,
2259
+ workerApi(`/api/conversations/${convId}/messages/recent?limit=30`) as Promise<any[]>,
2231
2260
  ]);
2232
2261
  botName = status.agentName || 'Bloby';
2233
2262
  humanName = status.userName || 'Human';
@@ -279,7 +279,7 @@ If your human asks you to update a skill's behavior, edit the files INSIDE `skil
279
279
  You can communicate through several surfaces at once. The two built-in ones are:
280
280
 
281
281
  - **`[PWA]`** — the chat bubble in the dashboard (web app / PWA). This is the main one: the floating Bloby widget your human clicks open, the conversation you're reading right now if no other tag is present. Treat it as your home base.
282
- - **`[Mac]`** — the Morphy native Mac app living in the MacBook notch. Your human held a hotkey, spoke, and Morphy sent the transcript here. Screenshots of their screen may be attached. Keep replies concise — they'll be spoken aloud via TTS. No markdown, no bullet lists — plain spoken sentences only.
282
+ - **`[Mac]`** — the Morphy native Mac app living in the MacBook notch. Your human held a hotkey, spoke, and Morphy sent the transcript here. Screenshots of their screen may be attached. Keep replies concise — they'll be spoken aloud via TTS. No markdown, no bullet lists — plain spoken sentences only. You may **optionally** accompany the reply with a small visual card in the notch, two ways: **(1) a PRESET (preferred)** — send structured data and Morphy renders a beautiful, on-brand card for you: `<notch_card type="email">{ "from": "...", "subject": "...", "time": "...", "body": "..." }</notch_card>`. Preset types: `email`, `list`, `calendar`, `weather`, `ticker`, `stat`, `info`, `text`, `comparison` (full schemas in `skills/mac/presets/PRESETS.md`). The body is one JSON object; you never write CSS. **(2) CUSTOM (escape hatch)** — for a bespoke *visual layout* you hand-build with real markup, use `<notch_html>…</notch_html>` (lowercase, exactly that spelling — NOT `<NotchHTML>`, NOT `<notch>`). For long prose / a "read me this" / a summary, use the `text` PRESET instead — never dump plain paragraphs into `<notch_html>`, it's HTML (not a text box) and renders unstyled and edge-to-edge. Send **at most one** card block per reply (a `<notch_card>` OR a `<notch_html>`, never both). Both tags are stripped from TTS, so card contents are **never** spoken. Canvas is **fixed 383 × 147 pt, transparent over BLACK**; custom cards use light/white text, no external assets (no `<img src>`, no fonts, no JS network) and no interactive elements (clicks do nothing) — though long content IS scrollable, the user scrolls it with the trackpad, so letting content overflow is fine. **CRITICAL: never speak the contents of your card.** If the card carries a list / calendar / email / news / comparison / any structured answer, the spoken text MUST be a short lead-in ONLY ("Here are today's top five, Bruno.") and then stop — do not recite the items, the human is already looking at them. If the answer fits in one spoken sentence, send NO card. Voice and card are complementary, never redundant — pick which one carries the detail per reply, and let the other be brief or absent. When in doubt, **open `skills/mac/SKILL.md` and follow it** — it has the preset catalog, examples (including the canonical bad/good comparison of the speak-the-card duplication failure), custom templates under `skills/mac/frequentSnippets/`, and a pre-send checklist. **Mis-spelling a tag means the markup reaches TTS and gets spoken at the human** — that's a visible failure, not a silent one.
283
283
  - **`[workspace]`** — a chat-shaped widget your human placed somewhere inside their dashboard *workspace*. It mirrors the main chat, but the context is whatever the human (or you) built it into. It could be a magic-mirror panel on a tablet on the wall, a kiosk/DAC by the front door, a desk dashboard, a car-mounted display, a kitchen screen during cooking — anything you've ever helped them assemble on the workspace that has a chat-style entry point. **Check `MEMORY.md` and the workspace files** to learn the actual purpose of the device this message came from: is this the kitchen tablet asking for a recipe? The hallway mirror asking what's on today's schedule? The garage panel asking about the car? Tailor tone, brevity, and content to that role. A magic mirror should get a short ambient answer, not a long technical paragraph. If you don't yet know what the workspace surface is for, ask once and write it to memory so future `[workspace]` turns are grounded.
284
284
 
285
285
  Beyond those, your human can install additional channels (WhatsApp, Telegram, Discord, Alexa…) as **skills** from the Bloby Marketplace. Each channel skill teaches you the conventions for that surface.
@@ -1,7 +1,7 @@
1
1
  import { Search, Mail, DollarSign, TrendingUp } from 'lucide-react';
2
2
  import { AreaChart, Area, BarChart, Bar, ResponsiveContainer } from 'recharts';
3
3
 
4
- const GRADIENT = 'linear-gradient(to right, #4FF2FE 10%, #BC20DE 55%, #FF6B8A 100%)';
4
+ const GRADIENT = 'linear-gradient(to right, #0166FF 10%, #009AFE 55%, #4AEEFF 100%)';
5
5
  const CARD = 'relative rounded-xl overflow-hidden';
6
6
  const BORDER = 'absolute inset-0 rounded-xl bg-gradient-to-b from-white/[0.08] via-white/[0.02] to-transparent pointer-events-none';
7
7
  const INNER = 'relative rounded-xl bg-[#141414] m-px p-3.5 h-full';
@@ -52,8 +52,8 @@ export default function DashboardPage() {
52
52
  <ResponsiveContainer width="100%" height={48}>
53
53
  <AreaChart data={rev}>
54
54
  <defs>
55
- <linearGradient id="sg" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stopColor="#4FF2FE" /><stop offset="50%" stopColor="#BC20DE" /><stop offset="100%" stopColor="#FE546B" /></linearGradient>
56
- <linearGradient id="sf" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor="#BC20DE" stopOpacity={0.12} /><stop offset="100%" stopColor="#BC20DE" stopOpacity={0} /></linearGradient>
55
+ <linearGradient id="sg" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stopColor="#0166FF" /><stop offset="50%" stopColor="#009AFE" /><stop offset="100%" stopColor="#4AEEFF" /></linearGradient>
56
+ <linearGradient id="sf" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor="#009AFE" stopOpacity={0.12} /><stop offset="100%" stopColor="#009AFE" stopOpacity={0} /></linearGradient>
57
57
  </defs>
58
58
  <Area type="monotone" dataKey="v" stroke="url(#sg)" strokeWidth={1.5} fill="url(#sf)" />
59
59
  </AreaChart>
@@ -79,7 +79,7 @@ export default function DashboardPage() {
79
79
  <ResponsiveContainer width="100%" height={40}>
80
80
  <BarChart data={fol} barCategoryGap="25%">
81
81
  <defs>
82
- <linearGradient id="xg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor="#BC20DE" stopOpacity={0.3} /><stop offset="100%" stopColor="#4FF2FE" stopOpacity={0.05} /></linearGradient>
82
+ <linearGradient id="xg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor="#009AFE" stopOpacity={0.3} /><stop offset="100%" stopColor="#0166FF" stopOpacity={0.05} /></linearGradient>
83
83
  </defs>
84
84
  <Bar dataKey="v" fill="url(#xg)" radius={[2, 2, 0, 0]} />
85
85
  </BarChart>
@@ -40,7 +40,7 @@ export default function Sidebar({ userName, botName = 'Bloby', backendStatus = '
40
40
  <h2
41
41
  className="text-4xl font-bold mt-0.5 tracking-tight leading-[1.08] w-fit"
42
42
  style={{
43
- backgroundImage: 'linear-gradient(to right, #4FF2FE, #BC20DE, #FE546B)',
43
+ backgroundImage: 'linear-gradient(to right, #0166FF, #009AFE, #4AEEFF)',
44
44
  WebkitBackgroundClip: 'text',
45
45
  WebkitTextFillColor: 'transparent',
46
46
  backgroundClip: 'text',
@@ -28,7 +28,7 @@
28
28
  }
29
29
 
30
30
  .bloby-tour-popover .driver-popover-next-btn {
31
- background: linear-gradient(to right, #4FF2FE, #BC20DE, #FE546B) !important;
31
+ background: linear-gradient(to right, #0166FF, #009AFE, #4AEEFF) !important;
32
32
  color: #fff !important;
33
33
  border: none !important;
34
34
  border-radius: 8px !important;
@@ -10,7 +10,7 @@
10
10
  --color-card-foreground: #f5f5f5;
11
11
  --color-popover: #2a2a2a;
12
12
  --color-popover-foreground: #f5f5f5;
13
- --color-primary: #3C8FFF;
13
+ --color-primary: #0069FE;
14
14
  --color-primary-foreground: #ffffff;
15
15
  --color-secondary: #333333;
16
16
  --color-secondary-foreground: #f5f5f5;
@@ -22,20 +22,20 @@
22
22
  --color-destructive-foreground: #ffffff;
23
23
  --color-border: #3a3a3a;
24
24
  --color-input: #3a3a3a;
25
- --color-ring: #3C8FFF;
26
- --color-chart-1: #3C8FFF;
25
+ --color-ring: #0069FE;
26
+ --color-chart-1: #0069FE;
27
27
  --color-chart-2: #FD486B;
28
28
  --color-chart-3: #F59E0B;
29
29
  --color-chart-4: #8B5CF6;
30
30
  --color-chart-5: #10B981;
31
31
  --color-sidebar: #1c1c1c;
32
32
  --color-sidebar-foreground: #f5f5f5;
33
- --color-sidebar-primary: #3C8FFF;
33
+ --color-sidebar-primary: #0069FE;
34
34
  --color-sidebar-primary-foreground: #ffffff;
35
35
  --color-sidebar-accent: #282828;
36
36
  --color-sidebar-accent-foreground: #f5f5f5;
37
37
  --color-sidebar-border: #3a3a3a;
38
- --color-sidebar-ring: #3C8FFF;
38
+ --color-sidebar-ring: #0069FE;
39
39
  --radius: 0.75rem;
40
40
  }
41
41
 
@@ -55,7 +55,7 @@ body {
55
55
  }
56
56
 
57
57
  ::selection {
58
- background-color: rgba(175, 39, 227, 0.25);
58
+ background-color: rgba(0, 105, 254, 0.25);
59
59
  }
60
60
 
61
61
  ::-webkit-scrollbar { width: 6px; }
@@ -68,16 +68,16 @@ body {
68
68
  -webkit-background-clip: text;
69
69
  color: transparent;
70
70
  -webkit-text-fill-color: transparent;
71
- background-image: linear-gradient(135deg, #04D1FE, #AF27E3, #FB4072);
71
+ background-image: linear-gradient(135deg, #0166FF, #009AFE, #4AEEFF);
72
72
  }
73
73
 
74
74
  .bg-gradient-brand {
75
- background-image: linear-gradient(135deg, #04D1FE, #AF27E3, #FB4072);
75
+ background-image: linear-gradient(135deg, #0166FF, #009AFE, #4AEEFF);
76
76
  }
77
77
 
78
78
  .glow-border {
79
- box-shadow: 0 0 0 1px rgba(175, 39, 227, 0.1),
80
- 0 0 20px -5px rgba(175, 39, 227, 0.15);
79
+ box-shadow: 0 0 0 1px rgba(0, 105, 254, 0.1),
80
+ 0 0 20px -5px rgba(0, 105, 254, 0.15);
81
81
  }
82
82
 
83
83
  .animated-border {
@@ -90,10 +90,10 @@ body {
90
90
  inset: -150%;
91
91
  background: conic-gradient(
92
92
  from 0deg,
93
- #04D1FE,
94
- #AF27E3,
95
- #FB4072,
96
- #04D1FE
93
+ #0166FF,
94
+ #009AFE,
95
+ #4AEEFF,
96
+ #0166FF
97
97
  );
98
98
  animation: border-spin 3s linear infinite;
99
99
  }
@@ -107,10 +107,10 @@ body {
107
107
  }
108
108
 
109
109
  .input-glow:focus {
110
- border-color: rgba(175, 39, 227, 0.4);
111
- box-shadow: 0 0 0 1px rgba(175, 39, 227, 0.15),
112
- 0 0 20px -5px rgba(175, 39, 227, 0.25),
113
- 0 0 4px -1px rgba(4, 209, 254, 0.1);
110
+ border-color: rgba(0, 105, 254, 0.4);
111
+ box-shadow: 0 0 0 1px rgba(0, 105, 254, 0.15),
112
+ 0 0 20px -5px rgba(0, 105, 254, 0.25),
113
+ 0 0 4px -1px rgba(74, 238, 255, 0.1);
114
114
  }
115
115
 
116
116
  @keyframes border-spin {