Skip to content

Commit b31ccbd

Browse files
committed
feat(web-ui): add desktop pet bubble composer
1 parent fe19461 commit b31ccbd

10 files changed

Lines changed: 1450 additions & 89 deletions

File tree

src/web-ui/src/app/App.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
isStartupOverlayPresent,
2727
} from './startup/startupOverlay';
2828
import { ToolbarModeProvider } from '../flow_chat/components/toolbar-mode/ToolbarModeProvider';
29+
import type { AgentCompanionPetCommand } from './services/agentCompanionPetCommands';
2930
import AskUserAnnouncer from './components/NavPanel/AskUserAnnouncer';
3031

3132
const log = createLogger('App');
@@ -681,6 +682,35 @@ function App() {
681682
};
682683
}, []);
683684

685+
useEffect(() => {
686+
let unlisten: (() => void) | null = null;
687+
void import('@tauri-apps/api/event')
688+
.then(({ listen }) => listen<AgentCompanionPetCommand>(
689+
'agent-companion://pet-command',
690+
async event => {
691+
try {
692+
const { handleAgentCompanionPetCommand } = await import('./services/agentCompanionPetCommands');
693+
await handleAgentCompanionPetCommand(event.payload);
694+
} catch (error) {
695+
log.warn('Failed to handle Agent companion pet command', {
696+
commandType: event.payload?.type,
697+
error,
698+
});
699+
}
700+
},
701+
))
702+
.then(removeListener => {
703+
unlisten = removeListener;
704+
})
705+
.catch(error => {
706+
log.warn('Failed to listen for Agent companion pet commands', error);
707+
});
708+
709+
return () => {
710+
unlisten?.();
711+
};
712+
}, []);
713+
684714
// Block browser-native Ctrl+F (find bar) and Ctrl+R (hard reload).
685715
// On macOS the equivalent modifiers are Cmd+F / Cmd+R.
686716
useEffect(() => {

src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.scss

Lines changed: 248 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,33 @@
3232
background: transparent;
3333
user-select: none;
3434

35-
&__dock {
35+
/* Holds the dock anchored bottom-right, above the click-away backdrop. */
36+
&__stack {
3637
position: absolute;
3738
right: 0;
3839
bottom: 0;
40+
z-index: 2;
41+
display: flex;
42+
flex-direction: column;
43+
align-items: flex-end;
44+
justify-content: flex-end;
45+
max-width: 100vw;
46+
max-height: 100vh;
47+
min-height: 0;
48+
pointer-events: none;
49+
}
50+
51+
&__backdrop {
52+
position: absolute;
53+
inset: 0;
54+
z-index: 1;
55+
background: transparent;
56+
}
57+
58+
&__dock {
59+
position: relative;
60+
flex: 0 1 auto;
61+
min-height: 0;
3962
display: flex;
4063
flex-direction: row;
4164
align-items: center;
@@ -46,6 +69,143 @@
4669
pointer-events: none;
4770
}
4871

72+
/* While a menu is open every click outside it must reach the backdrop. The
73+
bubble composer is excluded: it lives inside a bubble and stays usable. */
74+
&--menu-open &__pet-hitbox,
75+
&--menu-open &__bubbles {
76+
pointer-events: none;
77+
}
78+
79+
&--menu-open &__bubble-compose {
80+
pointer-events: none;
81+
}
82+
83+
&__overlay {
84+
box-sizing: border-box;
85+
width: max-content;
86+
max-width: min(240px, 100vw);
87+
padding: 5px;
88+
display: flex;
89+
flex-direction: column;
90+
gap: 3px;
91+
border: 1px solid var(--color-overlay-slate-12);
92+
border-radius: 10px;
93+
/* Intentionally no backdrop-filter: adding a blurred layer on right-click
94+
re-composites the other blurred surfaces and flashes the whole window. */
95+
background: var(--color-overlay-white-95);
96+
color: var(--bitfun-agent-companion-bubble-fg);
97+
box-shadow: 0 6px 18px rgba(var(--color-overlay-slate-rgb), 0.18);
98+
pointer-events: auto;
99+
100+
/* Context menus are placed at the cursor. */
101+
&--anchored {
102+
position: absolute;
103+
z-index: 3;
104+
}
105+
}
106+
107+
&__menu {
108+
display: flex;
109+
flex-direction: column;
110+
gap: 2px;
111+
}
112+
113+
&__menu-item {
114+
appearance: none;
115+
width: 100%;
116+
box-sizing: border-box;
117+
padding: 5px 8px;
118+
border: 0;
119+
border-radius: 6px;
120+
background: transparent;
121+
color: inherit;
122+
font: inherit;
123+
font-size: 11px;
124+
line-height: 1.3;
125+
text-align: left;
126+
/* Wrap rather than spill: the window can be as narrow as the pet sprite. */
127+
white-space: normal;
128+
overflow-wrap: anywhere;
129+
cursor: pointer;
130+
131+
&:hover {
132+
background: var(--color-overlay-slate-08);
133+
}
134+
135+
&:focus-visible {
136+
outline: 2px solid color-mix(in srgb, var(--color-accent-600) 70%, transparent);
137+
outline-offset: -2px;
138+
}
139+
}
140+
141+
/* Input bar shown inside the bubble the user replied from. */
142+
&__bubble-composer {
143+
flex: 0 0 auto;
144+
align-self: stretch;
145+
width: 100%;
146+
margin-top: 5px;
147+
display: flex;
148+
flex-direction: row;
149+
align-items: center;
150+
gap: 4px;
151+
}
152+
153+
&__bubble-composer-input {
154+
appearance: none;
155+
-webkit-appearance: none;
156+
flex: 1 1 auto;
157+
min-width: 0;
158+
height: 21px;
159+
box-sizing: border-box;
160+
padding: 2px 5px;
161+
border: 1px solid var(--color-overlay-slate-12);
162+
border-radius: 5px;
163+
background: var(--color-overlay-white-95);
164+
color: inherit;
165+
font: inherit;
166+
font-size: 10px;
167+
line-height: 1.3;
168+
user-select: text;
169+
-webkit-user-select: text;
170+
171+
&:focus,
172+
&:focus-visible {
173+
outline: none;
174+
outline-offset: 0;
175+
box-shadow: none;
176+
}
177+
}
178+
179+
&__bubble-composer-send,
180+
&__bubble-composer-cancel {
181+
appearance: none;
182+
flex-shrink: 0;
183+
width: 21px;
184+
height: 21px;
185+
padding: 0;
186+
display: grid;
187+
place-items: center;
188+
border: 1px solid var(--color-overlay-slate-12);
189+
border-radius: 5px;
190+
background: var(--color-overlay-slate-04);
191+
color: inherit;
192+
cursor: pointer;
193+
194+
&:hover:not(:disabled) {
195+
background: var(--color-overlay-slate-08);
196+
}
197+
198+
&:disabled {
199+
opacity: 0.45;
200+
cursor: default;
201+
}
202+
203+
&:focus-visible {
204+
outline: 2px solid color-mix(in srgb, var(--color-accent-600) 70%, transparent);
205+
outline-offset: 1px;
206+
}
207+
}
208+
49209
&__pet {
50210
width: min(var(--bitfun-agent-companion-pet-width), 100vw);
51211
height: min(var(--bitfun-agent-companion-pet-height), 100vh);
@@ -93,8 +253,9 @@
93253
&__bubbles {
94254
position: relative;
95255
flex-shrink: 0;
96-
width: 146px;
97-
max-width: 146px;
256+
width: 180px;
257+
max-width: 180px;
258+
min-height: 0;
98259
max-height: 100vh;
99260
display: flex;
100261
flex-direction: column;
@@ -121,11 +282,66 @@
121282
}
122283
}
123284

285+
/* Wraps one bubble so the hover compose button can sit on top of it without
286+
nesting a button inside the bubble button. */
287+
&__bubble-shell {
288+
position: relative;
289+
isolation: isolate;
290+
flex-shrink: 0;
291+
width: 180px;
292+
max-width: 100%;
293+
294+
&--single {
295+
height: min(var(--bitfun-agent-companion-pet-height), 100vh);
296+
}
297+
}
298+
299+
&__bubble-compose {
300+
appearance: none;
301+
position: absolute;
302+
z-index: 2;
303+
right: 5px;
304+
bottom: 5px;
305+
width: 18px;
306+
height: 18px;
307+
padding: 0;
308+
display: grid;
309+
place-items: center;
310+
border: 1px solid var(--color-overlay-slate-12);
311+
border-radius: 50%;
312+
background: var(--color-overlay-white-95);
313+
color: var(--bitfun-agent-companion-bubble-fg);
314+
opacity: 0;
315+
visibility: hidden;
316+
pointer-events: none;
317+
transition:
318+
opacity 120ms ease,
319+
visibility 0s linear 120ms;
320+
cursor: pointer;
321+
322+
&:hover {
323+
background: var(--color-overlay-slate-08);
324+
}
325+
326+
&:focus-visible {
327+
outline: 2px solid color-mix(in srgb, var(--color-accent-600) 70%, transparent);
328+
outline-offset: 1px;
329+
}
330+
}
331+
332+
&--native-hover &__bubble-shell:hover &__bubble-compose,
333+
&__bubble-shell--hovered &__bubble-compose {
334+
opacity: 1;
335+
visibility: visible;
336+
pointer-events: auto;
337+
transition-delay: 0s;
338+
}
339+
124340
&__bubble {
125341
appearance: none;
126342
text-align: left;
127343
font: inherit;
128-
width: 146px;
344+
width: 180px;
129345
max-width: 100%;
130346
box-sizing: border-box;
131347
padding: 7px 10px;
@@ -136,6 +352,7 @@
136352
backdrop-filter: blur(10px);
137353
cursor: pointer;
138354
position: relative;
355+
z-index: 0;
139356

140357
&:hover {
141358
transform: translateY(-1px);
@@ -152,6 +369,33 @@
152369
flex-direction: column;
153370
overflow: hidden;
154371
}
372+
373+
/* Replying turns the bubble into its own composer, so it must not react to
374+
hover like a clickable bubble any more. */
375+
&--composing {
376+
display: flex;
377+
flex-direction: column;
378+
overflow: hidden;
379+
cursor: default;
380+
381+
&:hover {
382+
transform: none;
383+
}
384+
385+
/* Only the output yields space to the input row; letting the title and
386+
status shrink too would clip their glyphs. */
387+
.bitfun-agent-companion-window__bubble-title,
388+
.bitfun-agent-companion-window__bubble-status {
389+
flex: 0 0 auto;
390+
}
391+
392+
/* Two whole lines: a flexible height would leave a half-clipped line
393+
under the input row. */
394+
.bitfun-agent-companion-window__bubble-output {
395+
flex: 0 0 calc(12px * 2);
396+
height: calc(12px * 2);
397+
}
398+
}
155399
}
156400

157401
&__bubble-title,

0 commit comments

Comments
 (0)