Skip to content
Merged
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
13 changes: 13 additions & 0 deletions .changeset/adr-0057-p3b-fab-launcher.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
---
"@object-ui/app-shell": patch
---

feat(console-ai): the FAB becomes the ChatDock launcher when the dock is on (ADR-0057 P3b)

When `features.chatDock` is enabled, the console FAB opens the docked rail instead
of the floating overlay — one entry point, the ADR's "FAB → launcher" step. In
dock mode the FAB stays the lightweight button (it never mounts the heavy floating
chatbot; the rail loads the chat on demand), and a designer "Ask AI" open signal
(assistantBus) opens the dock too. With the flag OFF the FAB is unchanged (floating
overlay). Supersedes P3a's edge launcher (the dock is gated on the same
`showChatbot`, so the FAB is always present to launch it).
29 changes: 21 additions & 8 deletions packages/app-shell/src/layout/ConsoleChatbotFab.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,25 +26,38 @@ const prefetchChatbot = () => {

import type { ConsoleFloatingChatbotProps } from './ConsoleFloatingChatbot';

export type ConsoleChatbotFabProps = ConsoleFloatingChatbotProps;
export type ConsoleChatbotFabProps = ConsoleFloatingChatbotProps & {
/**
* ADR-0057 P3b — when the ChatDock is enabled, the FAB becomes the dock's
* LAUNCHER: a click (and a designer "Ask AI" open signal) opens the docked
* rail instead of arming the floating overlay, and the heavy floating chatbot
* is never mounted (the dock owns the chat). Absent → unchanged floating-FAB
* behavior.
*/
onOpenDock?: () => void;
};

export function ConsoleChatbotFab(props: ConsoleChatbotFabProps) {
export function ConsoleChatbotFab({ onOpenDock, ...props }: ConsoleChatbotFabProps) {
const [armed, setArmed] = useState(false);
const { t } = useObjectTranslation();

// A designer surface can ask the assistant to open (e.g. an "Ask AI"
// button) via the assistant bus — arming the lazy chatbot just like a
// click does. Once armed, the chatbot's own trigger owns open/close.
// When the dock is the target (P3b), the open signal opens the dock instead.
const { openSeq } = useAssistant();
const seenOpenSeq = useRef(openSeq);
useEffect(() => {
if (openSeq !== seenOpenSeq.current) {
seenOpenSeq.current = openSeq;
setArmed(true);
if (onOpenDock) onOpenDock();
else setArmed(true);
}
}, [openSeq]);
}, [openSeq, onOpenDock]);

if (armed) {
// Dock mode (P3b): stay the lightweight launcher — never mount the floating
// overlay; the click opens the rail, which loads the chat on demand.
if (armed && !onOpenDock) {
return (
<Suspense fallback={null}>
<ConsoleFloatingChatbot {...props} defaultOpen />
Expand All@@ -56,9 +69,9 @@ export function ConsoleChatbotFab(props: ConsoleChatbotFabProps) {
<button
type="button"
aria-label={t('topbar.openAssistant', { defaultValue: 'Open {{name}} assistant', name: props.appLabel })}
onClick={() => setArmed(true)}
onMouseEnter={prefetchChatbot}
onFocus={prefetchChatbot}
onClick={() => (onOpenDock ? onOpenDock() : setArmed(true))}
onMouseEnter={onOpenDock ? undefined : prefetchChatbot}
onFocus={onOpenDock ? undefined : prefetchChatbot}
className="fixed bottom-20 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg ring-1 ring-primary/20 transition-transform hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 sm:bottom-6"
data-testid="console-chatbot-fab"
>
Expand Down
12 changes: 6 additions & 6 deletions packages/app-shell/src/layout/ConsoleLayout.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ import { AppShell } from '@object-ui/layout';
// shiki, streamdown, mermaid, @ai-sdk, ~20MB) only downloads on first
// hover/click. See ConsoleChatbotFab.tsx.
import { ConsoleChatbotFab } from './ConsoleChatbotFab';
import { useChatDockState, ChatDockPanel, ChatDockLauncher } from './ChatDock';
import { useChatDockState, ChatDockPanel } from './ChatDock';
import { matchChatDockShortcut } from './chatDockState';
import { DraftPreviewBar } from '../preview/DraftPreviewBar';
import { UnpublishedAppBar } from '../preview/UnpublishedAppBar';
Expand DownExpand Up@@ -165,13 +165,13 @@ export function ConsoleLayout({
defaultAgent={activeApp?.defaultAgent}
objects={objects}
userId={userId}
// ADR-0057 P3b — when the dock is enabled, the FAB becomes its
// launcher (opens the rail) instead of the floating overlay. This
// supersedes P3a's edge launcher: the dock is gated on `showChatbot`,
// so the FAB is always present to launch it.
onOpenDock={dockEnabled ? dock.expand : undefined}
/>
)}

{/* ADR-0057 P3a — collapsed dock affordance (edge launcher). Shown only
when the dock is enabled and collapsed; expanding renders the rail via
AppShell `rightRail` above. */}
{dockEnabled && !dock.expanded && <ChatDockLauncher onExpand={dock.expand} />}
</AppShell>
</MobileViewSwitcherProvider>
</CommandPaletteProvider>
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
import '@testing-library/jest-dom/vitest';
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { render, screen, fireEvent } from '@testing-library/react';
import { ConsoleChatbotFab } from '../ConsoleChatbotFab';

vi.mock('@object-ui/i18n', () => ({
Expand All@@ -21,4 +21,16 @@ describe('ConsoleChatbotFab', () => {
expect(fab).toHaveClass('bottom-20');
expect(fab).toHaveClass('sm:bottom-6');
});

it('ADR-0057 P3b — when onOpenDock is wired, the FAB launches the dock instead of the overlay', () => {
const onOpenDock = vi.fn();
render(<ConsoleChatbotFab appLabel="Workspace" objects={[]} onOpenDock={onOpenDock} />);

// Still the lightweight launcher button (no heavy floating chatbot mounted).
const fab = screen.getByTestId('console-chatbot-fab');
fireEvent.click(fab);
expect(onOpenDock).toHaveBeenCalledTimes(1);
// The floating overlay is never armed in dock mode — the button stays.
expect(screen.getByTestId('console-chatbot-fab')).toBeInTheDocument();
});
});
Loading