Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5d18fb7
feat(desktop-shell-ball): add bubble message feed contract
gdemonc Apr 11, 2026
2da1079
fix(desktop-shell-ball): align bubble message roles
gdemonc Apr 11, 2026
cb45e06
fix(desktop-shell-ball): copy bubble snapshot messages
gdemonc Apr 11, 2026
b1e6855
feat(desktop-shell-ball): sync bubble messages to helper window
gdemonc Apr 11, 2026
927971b
feat(desktop-shell-ball): render bubble message list
gdemonc Apr 11, 2026
8664b4b
feat(desktop-shell-ball): style transparent bubble history window
gdemonc Apr 11, 2026
b5def2a
fix(desktop-shell-ball): tighten bubble style contract
gdemonc Apr 11, 2026
3cea33c
fix(desktop-shell-ball): unify helper width override
gdemonc Apr 11, 2026
43b4d79
feat(desktop-shell-ball): add bubble window plan
gdemonc Apr 11, 2026
9d19665
fix(desktop-shell-ball): anchor bubble feed to latest
gdemonc Apr 11, 2026
31a8382
feat(desktop-shell-ball): align bubble items to protocol payloads
gdemonc Apr 11, 2026
a95ad44
fix(desktop-shell-ball): complete bubble item migration
gdemonc Apr 11, 2026
6187a56
feat(desktop-shell-ball): define bubble window existence strategy
gdemonc Apr 11, 2026
ccdb926
feat(desktop-shell-ball): add bubble pin and delete controls
gdemonc Apr 11, 2026
71ff464
feat(desktop-shell-ball): define pinned bubble state ownership
gdemonc Apr 11, 2026
ccbf69d
feat(desktop-shell-ball): detach pinned bubbles into windows
gdemonc Apr 11, 2026
ec626c9
feat(desktop-shell-ball): restore pinned bubbles to region order
gdemonc Apr 11, 2026
d7ce857
feat(desktop-shell-ball): tune bubble region anchor
gdemonc Apr 11, 2026
eb40cb6
feat(desktop-shell-ball): add pinned bubble plan
gdemonc Apr 11, 2026
0b1c474
Merge branch '1024XEngineer:main' into feat/shell-ball-bubble
gdemonc Apr 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

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"
```
12 changes: 12 additions & 0 deletions apps/desktop/shell-ball-bubble-pinned.html
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>
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default desktop app permissions plus shell-ball floating window controls.",
"windows": ["shell-ball", "shell-ball-bubble", "shell-ball-input", "dashboard", "control-panel"],
"windows": ["shell-ball", "shell-ball-bubble", "shell-ball-input", "shell-ball-bubble-pinned-*", "dashboard", "control-panel"],
"permissions": [
"core:default",
"core:window:allow-create",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding core:window:allow-create to the shared default capability 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.

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/gen/schemas/capabilities.json
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"]}}
19 changes: 19 additions & 0 deletions apps/desktop/src/app/shell-ball-bubble-pinned/main.tsx
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>,
);
18 changes: 15 additions & 3 deletions apps/desktop/src/features/shell-ball/ShellBallBubbleWindow.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ShellBallVisualState } from "./shellBall.types";
import { useShellBallHelperWindowSnapshot } from "./useShellBallCoordinator";
import { getShellBallVisibleBubbleItems } from "./shellBall.windowSync";
import { emitShellBallBubbleAction, useShellBallHelperWindowSnapshot } from "./useShellBallCoordinator";
import { useShellBallWindowMetrics } from "./useShellBallWindowMetrics";
import { ShellBallBubbleZone } from "./components/ShellBallBubbleZone";

Expand All @@ -10,14 +11,25 @@ type ShellBallBubbleWindowProps = {
export function ShellBallBubbleWindow({ visualState }: ShellBallBubbleWindowProps) {
const snapshot = useShellBallHelperWindowSnapshot({ role: "bubble" });
const resolvedVisualState = visualState ?? snapshot.visualState;
const visibleBubbleItems = getShellBallVisibleBubbleItems(snapshot.bubbleItems);
const { rootRef } = useShellBallWindowMetrics({
role: "bubble",
visible: snapshot.visibility.bubble,
visible: true,
clickThrough: snapshot.bubbleRegion.clickThrough,
});

return (
<div ref={rootRef} className="shell-ball-window shell-ball-window--bubble" aria-label="Shell-ball bubble window">
<ShellBallBubbleZone visualState={resolvedVisualState} />
<ShellBallBubbleZone
visualState={resolvedVisualState}
bubbleItems={visibleBubbleItems}
onDeleteBubble={(bubbleId) => {
void emitShellBallBubbleAction("delete", bubbleId);
}}
onPinBubble={(bubbleId) => {
void emitShellBallBubbleAction("pin", bubbleId);
}}
/>
</div>
);
}
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The window is marked detached on pointerdown, before any actual drag occurs. A simple click on the drag handle will emit pinnedWindowDetached, after which shell-ball moves stop re-anchoring this bubble even though the user never dragged it. That does not match the documented behavior of detaching only once the user has dragged the pinned window.

}

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}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This marks the bubble as detached on pointerdown, not after an actual drag completes. A simple click on the drag handle permanently stops future re-anchoring, which does not match the documented detach after drag behavior.

>
Drag
</button>
<p className="shell-ball-bubble-message__text">{pinnedItem.bubble.text}</p>
</div>
</div>
);
}
Loading
Loading