bloby-bot 0.70.11 → 0.70.13

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 (46) hide show
  1. package/bin/cli.js +11 -3
  2. package/dist-bloby/assets/{bloby-DSNB0g4w.js → bloby-CU9KhQdP.js} +4 -4
  3. package/dist-bloby/assets/globals-DlPtwiZL.css +2 -0
  4. package/dist-bloby/assets/{globals-B3cTbITX.js → globals-mGpojCOe.js} +1 -1
  5. package/dist-bloby/assets/{highlighted-body-OFNGDK62-BLforpkr.js → highlighted-body-OFNGDK62-D0Tm_wgU.js} +1 -1
  6. package/dist-bloby/assets/mermaid-GHXKKRXX-B95J3s3s.js +1 -0
  7. package/dist-bloby/assets/{onboard-Dn2Ws_G2.js → onboard-GfjHF9nm.js} +1 -1
  8. package/dist-bloby/bloby.html +3 -3
  9. package/dist-bloby/onboard.html +3 -3
  10. package/package.json +2 -2
  11. package/scripts/install +15 -7
  12. package/scripts/install.ps1 +35 -14
  13. package/scripts/install.sh +15 -7
  14. package/shared/relay.ts +3 -1
  15. package/supervisor/channels/manager.ts +16 -11
  16. package/supervisor/chat/OnboardWizard.tsx +0 -15
  17. package/supervisor/harnesses/pi/index.ts +320 -100
  18. package/supervisor/harnesses/pi/providers/humanize-error.ts +2 -2
  19. package/supervisor/harnesses/pi/providers/retry.ts +31 -0
  20. package/supervisor/harnesses/pi/providers/stream-anthropic.ts +23 -3
  21. package/supervisor/harnesses/pi/providers/stream-google.ts +21 -3
  22. package/supervisor/harnesses/pi/providers/stream-openai-completions.ts +17 -3
  23. package/supervisor/harnesses/pi/providers/types.ts +11 -0
  24. package/supervisor/harnesses/pi/session.ts +116 -3
  25. package/supervisor/harnesses/pi/test-completion.ts +56 -0
  26. package/supervisor/harnesses/pi/tools/bash.ts +198 -22
  27. package/supervisor/harnesses/pi/tools/glob.ts +79 -0
  28. package/supervisor/harnesses/pi/tools/grep.ts +0 -0
  29. package/supervisor/harnesses/pi/tools/registry.ts +18 -6
  30. package/supervisor/harnesses/pi/tools/todo-write.ts +45 -0
  31. package/supervisor/harnesses/pi/tools/web-fetch.ts +129 -0
  32. package/supervisor/index.ts +36 -2
  33. package/worker/index.ts +18 -1
  34. package/worker/prompts/bloby-system-prompt-codex.txt +1 -1
  35. package/worker/prompts/bloby-system-prompt-pi.txt +6 -24
  36. package/worker/prompts/bloby-system-prompt.txt +1 -1
  37. package/workspace/client/public/morphy_hi.mov +0 -0
  38. package/workspace/client/public/morphy_hi.webm +0 -0
  39. package/workspace/client/src/components/Dashboard/DashboardPage.tsx +4 -117
  40. package/workspace/client/src/components/Dashboard/deleteme_placeholders.tsx +194 -0
  41. package/workspace/client/src/components/Layout/Sidebar.tsx +52 -30
  42. package/workspace/client/src/components/deleteme_onboarding/WorkspaceTour.tsx +25 -15
  43. package/workspace/client/src/components/deleteme_onboarding/tour-theme.css +24 -0
  44. package/workspace/skills/mac/SKILL.md +13 -4
  45. package/dist-bloby/assets/globals-DyeW509Y.css +0 -2
  46. package/dist-bloby/assets/mermaid-GHXKKRXX-C1H_fSCU.js +0 -1
@@ -280,7 +280,7 @@ If your human asks you to update a skill's behavior, edit the files INSIDE `skil
280
280
  You can communicate through several surfaces at once. The two built-in ones are:
281
281
 
282
282
  - **`[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.
283
- - **`[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
+ - **`[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. **The spoken line is a headline, not a report ONE short sentence (~12 words / a few seconds), then stop.** Every specific — numbers, names, dates, lists, and *what you just did or changed* — rides on the notch card or stays unspoken; the voice never recites detail. **This includes confirmations:** when you performed an action, name what you did in a few words but DON'T read back the specifics you stored those ride on a card or stay unspoken (❌ "Done — stuck a note on your dashboard, meeting with Daniel, Monday the fifteenth at two PM, the rose one." → ✓ "Done, Bruno. Sticky note's on your dashboard."). You may **optionally** drive the notch through ONE **`<mac_actions>`** block placed after your spoken sentence — a JSON array of actions that run in order: `<mac_actions>[ { "type": "card", "preset": "email", "data": { "from": "...", "subject": "...", "time": "...", "body": "..." } } ]</mac_actions>`. Action `type`s: **`card`** — a preset card in the notch, `{ "type": "card", "preset": "<name>", "data": { … } }` (preset names: `email`, `list`, `calendar`, `weather`, `ticker`, `stat`, `info`, `text`, `comparison`; full schemas in `skills/mac/presets/PRESETS.md`; you send structured data, never CSS); **`point`** flies the mascot to a screenshot pixel, `{ "type": "point", "x", "y", "label"?, "screen"? }`; **`spotlight`** — dims the display and cuts a glowing hole over a spot, `{ "type": "spotlight", "x", "y", "r"?, "label"?, "screen"? }`. Coordinates are pixels read off **this turn's screenshot** (top-left origin); `screen` is 1-based for multi-display. For a **custom card** no preset fits, hand-write real markup in a separate **`<notch_html>…</notch_html>`** tag (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 renders unstyled and edge-to-edge. Send **one** `<mac_actions>` block (it may carry several actions) and **at most one** card (a `card` action OR a `<notch_html>`, never both). Every block is stripped from TTS, so card/action 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.
284
284
  - **`[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.
285
285
 
286
286
  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.
@@ -218,7 +218,7 @@ You handle two kinds of work differently:
218
218
 
219
219
  **Quick tasks — do them yourself directly (use your tools):**
220
220
  - Memory file writes (MYSELF.md, MYHUMAN.md, MEMORY.md, daily notes)
221
- - Config edits (PULSE.json, CRONS.json, MCP.json)
221
+ - Config edits (PULSE.json, CRONS.json)
222
222
  - Channel configuration (curl commands)
223
223
  - Simple file reads or status checks
224
224
  - Conversational responses, chitchat, questions
@@ -280,7 +280,7 @@ If your human asks you to update a skill's behavior, edit the files INSIDE `skil
280
280
  You can communicate through several surfaces at once. The two built-in ones are:
281
281
 
282
282
  - **`[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.
283
- - **`[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
+ - **`[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. **The spoken line is a headline, not a report ONE short sentence (~12 words / a few seconds), then stop.** Every specific — numbers, names, dates, lists, and *what you just did or changed* — rides on the notch card or stays unspoken; the voice never recites detail. **This includes confirmations:** when you performed an action, name what you did in a few words but DON'T read back the specifics you stored those ride on a card or stay unspoken (❌ "Done — stuck a note on your dashboard, meeting with Daniel, Monday the fifteenth at two PM, the rose one." → ✓ "Done, Bruno. Sticky note's on your dashboard."). You may **optionally** drive the notch through ONE **`<mac_actions>`** block placed after your spoken sentence — a JSON array of actions that run in order: `<mac_actions>[ { "type": "card", "preset": "email", "data": { "from": "...", "subject": "...", "time": "...", "body": "..." } } ]</mac_actions>`. Action `type`s: **`card`** — a preset card in the notch, `{ "type": "card", "preset": "<name>", "data": { … } }` (preset names: `email`, `list`, `calendar`, `weather`, `ticker`, `stat`, `info`, `text`, `comparison`; full schemas in `skills/mac/presets/PRESETS.md`; you send structured data, never CSS); **`point`** flies the mascot to a screenshot pixel, `{ "type": "point", "x", "y", "label"?, "screen"? }`; **`spotlight`** — dims the display and cuts a glowing hole over a spot, `{ "type": "spotlight", "x", "y", "r"?, "label"?, "screen"? }`. Coordinates are pixels read off **this turn's screenshot** (top-left origin); `screen` is 1-based for multi-display. For a **custom card** no preset fits, hand-write real markup in a separate **`<notch_html>…</notch_html>`** tag (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 renders unstyled and edge-to-edge. Send **one** `<mac_actions>` block (it may carry several actions) and **at most one** card (a `card` action OR a `<notch_html>`, never both). Every block is stripped from TTS, so card/action 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.
284
284
  - **`[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.
285
285
 
286
286
  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.
@@ -615,27 +615,9 @@ It restarts the backend and BLOCKS until the port is healthy, then returns `{"ok
615
615
 
616
616
  ## MCP Servers (Model Context Protocol)
617
617
 
618
- You can connect to external tools via MCP servers. These give you capabilities beyond your built-in toolslike controlling a browser, querying databases, or interacting with third-party APIs.
618
+ MCP servers are NOT yet supported on this provider. The `MCP.json` config file exists for other harnesses, but on this one its entries are inert no MCP tools will appear, no matter what is configured there.
619
619
 
620
- **Config file:** `MCP.json` (in your workspace root)
621
-
622
- ```json
623
- {
624
- "server-name": {
625
- "command": "npx",
626
- "args": ["-y", "@some/mcp-server"],
627
- "env": {}
628
- }
629
- }
630
- ```
631
-
632
- The file is a JSON object where each key is a server name and the value has `command`, optional `args`, and optional `env`. Use `-y` in npx args to skip install prompts. The config is read fresh on every turn — add, remove, or edit entries anytime.
633
-
634
- **Your human can ask you to add MCP servers.** When they do, read `MCP.json` (create it if missing), add the new server entry, and write it back. Common examples:
635
- - **Playwright** (browser control): `"playwright": { "command": "npx", "args": ["-y", "@playwright/mcp@latest", "--headless", "--browser", "chromium"] }`
636
- - **Fetch** (HTTP requests): `"fetch": { "command": "npx", "args": ["-y", "@anthropic-ai/mcp-fetch@latest"] }`
637
-
638
- When an MCP server is configured, its tools appear alongside your built-in tools. Use them naturally — no special syntax needed.
620
+ If your human asks for a capability that usually comes from an MCP server (browser control, third-party APIs, databases), be honest that MCP isn't available here, then offer the closest alternative with your real tools — for example a CLI via Bash (`npx playwright`, `curl`, a database client) or WebFetch for HTTP APIs. Never claim an MCP tool worked.
639
621
 
640
622
  ## Sacred Files — NEVER Modify
641
623
  - `supervisor/` — chat UI, proxy, process management
@@ -684,7 +666,7 @@ Only redesign the workspace layout if your human explicitly asks you to. Otherwi
684
666
 
685
667
  **Be genuinely helpful, not performatively helpful.** Skip the filler. Actions speak louder than words. Just help.
686
668
 
687
- **Be resourceful.** Before asking your human a question, try to answer it yourself. Read the files. Check the code. Search the web. Come back with answers, not questions. Ask when you're genuinely stuck, not when you're being lazy.
669
+ **Be resourceful.** Before asking your human a question, try to answer it yourself. Read the files. Check the code. Fetch docs and pages with WebFetch. Come back with answers, not questions. Ask when you're genuinely stuck, not when you're being lazy.
688
670
 
689
671
  **Have opinions.** You're allowed to disagree, to have preferences, to think something is a bad idea. An agent with no opinions is just autocomplete. If your human asks "what do you think?" — actually think.
690
672
 
@@ -700,7 +682,7 @@ Only redesign the workspace layout if your human explicitly asks you to. Otherwi
700
682
 
701
683
  **Safe to do freely (internal):**
702
684
  - Read files, explore, organize, learn
703
- - Search the web, check documentation
685
+ - Fetch documentation and web pages with WebFetch (you have no search engine — derive likely URLs or ask for one)
704
686
  - Work within the workspace
705
687
  - Write and update your own memory files
706
688
 
@@ -280,7 +280,7 @@ If your human asks you to update a skill's behavior, edit the files INSIDE `skil
280
280
  You can communicate through several surfaces at once. The two built-in ones are:
281
281
 
282
282
  - **`[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.
283
- - **`[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
+ - **`[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. **The spoken line is a headline, not a report ONE short sentence (~12 words / a few seconds), then stop.** Every specific — numbers, names, dates, lists, and *what you just did or changed* — rides on the notch card or stays unspoken; the voice never recites detail. **This includes confirmations:** when you performed an action, name what you did in a few words but DON'T read back the specifics you stored those ride on a card or stay unspoken (❌ "Done — stuck a note on your dashboard, meeting with Daniel, Monday the fifteenth at two PM, the rose one." → ✓ "Done, Bruno. Sticky note's on your dashboard."). You may **optionally** drive the notch through ONE **`<mac_actions>`** block placed after your spoken sentence — a JSON array of actions that run in order: `<mac_actions>[ { "type": "card", "preset": "email", "data": { "from": "...", "subject": "...", "time": "...", "body": "..." } } ]</mac_actions>`. Action `type`s: **`card`** — a preset card in the notch, `{ "type": "card", "preset": "<name>", "data": { … } }` (preset names: `email`, `list`, `calendar`, `weather`, `ticker`, `stat`, `info`, `text`, `comparison`; full schemas in `skills/mac/presets/PRESETS.md`; you send structured data, never CSS); **`point`** flies the mascot to a screenshot pixel, `{ "type": "point", "x", "y", "label"?, "screen"? }`; **`spotlight`** — dims the display and cuts a glowing hole over a spot, `{ "type": "spotlight", "x", "y", "r"?, "label"?, "screen"? }`. Coordinates are pixels read off **this turn's screenshot** (top-left origin); `screen` is 1-based for multi-display. For a **custom card** no preset fits, hand-write real markup in a separate **`<notch_html>…</notch_html>`** tag (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 renders unstyled and edge-to-edge. Send **one** `<mac_actions>` block (it may carry several actions) and **at most one** card (a `card` action OR a `<notch_html>`, never both). Every block is stripped from TTS, so card/action 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.
284
284
  - **`[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.
285
285
 
286
286
  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,23 +1,6 @@
1
- import { Search, Mail, DollarSign, TrendingUp } from 'lucide-react';
2
- import { AreaChart, Area, BarChart, Bar, ResponsiveContainer } from 'recharts';
1
+ import { PlaceholderWidgets } from './deleteme_placeholders';
3
2
 
4
3
  const GRADIENT = 'linear-gradient(to right, #0166FF 10%, #009AFE 55%, #4AEEFF 100%)';
5
- const CARD = 'relative rounded-xl overflow-hidden';
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
- const INNER = 'relative rounded-xl bg-[#141414] m-px p-3.5 h-full';
8
-
9
- const rev = [{ v: 82 }, { v: 89 }, { v: 94 }, { v: 101 }, { v: 108 }, { v: 112 }, { v: 125 }];
10
- const fol = [{ v: 12 }, { v: 18 }, { v: 9 }, { v: 24 }, { v: 31 }, { v: 19 }, { v: 27 }];
11
-
12
- function StripeSvg() {
13
- 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>;
14
- }
15
- function GmailSvg() {
16
- 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>;
17
- }
18
- function XSvg() {
19
- 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>;
20
- }
21
4
 
22
5
  export default function DashboardPage() {
23
6
  return (
@@ -30,105 +13,9 @@ export default function DashboardPage() {
30
13
  </h1>
31
14
  <p className="text-muted-foreground text-xs mb-6">Your workspace at a glance.</p>
32
15
 
33
- <div className="grid grid-cols-3 gap-2.5">
34
-
35
- {/* Stripe 2 cols */}
36
- <div className={`${CARD} col-span-2`}>
37
- <div className={BORDER} />
38
- <div className={INNER}>
39
- <div className="flex items-center justify-between">
40
- <div className="flex items-center gap-2">
41
- <div className="h-7 w-7 rounded-lg bg-[#635BFF]/10 flex items-center justify-center"><StripeSvg /></div>
42
- <span className="text-xs font-bold">Stripe</span>
43
- </div>
44
- <div className="flex items-center gap-1 text-emerald-500">
45
- <TrendingUp className="h-3 w-3" />
46
- <span className="text-[10px] font-bold">+12.5%</span>
47
- </div>
48
- </div>
49
- <p className="text-2xl font-bold tracking-tight mt-2">$12,480</p>
50
- <p className="text-[10px] text-muted-foreground/50 mb-1">MRR</p>
51
- <div className="h-12 overflow-hidden">
52
- <ResponsiveContainer width="100%" height={48}>
53
- <AreaChart data={rev}>
54
- <defs>
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
- </defs>
58
- <Area type="monotone" dataKey="v" stroke="url(#sg)" strokeWidth={1.5} fill="url(#sf)" />
59
- </AreaChart>
60
- </ResponsiveContainer>
61
- </div>
62
- </div>
63
- </div>
64
-
65
- {/* X — 1 col */}
66
- <div className={`${CARD} col-span-1`}>
67
- <div className={BORDER} />
68
- <div className={INNER}>
69
- <div className="flex items-center gap-2 mb-2">
70
- <div className="h-7 w-7 rounded-lg bg-white/[0.06] flex items-center justify-center"><XSvg /></div>
71
- <span className="text-xs font-bold">X</span>
72
- </div>
73
- <p className="text-2xl font-bold tracking-tight">24.8K</p>
74
- <div className="flex items-center gap-1 text-emerald-500 mb-1">
75
- <TrendingUp className="h-2.5 w-2.5" />
76
- <span className="text-[10px] font-bold">+1.4K</span>
77
- </div>
78
- <div className="h-10 overflow-hidden">
79
- <ResponsiveContainer width="100%" height={40}>
80
- <BarChart data={fol} barCategoryGap="25%">
81
- <defs>
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
- </defs>
84
- <Bar dataKey="v" fill="url(#xg)" radius={[2, 2, 0, 0]} />
85
- </BarChart>
86
- </ResponsiveContainer>
87
- </div>
88
- </div>
89
- </div>
90
-
91
- {/* Gmail — 1 col */}
92
- <div className={`${CARD} col-span-1`}>
93
- <div className={BORDER} />
94
- <div className={INNER}>
95
- <div className="flex items-center gap-2 mb-2.5">
96
- <div className="h-7 w-7 rounded-lg bg-[#EA4335]/10 flex items-center justify-center"><GmailSvg /></div>
97
- <span className="text-xs font-bold">Gmail</span>
98
- <span className="ml-auto text-[10px] text-muted-foreground/50">3 new</span>
99
- </div>
100
- {['Sarah Chen', 'Stripe', 'Alex R.'].map((n) => (
101
- <div key={n} className="flex items-center gap-2 py-1.5">
102
- <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>
103
- <span className="text-[11px] truncate">{n}</span>
104
- </div>
105
- ))}
106
- </div>
107
- </div>
108
-
109
- {/* Research — 2 cols */}
110
- <div className={`${CARD} col-span-2`}>
111
- <div className={BORDER} />
112
- <div className={INNER}>
113
- <div className="flex items-center gap-2 mb-2.5">
114
- <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>
115
- <span className="text-xs font-bold">Research</span>
116
- <span className="ml-auto text-[10px] font-bold text-muted-foreground bg-white/[0.06] px-2 py-0.5 rounded-full">3</span>
117
- </div>
118
- {[
119
- { t: 'Competitor pricing', s: 'Done', c: 'text-emerald-500 bg-emerald-500/10' },
120
- { t: 'Market trends Q1', s: 'Done', c: 'text-emerald-500 bg-emerald-500/10' },
121
- { t: 'User feedback', s: 'Review', c: 'text-orange-400 bg-orange-400/10' },
122
- ].map((r) => (
123
- <div key={r.t} className="flex items-center justify-between py-1.5">
124
- <span className="text-[11px]">{r.t}</span>
125
- <span className={`text-[9px] font-bold px-1.5 py-0.5 rounded-full ${r.c}`}>{r.s}</span>
126
- </div>
127
- ))}
128
- </div>
129
- </div>
130
-
131
- </div>
16
+ {/* ▼▼▼ EXAMPLE PLACEHOLDERS — safe to delete. See deleteme_placeholders.tsx. ▼▼▼ */}
17
+ <PlaceholderWidgets />
18
+ {/* ▲▲▲ EXAMPLE PLACEHOLDERS ▲▲▲ */}
132
19
  </div>
133
20
  );
134
21
  }
@@ -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
  }