Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,9 @@ src/apps/mobile/android/.kotlin/
/docs/superpowers/

AGENTS.override.md

# CodeGraph index (local, large)
.codegraph/

# Desktop test artifacts
target-desktop-test/
7 changes: 7 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ packages:
- "website"
- "tests/e2e"

allowBuilds:
'@parcel/watcher': false
edgedriver: false
esbuild: true
geckodriver: false
sharp: true

patchedDependencies:
"@xterm/xterm@6.1.0-beta.141": patches/@xterm__xterm@6.1.0-beta.141.patch
mdast-util-gfm-autolink-literal@2.0.1: patches/mdast-util-gfm-autolink-literal@2.0.1.patch
20 changes: 18 additions & 2 deletions scripts/frontend-color-surface-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@
"src/flow_chat/assets/subagent-avatars/robot-18.svg",
"src/flow_chat/assets/subagent-avatars/robot-19.svg",
"src/flow_chat/assets/subagent-avatars/robot-20.svg",
"src/tools/generative-ui/components/openui",
"src/tools/generative-ui/components/openui",
"src/tools/generative-ui/components/openuiLibrary.tsx",
"src/tools/generative-ui/components/vendor"
"src/tools/generative-ui/components/vendor",
"public/agent-companion-pets/grok-live/preview.svg",
"src/flow_chat/components/grok-live/grok-player.js"
]
}
},
Expand Down Expand Up @@ -525,6 +527,20 @@
}
],
"exclusions": [
{
"id": "grok-live-thumbnail",
"path": "src/web-ui/public/agent-companion-pets/grok-live/preview.svg",
"kind": "authored-vector-artwork",
"owner": "GrokLive preview in AgentCompanionPetService",
"reason": "The transparent pet picker artwork requires an invariant black body and two opaque white eyes; it is an illustration, not theme-dependent UI chrome."
},
{
"id": "grok-live-vendor-mask",
"path": "src/web-ui/src/flow_chat/components/grok-live/grok-player.js",
"kind": "vendored-renderer",
"owner": "src/web-ui/src/flow_chat/components/grok-live/SOURCE.txt",
"reason": "The local animation renderer needs fixed black and white mask-pixel semantics; this narrowly scoped vendor file is not product UI chrome."
},
{
"id": "subagent-hatch-artwork",
"path": "design-system/packages/ui/src/brand/assets/subagent-egg.svg",
Expand Down
2 changes: 1 addition & 1 deletion src/apps/desktop/src/appearance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::startup_trace::DesktopStartupTrace;

const AGENT_COMPANION_WINDOW_LABEL: &str = "agent-companion-pet";
const AGENT_COMPANION_WINDOW_MIN_SIZE: f64 = 96.0;
const AGENT_COMPANION_WINDOW_MAX_WIDTH: f64 = 360.0;
const AGENT_COMPANION_WINDOW_MAX_WIDTH: f64 = 480.0;
const AGENT_COMPANION_WINDOW_MAX_HEIGHT: f64 = 240.0;
const AGENT_COMPANION_WINDOW_MARGIN: i32 = 64;
const AGENT_COMPANION_WINDOW_EDGE_MARGIN: f64 = 8.0;
Expand Down
5 changes: 5 additions & 0 deletions src/web-ui/public/agent-companion-pets/grok-live/preview.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vite
import type { AgentCompanionActivityPayload, AgentCompanionTaskStatus } from '@/flow_chat/utils/agentCompanionActivity';
import { AgentCompanionDesktopPet } from './AgentCompanionDesktopPet';

const companionPet = vi.hoisted(() => ({ selection: null as any }));
const emitMock = vi.hoisted(() => vi.fn(() => Promise.resolve()));
const listenMock = vi.hoisted(() => vi.fn());
const invokeMock = vi.hoisted(() => vi.fn(() => Promise.resolve()));
Expand Down Expand Up @@ -43,17 +44,17 @@ vi.mock('@tauri-apps/api/window', () => ({

vi.mock('@/infrastructure/config/services/AIExperienceConfigService', () => ({
aiExperienceConfigService: {
getSettings: () => ({ agent_companion_pet: null }),
getSettings: () => ({ agent_companion_pet: companionPet.selection }),
getSettingsAsync: () => Promise.resolve({
enable_agent_companion: true,
agent_companion_pet: null,
agent_companion_pet: companionPet.selection,
}),
},
}));

vi.mock('@/flow_chat/components/AgentCompanionPet', () => ({
AgentCompanionPet: ({ lookDirection, action, mood, dragDirection }: { lookDirection?: number | null; action?: string | null; mood?: string; dragDirection?: string }) => (
<div data-testid="pixel-pet" data-look-direction={lookDirection ?? 'none'} data-action={action ?? 'none'} data-mood={mood} data-direction={dragDirection} />
AgentCompanionPet: ({ lookDirection, action, mood, dragDirection, livingLineState }: { lookDirection?: number | null; action?: string | null; mood?: string; dragDirection?: string; livingLineState?: string }) => (
<div data-testid="pixel-pet" data-look-direction={lookDirection ?? 'none'} data-action={action ?? 'none'} data-mood={mood} data-direction={dragDirection} data-living-line-state={livingLineState} />
),
}));

Expand Down Expand Up @@ -147,6 +148,7 @@ describe('AgentCompanionDesktopPet', () => {

beforeEach(async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
companionPet.selection = null;
emitMock.mockClear();
invokeMock.mockClear();
cursorPositionMock.mockReset();
Expand Down Expand Up @@ -200,6 +202,63 @@ describe('AgentCompanionDesktopPet', () => {
vi.unstubAllGlobals();
});

it('reserves the LivingLine rectangle and forwards its persistent state', async () => {
await act(async () => root.unmount());
companionPet.selection = {
id: 'living-line', displayName: 'LivingLine', source: 'preset',
packagePath: '/agent-companion-pets/living-line',
spritesheetPath: '/agent-companion-pets/living-line/preview.svg', spritesheetMimeType: 'image/svg+xml',
};
invokeMock.mockClear();
await act(async () => {
root = createRoot(container);
root.render(<I18nextProvider i18n={i18n}><AgentCompanionDesktopPet /></I18nextProvider>);
});
// Reserve the bubble slot before there is a task, not after it paints.
expect(invokeMock).toHaveBeenLastCalledWith('resize_agent_companion_desktop_pet', { width: 408, height: 102 });
invokeMock.mockClear();
pushActivity({ mood: 'rest', tasks: [task({ state: 'error', mood: 'rest' })], livingLineState: 'error', emittedAt: 1, sequence: 1 });

expect(query('.openbitfun-agent-companion-window__stack')?.getAttribute('style')).toContain('--openbitfun-agent-companion-pet-width: 216px');
expect(query('.openbitfun-agent-companion-window__stack')?.getAttribute('style')).toContain('--openbitfun-agent-companion-pet-height: 96px');
expect(query('[data-testid="pixel-pet"]')?.getAttribute('data-living-line-state')).toBe('error');
pushActivity({ mood: 'working', tasks: [task(), task({ sessionId: 'second' })], emittedAt: 2, sequence: 2 });
pushActivity({ mood: 'rest', tasks: [], emittedAt: 3, sequence: 3 });
await act(async () => { await Promise.resolve(); });
// No horizontal shrink/grow/reposition cycle for zero, one or two bubbles.
expect(invokeMock).not.toHaveBeenCalledWith('resize_agent_companion_desktop_pet', expect.anything());
});

it('waits for the LivingLine viewport before revealing bubbles and preserves their nodes', async () => {
vi.stubGlobal('innerWidth', 216);
act(() => window.dispatchEvent(new Event('resize')));
const settingsListener = listenMock.mock.calls.find(([name]) => name === 'agent-companion://settings-updated')?.[1];
await act(async () => settingsListener({ payload: {
enable_agent_companion: true,
agent_companion_pet: {
id: 'living-line', displayName: 'LivingLine', source: 'preset',
packagePath: '/agent-companion-pets/living-line',
spritesheetPath: '/agent-companion-pets/living-line/preview.svg', spritesheetMimeType: 'image/svg+xml',
},
} }));
pushActivity({ mood: 'working', tasks: [task()], emittedAt: 1, sequence: 1 });
const bubbles = query<HTMLElement>('.openbitfun-agent-companion-window__bubbles')!;
const bubble = query('.openbitfun-agent-companion-window__bubble');
expect(bubbles.style.visibility).toBe('hidden');
expect(bubbles.getAttribute('aria-hidden')).toBe('true');
expect(invokeMock).toHaveBeenLastCalledWith('resize_agent_companion_desktop_pet', { width: 408, height: 102 });

vi.stubGlobal('innerWidth', 408);
act(() => window.dispatchEvent(new Event('resize')));
expect(bubbles.style.visibility).toBe('');
expect(bubbles.hasAttribute('aria-hidden')).toBe(false);
expect(query('.openbitfun-agent-companion-window__bubble')).toBe(bubble);
invokeMock.mockClear();
pushActivity({ mood: 'waiting', tasks: [task({ state: 'attention', latestOutput: 'Reply needed' })], emittedAt: 2, sequence: 2 });
await act(async () => { await Promise.resolve(); });
expect(invokeMock).not.toHaveBeenCalledWith('resize_agent_companion_desktop_pet', expect.anything());
});

it.each(['before activity', 'after activity'])(
'shows a new task without dragging when the main window gains focus %s',
focusOrder => {
Expand Down
Loading
Loading