-
Notifications
You must be signed in to change notification settings - Fork 6
feat(desktop-shell-ball): add bubble region and pinned bubble windows #183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5d18fb7
2da1079
cb45e06
b1e6855
927971b
8664b4b
b5def2a
3cea33c
43b4d79
9d19665
31a8382
a95ad44
6187a56
ccdb926
71ff464
ccbf69d
ec626c9
d7ce857
eb40cb6
0b1c474
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,242 @@ | ||
| # Shell-Ball Bubble Window Implementation Plan | ||
|
|
||
| > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. | ||
|
|
||
| **Goal:** Turn the shell-ball bubble window into a real transparent message stream that shows left/right chat bubbles, scrollable history, edge fade, and subtle new-message motion. | ||
|
|
||
| **Architecture:** Keep the bubble window presentation-only and feed it with a shell-ball-local bubble message snapshot contract. The coordinator remains the source of truth for mock/live-ready bubble data, while the bubble window only renders the list, motion state, and scroll behavior without introducing protocol-layer coupling. | ||
|
|
||
| **Tech Stack:** React 18, TypeScript, shell-ball window sync/coordinator layer, CSS animations, existing shell-ball contract test suite. | ||
|
|
||
| --- | ||
|
|
||
| ## File Map | ||
|
|
||
| - Create: `apps/desktop/src/features/shell-ball/shellBall.bubble.ts` | ||
| - Create: `apps/desktop/src/features/shell-ball/components/ShellBallBubbleMessage.tsx` | ||
| - Modify: `apps/desktop/src/features/shell-ball/shellBall.windowSync.ts` | ||
| - Modify: `apps/desktop/src/features/shell-ball/useShellBallCoordinator.ts` | ||
| - Modify: `apps/desktop/src/features/shell-ball/ShellBallBubbleWindow.tsx` | ||
| - Modify: `apps/desktop/src/features/shell-ball/components/ShellBallBubbleZone.tsx` | ||
| - Modify: `apps/desktop/src/features/shell-ball/shellBall.css` | ||
| - Modify: `apps/desktop/src/features/shell-ball/shellBall.contract.test.ts` | ||
|
|
||
| ## Constraints | ||
|
|
||
| - Bubble window only shows message bubbles; no title, caption, input bar, send area, card shell, border, backdrop panel, or window-level framing. | ||
| - Bubble window background must remain transparent. | ||
| - User messages align right; agent messages align left. | ||
| - History scrolls upward; scrollbar stays hidden. | ||
| - Top and bottom edges fade messages out with a transparent mask effect. | ||
| - New messages enter with a light upward-float plus fade-in motion. | ||
| - This iteration should be live-ready but still shell-ball-local: do not add `/packages/protocol` changes. | ||
|
|
||
| ### Task 1: Define Bubble Message Feed Contract | ||
|
|
||
| **Files:** | ||
| - Create: `apps/desktop/src/features/shell-ball/shellBall.bubble.ts` | ||
| - Modify: `apps/desktop/src/features/shell-ball/shellBall.windowSync.ts` | ||
| - Modify: `apps/desktop/src/features/shell-ball/shellBall.contract.test.ts` | ||
|
|
||
| - [ ] **Step 1: Write the failing contract test** | ||
|
|
||
| Add tests asserting that the shell-ball bubble feed exposes a live-ready message model with: | ||
| - `id` | ||
| - `role` as `user` or `agent` | ||
| - `text` | ||
| - `createdAt` | ||
| - an optional freshness/motion hint for new-message animation | ||
|
|
||
| Also assert that the window snapshot shape includes a `bubbleMessages` array. | ||
|
|
||
| - [ ] **Step 2: Run test to verify it fails** | ||
|
|
||
| Run: `pnpm --dir apps/desktop test:shell-ball` | ||
| Expected: FAIL because no bubble feed contract exists yet. | ||
|
|
||
| - [ ] **Step 3: Write minimal implementation** | ||
|
|
||
| Create `apps/desktop/src/features/shell-ball/shellBall.bubble.ts` with: | ||
| - `ShellBallBubbleMessageRole` | ||
| - `ShellBallBubbleMessage` | ||
| - `createShellBallBubbleMessages(...)` or equivalent shell-ball-local mock/live-ready feed helper | ||
|
|
||
| Update `apps/desktop/src/features/shell-ball/shellBall.windowSync.ts` so the snapshot type includes `bubbleMessages`. | ||
|
|
||
| - [ ] **Step 4: Run test to verify it passes** | ||
|
|
||
| Run: `pnpm --dir apps/desktop test:shell-ball` | ||
| Expected: PASS for the bubble feed contract. | ||
|
|
||
| - [ ] **Step 5: Commit** | ||
|
|
||
| ```bash | ||
| git add apps/desktop/src/features/shell-ball/shellBall.bubble.ts apps/desktop/src/features/shell-ball/shellBall.windowSync.ts apps/desktop/src/features/shell-ball/shellBall.contract.test.ts | ||
| git commit -m "feat(desktop-shell-ball): add bubble message feed contract" | ||
| ``` | ||
|
|
||
| ### Task 2: Feed Bubble Messages Through the Coordinator | ||
|
|
||
| **Files:** | ||
| - Modify: `apps/desktop/src/features/shell-ball/useShellBallCoordinator.ts` | ||
| - Modify: `apps/desktop/src/features/shell-ball/ShellBallBubbleWindow.tsx` | ||
| - Modify: `apps/desktop/src/features/shell-ball/shellBall.contract.test.ts` | ||
|
|
||
| - [ ] **Step 1: Write the failing contract test** | ||
|
|
||
| Add tests asserting that: | ||
| - the coordinator snapshot now carries `bubbleMessages` | ||
| - `ShellBallBubbleWindow` resolves messages from the helper-window snapshot | ||
| - the bubble window no longer depends on only `visualState` to render its body | ||
|
|
||
| - [ ] **Step 2: Run test to verify it fails** | ||
|
|
||
| Run: `pnpm --dir apps/desktop test:shell-ball` | ||
| Expected: FAIL because the coordinator and bubble window do not move message arrays yet. | ||
|
|
||
| - [ ] **Step 3: Write minimal implementation** | ||
|
|
||
| Update `apps/desktop/src/features/shell-ball/useShellBallCoordinator.ts` to include a shell-ball-local bubble message feed in every snapshot. | ||
|
|
||
| Update `apps/desktop/src/features/shell-ball/ShellBallBubbleWindow.tsx` to read `snapshot.bubbleMessages` and pass them into the bubble zone. | ||
|
|
||
| - [ ] **Step 4: Run test to verify it passes** | ||
|
|
||
| Run: `pnpm --dir apps/desktop test:shell-ball` | ||
| Expected: PASS for coordinator/bubble-window snapshot coverage. | ||
|
|
||
| - [ ] **Step 5: Commit** | ||
|
|
||
| ```bash | ||
| git add apps/desktop/src/features/shell-ball/useShellBallCoordinator.ts apps/desktop/src/features/shell-ball/ShellBallBubbleWindow.tsx apps/desktop/src/features/shell-ball/shellBall.contract.test.ts | ||
| git commit -m "feat(desktop-shell-ball): sync bubble messages to helper window" | ||
| ``` | ||
|
|
||
| ### Task 3: Replace the Placeholder Bubble Zone With a Real Message List | ||
|
|
||
| **Files:** | ||
| - Create: `apps/desktop/src/features/shell-ball/components/ShellBallBubbleMessage.tsx` | ||
| - Modify: `apps/desktop/src/features/shell-ball/components/ShellBallBubbleZone.tsx` | ||
| - Modify: `apps/desktop/src/features/shell-ball/shellBall.contract.test.ts` | ||
|
|
||
| - [ ] **Step 1: Write the failing render test** | ||
|
|
||
| Add tests asserting that: | ||
| - bubble zone renders message items instead of placeholder shells | ||
| - `agent` messages align left | ||
| - `user` messages align right | ||
| - no title/input/toolbar text is rendered | ||
|
|
||
| - [ ] **Step 2: Run test to verify it fails** | ||
|
|
||
| Run: `pnpm --dir apps/desktop test:shell-ball` | ||
| Expected: FAIL because the bubble zone still renders placeholder ellipses. | ||
|
|
||
| - [ ] **Step 3: Write minimal implementation** | ||
|
|
||
| Create `apps/desktop/src/features/shell-ball/components/ShellBallBubbleMessage.tsx` for one message bubble. | ||
|
|
||
| Refactor `apps/desktop/src/features/shell-ball/components/ShellBallBubbleZone.tsx` to: | ||
| - render a transparent scroll container | ||
| - render only message bubbles | ||
| - align left/right by `role` | ||
| - remove the two placeholder shell bars | ||
|
|
||
| - [ ] **Step 4: Run test to verify it passes** | ||
|
|
||
| Run: `pnpm --dir apps/desktop test:shell-ball` | ||
| Expected: PASS for real message-list rendering. | ||
|
|
||
| - [ ] **Step 5: Commit** | ||
|
|
||
| ```bash | ||
| git add apps/desktop/src/features/shell-ball/components/ShellBallBubbleMessage.tsx apps/desktop/src/features/shell-ball/components/ShellBallBubbleZone.tsx apps/desktop/src/features/shell-ball/shellBall.contract.test.ts | ||
| git commit -m "feat(desktop-shell-ball): render bubble message list" | ||
| ``` | ||
|
|
||
| ### Task 4: Style the Bubble Window for Transparent History, Hidden Scrollbars, Edge Fade, and New-Message Motion | ||
|
|
||
| **Files:** | ||
| - Modify: `apps/desktop/src/features/shell-ball/shellBall.css` | ||
| - Modify: `apps/desktop/src/features/shell-ball/components/ShellBallBubbleZone.tsx` | ||
| - Modify: `apps/desktop/src/features/shell-ball/shellBall.contract.test.ts` | ||
|
|
||
| - [ ] **Step 1: Write the failing style contract test** | ||
|
|
||
| Add tests asserting that the bubble styles provide: | ||
| - transparent container with no panel/card chrome | ||
| - hidden scrollbars | ||
| - top/bottom fade mask or equivalent edge fade treatment | ||
| - new-message motion classes/data attributes for slight upward-float + fade-in | ||
|
|
||
| - [ ] **Step 2: Run test to verify it fails** | ||
|
|
||
| Run: `pnpm --dir apps/desktop test:shell-ball` | ||
| Expected: FAIL because the current bubble CSS still targets placeholder shells. | ||
|
|
||
| - [ ] **Step 3: Write minimal implementation** | ||
|
|
||
| Update `apps/desktop/src/features/shell-ball/shellBall.css` so the bubble window: | ||
| - stays transparent | ||
| - has no outer box/border/panel | ||
| - hides scrollbars | ||
| - applies a top/bottom fade mask | ||
| - animates new messages with a light upward-float + fade-in | ||
|
|
||
| Keep the motion subtle and preserve window sizing behavior. | ||
|
|
||
| - [ ] **Step 4: Run test to verify it passes** | ||
|
|
||
| Run: `pnpm --dir apps/desktop test:shell-ball` | ||
| Expected: PASS for the bubble-window style contract. | ||
|
|
||
| - [ ] **Step 5: Commit** | ||
|
|
||
| ```bash | ||
| git add apps/desktop/src/features/shell-ball/shellBall.css apps/desktop/src/features/shell-ball/components/ShellBallBubbleZone.tsx apps/desktop/src/features/shell-ball/shellBall.contract.test.ts | ||
| git commit -m "feat(desktop-shell-ball): style transparent bubble history window" | ||
| ``` | ||
|
|
||
| ### Task 5: Verify Bubble Window Behavior End-to-End | ||
|
|
||
| **Files:** | ||
| - Verify only | ||
|
|
||
| - [ ] **Step 1: Run shell-ball tests** | ||
|
|
||
| Run: `pnpm --dir apps/desktop test:shell-ball` | ||
| Expected: PASS. | ||
|
|
||
| - [ ] **Step 2: Run typecheck** | ||
|
|
||
| Run: `pnpm --dir apps/desktop typecheck` | ||
| Expected: PASS. | ||
|
|
||
| - [ ] **Step 3: Run build** | ||
|
|
||
| Run: `pnpm --dir apps/desktop build` | ||
| Expected: PASS. | ||
|
|
||
| - [ ] **Step 4: Run the desktop app manually** | ||
|
|
||
| Run: `pnpm --dir apps/desktop exec tauri dev` | ||
|
|
||
| Expected: | ||
| - bubble window shows only message bubbles | ||
| - bubble background remains fully transparent | ||
| - user bubbles are right-aligned | ||
| - agent bubbles are left-aligned | ||
| - older messages scroll upward | ||
| - scrollbar stays hidden | ||
| - top/bottom edges fade overflowing history | ||
| - new incoming message uses subtle upward-float + fade-in | ||
|
|
||
| - [ ] **Step 5: Commit only if manual verification required a follow-up fix** | ||
|
|
||
| Run: `git status --short` | ||
| Expected: clean working tree; if not, commit with: | ||
|
|
||
| ```bash | ||
| git add <fixed-files> | ||
| git commit -m "feat(desktop-shell-ball): polish bubble window motion and history" | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>CialloClaw Shell Ball Bubble Pinned</title> | ||
| </head> | ||
| <body> | ||
| <div id="root"></div> | ||
| <script type="module" src="/src/app/shell-ball-bubble-pinned/main.tsx"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| {"default":{"identifier":"default","description":"Default desktop app permissions plus shell-ball floating window controls.","local":true,"windows":["shell-ball","shell-ball-bubble","shell-ball-input","dashboard","control-panel"],"permissions":["core:default","core:window:allow-create","core:window:allow-show","core:window:allow-hide","core:window:allow-set-position","core:window:allow-set-size","core:window:allow-set-size-constraints","core:window:allow-set-focus","core:window:allow-set-focusable","core:window:allow-set-ignore-cursor-events","core:window:allow-set-always-on-top","core:window:allow-unminimize","core:window:allow-start-dragging"]}} | ||
| {"default":{"identifier":"default","description":"Default desktop app permissions plus shell-ball floating window controls.","local":true,"windows":["shell-ball","shell-ball-bubble","shell-ball-input","shell-ball-bubble-pinned-*","dashboard","control-panel"],"permissions":["core:default","core:window:allow-create","core:window:allow-show","core:window:allow-hide","core:window:allow-set-position","core:window:allow-set-size","core:window:allow-set-size-constraints","core:window:allow-set-focus","core:window:allow-set-focusable","core:window:allow-set-ignore-cursor-events","core:window:allow-set-always-on-top","core:window:allow-unminimize","core:window:allow-start-dragging"]}} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import ReactDOM from "react-dom/client"; | ||
| import { AppProviders } from "@/features/shared/AppProviders"; | ||
| import { ShellBallPinnedBubbleWindow } from "@/features/shell-ball/ShellBallPinnedBubbleWindow"; | ||
| import "@/features/shell-ball/shellBall.css"; | ||
|
|
||
| const rootElement = document.getElementById("root")!; | ||
|
|
||
| document.documentElement.dataset.appWindow = "shell-ball-bubble-pinned"; | ||
| document.body.dataset.appWindow = "shell-ball-bubble-pinned"; | ||
| rootElement.dataset.appWindow = "shell-ball-bubble-pinned"; | ||
| document.documentElement.setAttribute("data-app-window", "shell-ball-bubble-pinned"); | ||
| document.body.setAttribute("data-app-window", "shell-ball-bubble-pinned"); | ||
| rootElement.setAttribute("data-app-window", "shell-ball-bubble-pinned"); | ||
|
|
||
| ReactDOM.createRoot(rootElement).render( | ||
| <AppProviders> | ||
| <ShellBallPinnedBubbleWindow /> | ||
| </AppProviders>, | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import { useMemo, useState } from "react"; | ||
| import { | ||
| getShellBallCurrentWindow, | ||
| getShellBallPinnedBubbleIdFromLabel, | ||
| startShellBallWindowDragging, | ||
| } from "../../platform/shellBallWindowController"; | ||
| import { emitShellBallBubbleAction, emitShellBallPinnedWindowDetached, useShellBallHelperWindowSnapshot } from "./useShellBallCoordinator"; | ||
|
|
||
| export function ShellBallPinnedBubbleWindow() { | ||
| const windowLabel = getShellBallCurrentWindow().label; | ||
| const bubbleId = getShellBallPinnedBubbleIdFromLabel(windowLabel); | ||
| const snapshot = useShellBallHelperWindowSnapshot({ role: "pinned" }); | ||
| const [followsShellBallGeometry, setFollowsShellBallGeometry] = useState(true); | ||
| const pinnedItem = useMemo( | ||
| () => snapshot.bubbleItems.find((item) => item.bubble.bubble_id === bubbleId && item.bubble.pinned), | ||
| [bubbleId, snapshot.bubbleItems], | ||
| ); | ||
|
|
||
| if (bubbleId === null || pinnedItem === undefined) { | ||
| return <div className="shell-ball-window shell-ball-window--bubble" aria-label="Shell-ball pinned bubble window" />; | ||
| } | ||
|
|
||
| const pinnedBubbleId = bubbleId; | ||
|
|
||
| function handleDetachDrag() { | ||
| if (followsShellBallGeometry) { | ||
| setFollowsShellBallGeometry(false); | ||
| void emitShellBallPinnedWindowDetached(pinnedBubbleId); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The window is marked detached on |
||
| } | ||
|
|
||
| void startShellBallWindowDragging(); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="shell-ball-window shell-ball-window--bubble" aria-label="Shell-ball pinned bubble window"> | ||
| <div className="shell-ball-bubble-message shell-ball-bubble-message--pinned" data-bubble-id={pinnedBubbleId}> | ||
| <button | ||
| type="button" | ||
| className="shell-ball-bubble-message__control shell-ball-bubble-message__pin-control" | ||
| data-bubble-action="unpin" | ||
| data-bubble-id={pinnedBubbleId} | ||
| aria-label="Unpin bubble" | ||
| onClick={() => { | ||
| void emitShellBallBubbleAction("unpin", pinnedBubbleId, "pinned_window"); | ||
| }} | ||
| > | ||
| Unpin | ||
| </button> | ||
| <button | ||
| type="button" | ||
| className="shell-ball-bubble-message__control shell-ball-bubble-message__delete-control" | ||
| data-bubble-action="delete" | ||
| data-bubble-id={pinnedBubbleId} | ||
| aria-label="Delete bubble" | ||
| onClick={() => { | ||
| void emitShellBallBubbleAction("delete", pinnedBubbleId, "pinned_window"); | ||
| }} | ||
| > | ||
| Delete | ||
| </button> | ||
| <button | ||
| type="button" | ||
| className="shell-ball-bubble-message__drag-handle" | ||
| aria-label="Drag pinned bubble" | ||
| onPointerDown={handleDetachDrag} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This marks the bubble as detached on |
||
| > | ||
| Drag | ||
| </button> | ||
| <p className="shell-ball-bubble-message__text">{pinnedItem.bubble.text}</p> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding
core:window:allow-createto the shareddefaultcapability broadens window-creation rights for every renderer covered by this profile, not just the shell-ball surfaces. This should be scoped to a narrower shell-ball-specific capability/window set so the new pinned-window feature does not expand the app-wide trust boundary.