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
35 changes: 33 additions & 2 deletions src/web-ui/src/app/hooks/useApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Provides unified app state management and actions.
*/

import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useSyncExternalStore } from 'react';
import {
UseAppReturn,
AppState,
Expand Down Expand Up @@ -56,11 +56,19 @@ export const useApp = (): UseAppReturn => {
const nextChatCollapsed = !state.layout.chatCollapsed;
appManager.updateLayout({
chatCollapsed: nextChatCollapsed,
// Collapsing the chat pane exits full-width tiled chat so the layout
// resets to its centered column state.
chatFullWidth: nextChatCollapsed ? false : state.layout.chatFullWidth,
// Keep behavior aligned with editor-mode layout:
// when chat is hidden, ensure the right panel is visible to occupy center space.
rightPanelCollapsed: nextChatCollapsed ? false : state.layout.rightPanelCollapsed
});
}, [state.layout.chatCollapsed, state.layout.rightPanelCollapsed]);
}, [state.layout.chatCollapsed, state.layout.rightPanelCollapsed, state.layout.chatFullWidth]);

const toggleChatFullWidth = useCallback(() => {
const next = !state.layout.chatFullWidth;
appManager.updateLayout({ chatFullWidth: next });
}, [state.layout.chatFullWidth]);

const switchLeftPanelTab = useCallback((tab: PanelType) => {
appManager.updateLayout({
Expand Down Expand Up @@ -222,6 +230,7 @@ export const useApp = (): UseAppReturn => {
toggleRightPanel,
toggleBottomTerminalPanel,
toggleChatPanel,
toggleChatFullWidth,
switchLeftPanelTab,
updateLeftPanelWidth,
updateCenterPanelWidth,
Expand Down Expand Up @@ -290,3 +299,25 @@ export const useTabs = () => {
selectTab
};
};

// Fine-grained read-only subscription to the chat full-width flag. Uses
// `useSyncExternalStore` so only components observing this flag re-render when
// it flips, instead of re-rendering on any app state change.
export const useChatFullWidth = (): boolean => {
return useSyncExternalStore(
(callback) => appManager.addEventListener(callback),
() => appManager.getState().layout.chatFullWidth,
() => false, // SSR / non-browser snapshot
);
};

// Fine-grained action to toggle the chat full-width flag without subscribing to
// the whole app store. Reads the current value straight from the manager rather
// than a subscribed snapshot, so components that only need this action re-render
// only when they observe the flag via `useChatFullWidth`, never on arbitrary app
// state changes.
export const useToggleChatFullWidth = (): (() => void) => {
return useCallback(() => {
appManager.updateLayout({ chatFullWidth: !appManager.getState().layout.chatFullWidth });
}, []);
};
8 changes: 8 additions & 0 deletions src/web-ui/src/app/scenes/session/ChatPane.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,11 @@
width: 100%;
height: 100%;
}

.bitfun-chat-pane__content--chat-full-width {
/* Full-width tiled chat: stretch the reading column edge to edge instead of
* capping at the content max-width token (900px). */
.virtual-item-wrapper {
max-width: none;
}
}
5 changes: 4 additions & 1 deletion src/web-ui/src/app/scenes/session/ChatPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { LineRange } from '@/component-library';
import path from 'path-browserify';
import { createLogger } from '@/shared/utils/logger';
import { hasNonFileUriScheme } from '@/shared/utils/pathUtils';
import { useChatFullWidth } from '@/app/hooks/useApp';

import './ChatPane.scss';

Expand Down Expand Up @@ -50,6 +51,7 @@ const ChatPaneInner: React.FC<ChatPaneProps> = ({
chatInputRegistration,
}) => {
const addTab = useCanvasStore(state => state.addTab);
const chatFullWidth = useChatFullWidth();
const deferredTaskDetailTimersRef = useRef<number[]>([]);
const deferredTaskDetailIdleCallbacksRef = useRef<number[]>([]);

Expand Down Expand Up @@ -151,7 +153,7 @@ const ChatPaneInner: React.FC<ChatPaneProps> = ({

return (
<div data-bf-component="chat-pane" data-bf-part="root"
className="bitfun-chat-pane__content"
className={`bitfun-chat-pane__content${chatFullWidth ? ' bitfun-chat-pane__content--chat-full-width' : ''}`}
data-shortcut-scope="chat"
data-fullscreen={isFullscreen}
data-testid="chat-pane"
Expand All @@ -178,6 +180,7 @@ const ChatPaneInner: React.FC<ChatPaneProps> = ({
isSceneActive={isSceneActive}
onSendMessage={(_message: string) => {}}
registration={chatInputRegistration}
className={chatFullWidth ? 'bitfun-chat-input--chat-full-width' : ''}
/>
)}
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/web-ui/src/app/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface LayoutState {
centerPanelWidth: number;
centerPanelCollapsed: boolean;
chatCollapsed: boolean;
chatFullWidth: boolean;
rightPanelWidth: number; // Fixed right panel width
rightPanelCollapsed: boolean;
bottomTerminalPanelHeight: number;
Expand Down Expand Up @@ -191,6 +192,7 @@ export interface UseAppReturn {
toggleRightPanel: () => void;
toggleBottomTerminalPanel: () => void;
toggleChatPanel: () => void;
toggleChatFullWidth: () => void;
switchLeftPanelTab: (tab: PanelType) => void;
updateLeftPanelWidth: (width: number, options?: { persist?: boolean }) => void;
updateCenterPanelWidth: (width: number) => void;
Expand Down Expand Up @@ -231,6 +233,7 @@ export const DEFAULT_LAYOUT_STATE: LayoutState = {
: 960,
centerPanelCollapsed: false,
chatCollapsed: false,
chatFullWidth: false,
rightPanelWidth: typeof window !== 'undefined'
? Math.max(540, Math.min(800, Math.floor(window.innerWidth * 0.35))) // Right 35%, min 540px (for config-tabs), max 800px
: 540,
Expand Down
18 changes: 16 additions & 2 deletions src/web-ui/src/flow_chat/components/ChatInput.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
width: 100%;
height: auto;
min-height: 0;
max-width: 900px;
max-width: var(--bf-appearance-token-flowchat-content-max-width);
z-index: $z-overlay;
display: flex;
flex-direction: column;
Expand All @@ -27,6 +27,20 @@
padding-bottom: 32px;
}

/* Full-width tiled chat: the composer stretches edge to edge with the chat
* pane (still horizontally centered) instead of capping at 900px. */
&:has(.bitfun-chat-input--chat-full-width) {
max-width: none;
padding-left: $size-gap-4;
padding-right: $size-gap-4;
}

/* Full-width tiled chat: inner rows (the file modifications bar) stretch
* with the composer instead of capping at 900px. */
&:has(.bitfun-chat-input--chat-full-width) .session-file-modifications-bar {
max-width: none;
}

&.bitfun-context-drop-zone--can-accept {
.bitfun-chat-input__box {
border-color: var(--bf-appearance-token-border-medium);
Expand Down Expand Up @@ -602,7 +616,7 @@

.session-file-modifications-bar {
width: 100%;
max-width: 900px;
max-width: var(--bf-appearance-token-flowchat-content-max-width);
}

& > * {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@

&__runtime-status .runtime-status-slot__content {
width: 100%;
max-width: 900px;
max-width: var(--bf-appearance-token-flowchat-content-max-width);
margin: 0 auto;
padding: 0 var(--bf-appearance-token-flowchat-content-inline-pad);
box-sizing: border-box;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
/* Same column box as .model-round-item so the collapse header (`>`) and the
* round's body text share one leading edge. */
box-sizing: border-box;
width: min(100%, 900px);
width: min(100%, var(--bf-appearance-token-flowchat-content-max-width));
margin: 0 auto;
padding: 0 var(--bf-appearance-token-flowchat-content-inline-pad);

Expand Down
25 changes: 25 additions & 0 deletions src/web-ui/src/flow_chat/components/modern/FlowChatHeader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ vi.mock('@/shared/utils/tabUtils', () => ({
createReviewPlatformTab: vi.fn(),
}));

const { mockToggleChatFullWidth } = vi.hoisted(() => ({
mockToggleChatFullWidth: vi.fn(),
}));

vi.mock('@/app/hooks/useApp', () => ({
useToggleChatFullWidth: () => mockToggleChatFullWidth,
useChatFullWidth: () => false,
}));

vi.mock('./SessionFilesBadge', () => ({
SessionFilesBadge: () => <div data-testid="session-files-badge" />,
}));
Expand Down Expand Up @@ -145,6 +154,22 @@ describe('FlowChatHeader', () => {
expect(container.querySelector('[data-testid="flowchat-header-turn-next"]')).toBeNull();
});

it('renders a full-width toggle that flips the chat layout', () => {
mockToggleChatFullWidth.mockClear();

act(() => {
root.render(<FlowChatHeader {...createProps()} />);
});

const toggle = container.querySelector<HTMLButtonElement>('[data-testid="session-fullwidth-toggle"]');
expect(toggle).not.toBeNull();

act(() => {
toggle?.click();
});
expect(mockToggleChatFullWidth).toHaveBeenCalledTimes(1);
});

it('places the Agent tree entry immediately before background commands', () => {
act(() => {
root.render(<FlowChatHeader {...createProps({ sessionId: 'session-1' })} />);
Expand Down
17 changes: 16 additions & 1 deletion src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import React, { useEffect, useLayoutEffect, useMemo, useRef, useState, useCallback } from 'react';
import { createPortal } from 'react-dom';
import { ChevronDown, ChevronUp, GitPullRequest, Keyboard, MoreHorizontal, Search, Square, SquareTerminal, Terminal, X } from 'lucide-react';
import { ChevronDown, ChevronUp, GitPullRequest, Keyboard, Maximize2, Minimize2, MoreHorizontal, Search, Square, SquareTerminal, Terminal, X } from 'lucide-react';
import { Tooltip, IconButton, Input } from '@/component-library';
import { useTranslation } from 'react-i18next';
import { SessionFilesBadge } from './SessionFilesBadge';
Expand All @@ -16,6 +16,7 @@ import { getAppearanceOverlayHost } from '@/infrastructure/appearance';
import { computeFixedPopoverPosition } from '@/shared/utils/fixedPopoverViewport';
import { useAnchoredPopoverPosition } from '@/shared/utils/useAnchoredPopoverPosition';
import { createReviewPlatformTab } from '@/shared/utils/tabUtils';
import { useChatFullWidth, useToggleChatFullWidth } from '@/app/hooks/useApp';
import './FlowChatHeader.scss';

export interface FlowChatHeaderCommandSummary {
Expand Down Expand Up @@ -102,6 +103,8 @@ export const FlowChatHeader: React.FC<FlowChatHeaderProps> = ({
onStopAllBackgroundCommands,
}) => {
const { t } = useTranslation('flow-chat');
const toggleChatFullWidth = useToggleChatFullWidth();
const isChatFullWidth = useChatFullWidth();
const { currentWorkspace } = useWorkspaceContext();
const [isBackgroundCommandPanelOpen, setIsBackgroundCommandPanelOpen] = useState(false);
const [isBackgroundCommandSectionMenuOpen, setIsBackgroundCommandSectionMenuOpen] = useState(false);
Expand Down Expand Up @@ -769,6 +772,18 @@ export const FlowChatHeader: React.FC<FlowChatHeaderProps> = ({
<Search size={14} />
</IconButton>
)}

<IconButton
className="flowchat-header__fullwidth-btn"
variant="ghost"
size="xs"
onClick={() => toggleChatFullWidth()}
tooltip={t(isChatFullWidth ? 'layout.fullWidth.exit' : 'layout.fullWidth.enter')}
aria-label={t(isChatFullWidth ? 'layout.fullWidth.exit' : 'layout.fullWidth.enter')}
data-testid="session-fullwidth-toggle"
>
{isChatFullWidth ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
</IconButton>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

&--footer &__content {
width: 100%;
max-width: 900px;
max-width: var(--bf-appearance-token-flowchat-content-max-width);
margin: 0 auto;
padding: 0 var(--bf-appearance-token-flowchat-content-inline-pad);
box-sizing: border-box;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* groups, user messages, notices). Matches ChatInput's 900px so headers,
* `>` collapse rows, and bubbles all sit on the same leading edge.
*/
max-width: 900px;
max-width: var(--bf-appearance-token-flowchat-content-max-width);
margin: 0 auto;
box-sizing: border-box;
min-height: 1px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ function createCssTokens(palette: AppearancePalette): Record<string, string> {
'--bf-appearance-token-flowchat-control-pad-x': '0.75rem',
'--bf-appearance-token-flowchat-content-inline-pad': '3rem',
'--bf-appearance-token-flowchat-content-inline-pad-mobile': '1.5rem',
'--bf-appearance-token-flowchat-content-max-width': '900px',
'--bf-appearance-token-flowchat-card-gap': '0.42rem',
'--bf-appearance-token-flowchat-card-radius': effects.radius.base,
'--bf-appearance-token-flowchat-card-pad-y': '0.625rem',
Expand Down
4 changes: 4 additions & 0 deletions src/web-ui/src/locales/en-US/flow-chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@
"comfortable": "Comfortable",
"expanded": "Expanded"
},
"fullWidth": {
"enter": "Tile chat full width",
"exit": "Exit full-width tiled chat"
},
"resizer": {
"leftAriaLabel": "Resize left panel",
"centerAriaLabel": "Resize panel",
Expand Down
4 changes: 4 additions & 0 deletions src/web-ui/src/locales/zh-CN/flow-chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@
"comfortable": "舒适",
"expanded": "展开"
},
"fullWidth": {
"enter": "全宽平铺对话",
"exit": "退出全宽平铺"
},
"resizer": {
"leftAriaLabel": "调整左侧面板大小",
"centerAriaLabel": "调整面板大小",
Expand Down
4 changes: 4 additions & 0 deletions src/web-ui/src/locales/zh-TW/flow-chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@
"comfortable": "舒適",
"expanded": "展開"
},
"fullWidth": {
"enter": "全寬平鋪對話",
"exit": "退出全寬平鋪"
},
"resizer": {
"leftAriaLabel": "調整左側面板大小",
"centerAriaLabel": "調整面板大小",
Expand Down