bloby-bot 0.70.12 → 0.71.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 (65) hide show
  1. package/bin/cli.js +234 -48
  2. package/dist-bloby/assets/{bloby-DSNB0g4w.js → bloby-es6cZJzs.js} +6 -6
  3. package/dist-bloby/assets/globals-DBqwNiJV.css +2 -0
  4. package/dist-bloby/assets/{globals-B3cTbITX.js → globals-DN3F0CQE.js} +1 -1
  5. package/dist-bloby/assets/{highlighted-body-OFNGDK62-BLforpkr.js → highlighted-body-OFNGDK62-8PiOHw9p.js} +1 -1
  6. package/dist-bloby/assets/mermaid-GHXKKRXX-BJWX8urU.js +1 -0
  7. package/dist-bloby/assets/{onboard-Dn2Ws_G2.js → onboard-BKgy17OU.js} +1 -1
  8. package/dist-bloby/bloby.html +3 -3
  9. package/dist-bloby/onboard.html +3 -3
  10. package/package.json +3 -4
  11. package/scripts/install +156 -41
  12. package/scripts/install.ps1 +146 -29
  13. package/scripts/install.sh +156 -41
  14. package/shared/config.ts +37 -2
  15. package/shared/relay.ts +3 -1
  16. package/supervisor/channels/manager.ts +84 -44
  17. package/supervisor/channels/telegram.ts +57 -16
  18. package/supervisor/channels/types.ts +4 -1
  19. package/supervisor/channels/whatsapp.ts +57 -10
  20. package/supervisor/chat/OnboardWizard.tsx +0 -15
  21. package/supervisor/chat/src/components/Chat/AudioBubble.tsx +1 -1
  22. package/supervisor/chat/src/components/Chat/AuthedImage.tsx +16 -3
  23. package/supervisor/chat/src/components/Chat/BlobyImageCard.tsx +2 -2
  24. package/supervisor/chat/src/components/Chat/ImageLightbox.tsx +25 -8
  25. package/supervisor/chat/src/components/Chat/InputBar.tsx +62 -7
  26. package/supervisor/chat/src/components/Chat/MessageBubble.tsx +37 -18
  27. package/supervisor/chat/src/components/Chat/MessageList.tsx +3 -3
  28. package/supervisor/chat/src/hooks/useChat.ts +52 -0
  29. package/supervisor/chat/src/lib/authedFile.ts +24 -12
  30. package/supervisor/file-saver.ts +92 -19
  31. package/supervisor/harnesses/attachment-policy.ts +111 -0
  32. package/supervisor/harnesses/claude.ts +62 -15
  33. package/supervisor/harnesses/codex.ts +69 -43
  34. package/supervisor/harnesses/pi/index.ts +367 -112
  35. package/supervisor/harnesses/pi/providers/humanize-error.ts +27 -2
  36. package/supervisor/harnesses/pi/providers/retry.ts +31 -0
  37. package/supervisor/harnesses/pi/providers/stream-anthropic.ts +31 -3
  38. package/supervisor/harnesses/pi/providers/stream-google.ts +26 -3
  39. package/supervisor/harnesses/pi/providers/stream-openai-completions.ts +32 -9
  40. package/supervisor/harnesses/pi/providers/types.ts +29 -1
  41. package/supervisor/harnesses/pi/session.ts +143 -3
  42. package/supervisor/harnesses/pi/test-completion.ts +56 -0
  43. package/supervisor/harnesses/pi/tools/bash.ts +198 -22
  44. package/supervisor/harnesses/pi/tools/glob.ts +79 -0
  45. package/supervisor/harnesses/pi/tools/grep.ts +0 -0
  46. package/supervisor/harnesses/pi/tools/registry.ts +18 -6
  47. package/supervisor/harnesses/pi/tools/todo-write.ts +45 -0
  48. package/supervisor/harnesses/pi/tools/web-fetch.ts +129 -0
  49. package/supervisor/index.ts +93 -18
  50. package/supervisor/widget.js +19 -5
  51. package/worker/db.ts +2 -0
  52. package/worker/index.ts +18 -1
  53. package/worker/prompts/bloby-system-prompt-codex.txt +1 -1
  54. package/worker/prompts/bloby-system-prompt-pi.txt +6 -24
  55. package/worker/prompts/bloby-system-prompt.txt +1 -1
  56. package/workspace/client/src/components/Dashboard/DashboardPage.tsx +4 -117
  57. package/workspace/client/src/components/Dashboard/deleteme_placeholders.tsx +194 -0
  58. package/workspace/client/src/components/Layout/Sidebar.tsx +52 -30
  59. package/workspace/client/src/components/deleteme_onboarding/WorkspaceTour.tsx +25 -15
  60. package/workspace/client/src/components/deleteme_onboarding/tour-theme.css +24 -0
  61. package/workspace/skills/mac/SKILL.md +13 -4
  62. package/dist-bloby/assets/globals-DyeW509Y.css +0 -2
  63. package/dist-bloby/assets/mermaid-GHXKKRXX-C1H_fSCU.js +0 -1
  64. package/supervisor/public/headphones_spritesheet.webp +0 -0
  65. package/supervisor/public/spritesheet.webp +0 -0
@@ -0,0 +1,194 @@
1
+ /**
2
+ * ┌──────────────────────────────────────────────────────────────────┐
3
+ * │ EXAMPLE / PLACEHOLDER DASHBOARD WIDGETS — SAFE TO DELETE │
4
+ * └──────────────────────────────────────────────────────────────────┘
5
+ *
6
+ * Everything in this file is demo content to show what a Bloby workspace
7
+ * can look like. NONE of it is connected to real data.
8
+ *
9
+ * To start fresh with the user's real widgets:
10
+ * 1. Delete this entire file (deleteme_placeholders.tsx).
11
+ * 2. In DashboardPage.tsx, remove the `PlaceholderWidgets` import and the
12
+ * <PlaceholderWidgets /> usage.
13
+ * The dashboard will then be empty — ready for real apps/widgets.
14
+ *
15
+ * It is intentionally fully self-contained (own data, icons, styles, and the
16
+ * dismissible "these are examples" banner) so removal is a one-file delete.
17
+ */
18
+ import { useState } from 'react';
19
+ import { Search, TrendingUp, Sparkles, X as XIcon } from 'lucide-react';
20
+ import { AreaChart, Area, BarChart, Bar, ResponsiveContainer } from 'recharts';
21
+
22
+ const DISMISS_KEY = 'bloby_example_widgets_dismissed';
23
+
24
+ const CARD = 'relative rounded-xl overflow-hidden';
25
+ const BORDER = 'absolute inset-0 rounded-xl bg-gradient-to-b from-white/[0.08] via-white/[0.02] to-transparent pointer-events-none';
26
+ const INNER = 'relative rounded-xl bg-[#141414] m-px p-3.5 h-full';
27
+
28
+ const rev = [{ v: 82 }, { v: 89 }, { v: 94 }, { v: 101 }, { v: 108 }, { v: 112 }, { v: 125 }];
29
+ const fol = [{ v: 12 }, { v: 18 }, { v: 9 }, { v: 24 }, { v: 31 }, { v: 19 }, { v: 27 }];
30
+
31
+ function StripeSvg() {
32
+ return <svg className="h-3.5 w-3.5 text-[#635BFF]" viewBox="0 0 24 24" fill="currentColor"><path d="M13.976 9.15c-2.172-.806-3.356-1.426-3.356-2.409 0-.831.683-1.305 1.901-1.305 2.227 0 4.515.858 6.09 1.631l.89-5.494C18.252.975 15.697 0 12.165 0 9.667 0 7.589.654 6.104 1.872 4.56 3.147 3.757 4.992 3.757 7.218c0 4.039 2.467 5.76 6.476 7.219 2.585.92 3.445 1.574 3.445 2.583 0 .98-.84 1.545-2.354 1.545-1.875 0-4.965-.921-6.99-2.109l-.9 5.555C5.175 22.99 8.385 24 11.714 24c2.641 0 4.843-.624 6.328-1.813 1.664-1.305 2.525-3.236 2.525-5.732 0-4.128-2.524-5.851-6.591-7.305z" /></svg>;
33
+ }
34
+ function GmailSvg() {
35
+ return <svg className="h-3.5 w-3.5 text-[#EA4335]" viewBox="0 0 24 24" fill="currentColor"><path d="M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-2.023 2.309-3.178 3.927-1.964L5.455 4.64 12 9.548l6.545-4.91 1.528-1.145C21.69 2.28 24 3.434 24 5.457z" /></svg>;
36
+ }
37
+ function XSvg() {
38
+ return <svg className="h-3.5 w-3.5 text-white" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" /></svg>;
39
+ }
40
+
41
+ /** Small "Example" tag shown on each placeholder card. */
42
+ function ExampleBadge() {
43
+ return (
44
+ <span className="inline-flex items-center text-[8.5px] font-bold uppercase tracking-wider text-muted-foreground/60 bg-white/[0.05] border border-white/[0.06] px-1.5 py-0.5 rounded">
45
+ Example
46
+ </span>
47
+ );
48
+ }
49
+
50
+ export function PlaceholderWidgets() {
51
+ const [dismissed, setDismissed] = useState(() => {
52
+ try { return localStorage.getItem(DISMISS_KEY) === '1'; } catch { return false; }
53
+ });
54
+
55
+ function dismissBanner() {
56
+ setDismissed(true);
57
+ try { localStorage.setItem(DISMISS_KEY, '1'); } catch {}
58
+ }
59
+
60
+ return (
61
+ <>
62
+ {/* "These are examples" banner — dismissible, remembers via localStorage */}
63
+ {!dismissed && (
64
+ <div className="relative flex items-start gap-3 mb-4 rounded-xl bg-[#141414] border border-white/[0.07] p-3.5">
65
+ <div
66
+ className="h-7 w-7 rounded-lg flex items-center justify-center shrink-0"
67
+ style={{ background: 'linear-gradient(135deg, #0166FF, #4AEEFF)' }}
68
+ >
69
+ <Sparkles className="h-4 w-4 text-white" />
70
+ </div>
71
+ <div className="flex-1 min-w-0">
72
+ <p className="text-xs font-bold">These are example widgets</p>
73
+ <p className="text-[11px] text-muted-foreground/70 mt-0.5 leading-snug">
74
+ Just a preview of what your workspace can look like — none of it is real data.
75
+ Ask Bloby to replace them with your own apps.
76
+ </p>
77
+ </div>
78
+ <button
79
+ type="button"
80
+ onClick={dismissBanner}
81
+ aria-label="Dismiss"
82
+ className="text-muted-foreground/50 hover:text-foreground transition-colors shrink-0 -mr-1 -mt-1 p-1"
83
+ >
84
+ <XIcon className="h-4 w-4" />
85
+ </button>
86
+ </div>
87
+ )}
88
+
89
+ <div className="grid grid-cols-3 gap-2.5">
90
+
91
+ {/* Stripe — 2 cols */}
92
+ <div className={`${CARD} col-span-2`}>
93
+ <div className={BORDER} />
94
+ <div className={INNER}>
95
+ <div className="flex items-center justify-between">
96
+ <div className="flex items-center gap-2">
97
+ <div className="h-7 w-7 rounded-lg bg-[#635BFF]/10 flex items-center justify-center"><StripeSvg /></div>
98
+ <span className="text-xs font-bold">Stripe</span>
99
+ <ExampleBadge />
100
+ </div>
101
+ <div className="flex items-center gap-1 text-emerald-500">
102
+ <TrendingUp className="h-3 w-3" />
103
+ <span className="text-[10px] font-bold">+12.5%</span>
104
+ </div>
105
+ </div>
106
+ <p className="text-2xl font-bold tracking-tight mt-2">$12,480</p>
107
+ <p className="text-[10px] text-muted-foreground/50 mb-1">MRR</p>
108
+ <div className="h-12 overflow-hidden">
109
+ <ResponsiveContainer width="100%" height={48}>
110
+ <AreaChart data={rev}>
111
+ <defs>
112
+ <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>
113
+ <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>
114
+ </defs>
115
+ <Area type="monotone" dataKey="v" stroke="url(#sg)" strokeWidth={1.5} fill="url(#sf)" />
116
+ </AreaChart>
117
+ </ResponsiveContainer>
118
+ </div>
119
+ </div>
120
+ </div>
121
+
122
+ {/* X — 1 col */}
123
+ <div className={`${CARD} col-span-1`}>
124
+ <div className={BORDER} />
125
+ <div className={INNER}>
126
+ <div className="flex items-center gap-2 mb-2">
127
+ <div className="h-7 w-7 rounded-lg bg-white/[0.06] flex items-center justify-center"><XSvg /></div>
128
+ <span className="text-xs font-bold">X</span>
129
+ <ExampleBadge />
130
+ </div>
131
+ <p className="text-2xl font-bold tracking-tight">24.8K</p>
132
+ <div className="flex items-center gap-1 text-emerald-500 mb-1">
133
+ <TrendingUp className="h-2.5 w-2.5" />
134
+ <span className="text-[10px] font-bold">+1.4K</span>
135
+ </div>
136
+ <div className="h-10 overflow-hidden">
137
+ <ResponsiveContainer width="100%" height={40}>
138
+ <BarChart data={fol} barCategoryGap="25%">
139
+ <defs>
140
+ <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>
141
+ </defs>
142
+ <Bar dataKey="v" fill="url(#xg)" radius={[2, 2, 0, 0]} />
143
+ </BarChart>
144
+ </ResponsiveContainer>
145
+ </div>
146
+ </div>
147
+ </div>
148
+
149
+ {/* Gmail — 1 col */}
150
+ <div className={`${CARD} col-span-1`}>
151
+ <div className={BORDER} />
152
+ <div className={INNER}>
153
+ <div className="flex items-center gap-2 mb-2.5">
154
+ <div className="h-7 w-7 rounded-lg bg-[#EA4335]/10 flex items-center justify-center"><GmailSvg /></div>
155
+ <span className="text-xs font-bold">Gmail</span>
156
+ <ExampleBadge />
157
+ <span className="ml-auto text-[10px] text-muted-foreground/50">3 new</span>
158
+ </div>
159
+ {['Sarah Chen', 'Stripe', 'Alex R.'].map((n) => (
160
+ <div key={n} className="flex items-center gap-2 py-1.5">
161
+ <div className="h-5 w-5 rounded-full bg-white/[0.06] text-[9px] font-bold flex items-center justify-center shrink-0">{n[0]}</div>
162
+ <span className="text-[11px] truncate">{n}</span>
163
+ </div>
164
+ ))}
165
+ </div>
166
+ </div>
167
+
168
+ {/* Research — 2 cols */}
169
+ <div className={`${CARD} col-span-2`}>
170
+ <div className={BORDER} />
171
+ <div className={INNER}>
172
+ <div className="flex items-center gap-2 mb-2.5">
173
+ <div className="h-7 w-7 rounded-lg bg-[#9235F9]/10 flex items-center justify-center"><Search className="h-3.5 w-3.5 text-[#9235F9]" /></div>
174
+ <span className="text-xs font-bold">Research</span>
175
+ <ExampleBadge />
176
+ <span className="ml-auto text-[10px] font-bold text-muted-foreground bg-white/[0.06] px-2 py-0.5 rounded-full">3</span>
177
+ </div>
178
+ {[
179
+ { t: 'Competitor pricing', s: 'Done', c: 'text-emerald-500 bg-emerald-500/10' },
180
+ { t: 'Market trends Q1', s: 'Done', c: 'text-emerald-500 bg-emerald-500/10' },
181
+ { t: 'User feedback', s: 'Review', c: 'text-orange-400 bg-orange-400/10' },
182
+ ].map((r) => (
183
+ <div key={r.t} className="flex items-center justify-between py-1.5">
184
+ <span className="text-[11px]">{r.t}</span>
185
+ <span className={`text-[9px] font-bold px-1.5 py-0.5 rounded-full ${r.c}`}>{r.s}</span>
186
+ </div>
187
+ ))}
188
+ </div>
189
+ </div>
190
+
191
+ </div>
192
+ </>
193
+ );
194
+ }
@@ -1,4 +1,5 @@
1
1
  import { NavLink } from 'react-router';
2
+ import { motion } from 'framer-motion';
2
3
  import {
3
4
  LayoutDashboard,
4
5
  AppWindow,
@@ -26,13 +27,13 @@ export default function Sidebar({ userName, botName = 'Bloby', backendStatus = '
26
27
  return (
27
28
  <aside className="flex flex-col h-full w-64 bg-transparent p-5 pt-8">
28
29
  {/* Logo */}
29
- <div className="flex items-center gap-2.5 mb-8">
30
+ <div className="flex items-center gap-2.5 mb-8 shrink-0">
30
31
  <img src="/morphy.png" alt={botName} className="h-7 w-auto" />
31
32
  <span className="font-bold text-lg" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>{botName}</span>
32
33
  </div>
33
34
 
34
35
  {/* Greeting */}
35
- <div className="mb-10">
36
+ <div className="mb-10 shrink-0">
36
37
  <h1 className="text-4xl font-bold leading-[1.1]">
37
38
  {getGreeting()}{firstName ? ',' : ''}
38
39
  </h1>
@@ -51,16 +52,19 @@ export default function Sidebar({ userName, botName = 'Bloby', backendStatus = '
51
52
  )}
52
53
  </div>
53
54
 
54
- {/* Navigation */}
55
- <nav className="flex-1 space-y-0.5">
55
+ {/* Navigation — min-h-0 lets this flex child shrink below its content height
56
+ (flex items default to min-height:auto), so the list scrolls instead of
57
+ pushing the status pill past the card's clipped bottom edge. */}
58
+ <nav className="flex-1 min-h-0 overflow-y-auto space-y-0.5">
56
59
  <NavItem to="/" icon={LayoutDashboard} label="Dashboard" onNavigate={onNavigate} />
57
- <NavItem to="/app1" icon={AppWindow} label="App 1" sub="Ask Bloby to create your first app and remove this placeholder" onNavigate={onNavigate} />
58
- <NavItem to="/research" icon={Search} label="Research" sub="Ask Bloby to research anything for you, maybe even daily" onNavigate={onNavigate} />
59
- <NavItem to="/whatelse" icon={CircleHelp} label="What Else?" sub="The sky is the limit, just ask and Bloby will do it!" onNavigate={onNavigate} />
60
+ <NavItem to="/app1" icon={AppWindow} label="App 1" onNavigate={onNavigate} />
61
+ <NavItem to="/research" icon={Search} label="Research" onNavigate={onNavigate} />
62
+ <NavItem to="/whatelse" icon={CircleHelp} label="What Else?" onNavigate={onNavigate} />
60
63
  </nav>
61
64
 
62
- {/* Backend status */}
63
- <div className="rounded-xl bg-[#282828] px-3.5 py-2.5 flex items-center gap-2">
65
+ {/* Backend status — shrink-0 + mt keeps it pinned and visible below the
66
+ scrollable nav, never clipped by the sidebar card's rounded edge. */}
67
+ <div className="rounded-xl bg-[#282828] px-3.5 py-2.5 flex items-center gap-2 shrink-0 mt-3">
64
68
  <span
65
69
  className={`h-2 w-2 rounded-full shrink-0 ${
66
70
  backendStatus === 'healthy' ? 'bg-emerald-500' : 'bg-orange-400 animate-pulse'
@@ -74,38 +78,56 @@ export default function Sidebar({ userName, botName = 'Bloby', backendStatus = '
74
78
  );
75
79
  }
76
80
 
81
+ // Shared-layout spring — the active decoration slides between items on nav change.
82
+ const activeSpring = { type: 'spring' as const, stiffness: 420, damping: 34 };
83
+
84
+ /**
85
+ * The active-item decoration — a single clean recessed dark fill (no stroke, no
86
+ * border, no color). The shared `layoutId` makes it glide from the
87
+ * previously-active item to the new one.
88
+ */
89
+ function ActiveDecoration() {
90
+ return (
91
+ <motion.span
92
+ layoutId="nav-active"
93
+ className="absolute inset-0 rounded-[12px]"
94
+ style={{
95
+ background: '#0a0a0c',
96
+ boxShadow: 'inset 0 1px 4px rgba(0,0,0,0.6)',
97
+ }}
98
+ transition={activeSpring}
99
+ />
100
+ );
101
+ }
102
+
77
103
  function NavItem({
78
104
  to,
79
105
  icon: Icon,
80
106
  label,
81
- sub,
82
107
  onNavigate,
83
108
  }: {
84
109
  to: string;
85
- icon: React.ComponentType<{ className?: string }>;
110
+ icon: React.ComponentType<{ className?: string; strokeWidth?: number }>;
86
111
  label: string;
87
- sub?: string;
88
112
  onNavigate?: () => void;
89
113
  }) {
90
114
  return (
91
- <NavLink
92
- to={to}
93
- end
94
- onClick={onNavigate}
95
- className={({ isActive }) =>
96
- cn(
97
- 'flex items-start gap-3 w-full px-3 py-2.5 rounded-lg text-sm transition-colors',
98
- isActive
99
- ? 'bg-sidebar-accent text-foreground font-medium'
100
- : 'text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50',
101
- )
102
- }
103
- >
104
- <Icon className="h-[18px] w-[18px] mt-0.5 shrink-0" />
105
- <div>
106
- {label}
107
- {sub && <p className="text-[10px] text-muted-foreground/50 font-normal leading-tight mt-0.5">{sub}</p>}
108
- </div>
115
+ <NavLink to={to} end onClick={onNavigate} className="block">
116
+ {({ isActive }) => (
117
+ <div
118
+ className={cn(
119
+ 'relative flex items-center gap-3 w-full px-3 py-2.5 rounded-[12px] text-sm transition-colors',
120
+ // cult-ui dark-theme text colors: active white, inactive #6b6b6d
121
+ isActive
122
+ ? 'text-white font-medium'
123
+ : 'text-[#6b6b6d] hover:text-zinc-400 hover:bg-white/[0.03]',
124
+ )}
125
+ >
126
+ {isActive && <ActiveDecoration />}
127
+ <Icon className="relative z-10 h-[19px] w-[19px] shrink-0" strokeWidth={1.5} />
128
+ <span className="relative z-10">{label}</span>
129
+ </div>
130
+ )}
109
131
  </NavLink>
110
132
  );
111
133
  }
@@ -46,18 +46,19 @@ export default function WorkspaceTour({ disabled = false }: { disabled?: boolean
46
46
  },
47
47
  ];
48
48
 
49
- // Only add chat step if the widget exists (not present in dev:workspace mode)
50
- if (document.getElementById('bloby-widget-toggle')) {
51
- steps.push({
52
- element: '#bloby-widget-toggle',
53
- popover: {
54
- title: 'Chat with Bloby',
55
- description: 'This is where you talk to your Bloby. Tell it what to build, ask questions, or just chat. It is always here for you.',
56
- side: 'left',
57
- align: 'end',
58
- },
59
- });
60
- }
49
+ // Chat step: highlight the bottom-right corner of the viewport where the
50
+ // Bloby chat bubble lives. We point at a fixed 100×100 anchor (rendered
51
+ // below) instead of the bubble element itself, so it works even in
52
+ // dev:workspace mode where the widget isn't injected.
53
+ steps.push({
54
+ element: '#tour-chat-anchor',
55
+ popover: {
56
+ title: 'Chat',
57
+ description: 'Your Bloby lives down here. Click here to talk with your Bloby.',
58
+ side: 'left',
59
+ align: 'end',
60
+ },
61
+ });
61
62
 
62
63
  steps.push({
63
64
  popover: {
@@ -70,7 +71,7 @@ export default function WorkspaceTour({ disabled = false }: { disabled?: boolean
70
71
  <p><strong>Bloby does more than build apps.</strong> It can research topics, manage files, run commands, and help with just about anything. Just ask.</p>
71
72
  <p><strong>Bloby wakes up on its own.</strong> Every 30 minutes, it checks in and can take action proactively. You can adjust this anytime.</p>
72
73
  <p><strong>Schedule anything.</strong> Ask things like "Every day at 6am, send me a briefing" or "In 40 minutes, remind me to call the dentist." Bloby handles cron jobs and one-time reminders.</p>
73
- <p><strong>Install it on your phone.</strong> Bloby is a PWA. You can add it to your home screen and enable push notifications. It only pings you when something actually matters, and you can tune that by asking.</p>
74
+ <p><strong>Install it on your phone.</strong> Bloby works just like a normal app — you can add it to your phone's home screen and get notifications, and it only pings you when something actually matters. Ask Bloby to explain how to install it on your phone.</p>
74
75
  </div>
75
76
  `,
76
77
  },
@@ -81,7 +82,7 @@ export default function WorkspaceTour({ disabled = false }: { disabled?: boolean
81
82
  animate: true,
82
83
  allowClose: true,
83
84
  overlayColor: '#000',
84
- overlayOpacity: 0.75,
85
+ overlayOpacity: 0.92,
85
86
  stagePadding: 12,
86
87
  stageRadius: 16,
87
88
  popoverClass: 'bloby-tour-popover',
@@ -100,5 +101,14 @@ export default function WorkspaceTour({ disabled = false }: { disabled?: boolean
100
101
  return () => clearTimeout(timer);
101
102
  }, [disabled]);
102
103
 
103
- return null;
104
+ // Invisible, non-interactive anchor pinned to the bottom-right 100×100 of the
105
+ // viewport. The chat tour step highlights this, so the overlay cutout frames
106
+ // the corner where the Bloby chat bubble sits.
107
+ return (
108
+ <div
109
+ id="tour-chat-anchor"
110
+ aria-hidden
111
+ style={{ position: 'fixed', right: 0, bottom: 0, width: 100, height: 100, pointerEvents: 'none', zIndex: 0 }}
112
+ />
113
+ );
104
114
  }
@@ -47,6 +47,10 @@
47
47
  font-size: 12px !important;
48
48
  padding: 6px 16px !important;
49
49
  white-space: nowrap !important;
50
+ /* driver.css defaults footer buttons to `text-shadow: 1px 1px 0 #fff`, which
51
+ is invisible on its light theme but renders as a ghosted duplicate of the
52
+ "← Back" text on our dark button. Kill it. */
53
+ text-shadow: none !important;
50
54
  }
51
55
 
52
56
  .bloby-tour-popover .driver-popover-prev-btn:hover {
@@ -67,6 +71,26 @@
67
71
  color: #f5f5f5 !important;
68
72
  }
69
73
 
74
+ /* Blur the dimmed background. driver.js shows the highlighted element through an
75
+ SVG-cutout overlay (z-index 10000) and never lifts the element itself, so a
76
+ backdrop blur on the overlay would blur the highlight too. Fix: blur the
77
+ overlay's backdrop, then lift the active element above the overlay so it
78
+ stays crisp. The popover (z-index 1e9) is already above both. */
79
+ .driver-overlay {
80
+ -webkit-backdrop-filter: blur(4px) !important;
81
+ backdrop-filter: blur(4px) !important;
82
+ }
83
+ .driver-active-element {
84
+ position: relative !important;
85
+ z-index: 10001 !important;
86
+ }
87
+ /* The chat-step anchor is pinned to the viewport corner with position:fixed; the
88
+ generic lift above would force it to position:relative and drop it back into
89
+ document flow. Keep it fixed — the z-index lift still applies. */
90
+ #tour-chat-anchor.driver-active-element {
91
+ position: fixed !important;
92
+ }
93
+
70
94
  .bloby-tour-popover .driver-popover-arrow-side-left,
71
95
  .bloby-tour-popover .driver-popover-arrow-side-right,
72
96
  .bloby-tour-popover .driver-popover-arrow-side-top,
@@ -162,7 +162,7 @@ If the `mac` skill isn't installed, none of this exists — so only emit `<mac_p
162
162
 
163
163
  ## Spoken-text rules
164
164
 
165
- - **One or two sentences max.** It's audio. The human is mid-task don't make them stand still for a paragraph.
165
+ - **One short sentence by default — a headline, not a report.** Aim for ~12 words / a few seconds; add a second sentence only if it carries something the card can't. It's audio the human is mid-task, don't make them stand still for a paragraph.
166
166
  - **No markdown, no bullet lists, no enumerations.** TTS reads symbols literally and it sounds awful.
167
167
  - **Refer to the human by name** if you know it — personal, costs nothing.
168
168
  - **Acknowledge a card/action when you send one** ("Here it is.", "Pinned it up top.", "It's the gear, top-right.") so the visual feels connected to the voice.
@@ -184,6 +184,15 @@ The rule is symmetric: if the **voice alone** is the right answer, send **no car
184
184
  | "Where do I cancel?" | *"Bottom-right, Bruno."* | `spotlight` + `point` |
185
185
  | "What time is it in Tokyo?" | *"It's 8:14 PM in Tokyo."* | **nothing** — voice is enough |
186
186
 
187
+ ### 🚫 The #2 mistake: reading back what you just did
188
+
189
+ When you **performed an action** (saved a note, set a reminder, sent a message, updated the dashboard), **name what you did** in a beat — but **don't recite the specifics you stored**. Confirm the deed; skip the contents. The human asked you to do it; they don't need the meeting, the date, the time, and the color read back at them. If those specifics are worth seeing, put them on a **card** and keep the voice short.
190
+
191
+ **❌ BAD:** *"Done, Bruno. Stuck a note on your dashboard — meeting with Daniel, Monday the fifteenth at two PM. It's the rose one, you'll spot it."*
192
+ **✓ GOOD:** *"Done, Bruno. Sticky note's on your dashboard."* (add a `stat` / `info` / `text` card if the specifics should be visible)
193
+
194
+ Same root rule as #1: **the voice carries the headline, the screen carries the detail** — for answers *and* for actions.
195
+
187
196
  ---
188
197
 
189
198
  ## Examples
@@ -249,7 +258,7 @@ The rule is symmetric: if the **voice alone** is the right answer, send **no car
249
258
 
250
259
  ## What Not To Do
251
260
 
252
- - ❌ **No long monologues.** Two sentences. If you can't, let the card carry it.
261
+ - ❌ **No long monologues.** A headline, not a report — one short sentence; if there's more, let the card carry it.
253
262
  - ❌ **No reading the card aloud.** Voice + card complement, never duplicate.
254
263
  - ⚠️ **Proactive `point`/`spotlight` is mapped against the current screen you can't see** — only use it when you truly know the location, else send a card.
255
264
  - ❌ **No external assets** in custom HTML — no network in the notch view.
@@ -263,8 +272,8 @@ The rule is symmetric: if the **voice alone** is the right answer, send **no car
263
272
  ## Reply Checklist
264
273
 
265
274
  1. Did the message actually start with `[Mac]`? If not, don't use this skill.
266
- 2. Spoken text 2 sentences, no markdown, no enumerations?
267
- 3. 🚫 Does my speech recite what's already in my card? Rewrite it as a lead-in only.
275
+ 2. Spoken text a short headline (one sentence by default), no markdown, no enumerations?
276
+ 3. 🚫 Does my speech recite what's already in my card, **or read back an action I just performed**? Rewrite it as a short lead-in / acknowledgement only.
268
277
  4. If acting on screen, did I read the coordinates off **this turn's screenshot**, and set `screen` if multi-display?
269
278
  5. Is my `<mac_actions>` value **valid JSON** (an array of objects, each with a `type`)?
270
279
  6. For a `card`: did I check **PRESETS.md** first, and is `preset` a real lowercase name with valid `data`?