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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Prerequisites: [Node.js](https://nodejs.org/) 22.12+ (LTS recommended), [pnpm](h

### First run

1. Launch BitFun, click **Open** on the Welcome tab, and choose a project folder.
1. Launch BitFun, then use the workspace controls to open a project folder.
2. Open **More options (…) → Settings → Models → Create First Configuration**.
3. Choose a provider, enter its API key, select one or more models, and click **Save**. BitFun makes the first saved model primary and tests the connection automatically.
4. Return to the **Session** tab, type a concrete task, and press Enter or click **Send**.
Expand Down
1 change: 0 additions & 1 deletion src/shared/interactive-capabilities/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -8889,7 +8889,6 @@
"settings.shortcuts.open": "setting.application.input"
},
"sceneOwners": {
"welcome": "feature.ai-assistant",
"session": "feature.ai-assistant",
"terminal": "feature.terminal",
"git": "feature.git",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ describe('MiniAppEntry activity', () => {
container.remove();
useMiniAppStore.setState({ apps: [], runningWorkerIds: [], customizingAppIds: [] });
useSceneStore.setState({
openTabs: [{ id: 'welcome', lastUsed: 1 }],
activeTabId: 'welcome',
navHistory: ['welcome'],
navCursor: 0,
openTabs: [],
activeTabId: null,
navHistory: [],
navCursor: -1,
});
});

Expand Down
2 changes: 1 addition & 1 deletion src/web-ui/src/app/components/SceneBar/SceneBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ const SceneBar: React.FC<SceneBarProps> = ({
className="bitfun-scene-bar__tabs"
aria-label={t('sceneBar.tabsLabel')}
items={tabItems}
value={activeTabId}
value={activeTabId ?? undefined}
onValueChange={handleTabValueChange}
onScroll={handleTabsScroll}
onWheel={handleTabsWheel}
Expand Down
1 change: 0 additions & 1 deletion src/web-ui/src/app/components/SceneBar/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export type SceneTabIcon = (props: SceneTabIconProps) => ReactNode;

/** Scene tab identifier. Open scenes are kept until the user closes them. */
export type SceneTabId =
| 'welcome'
| 'session'
| 'terminal'
| 'git'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface TabScrollState {
}

interface UseSceneTabNavigationOptions {
activeTabId: SceneTabId;
activeTabId: SceneTabId | null;
navigationMotion: InteractionMotion;
openTabIds: readonly SceneTabId[];
}
Expand Down
8 changes: 5 additions & 3 deletions src/web-ui/src/app/components/SceneTopBar/SceneChrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface SceneChromeContributionRecord {
}

interface SceneChromeContextValue {
activeSceneId: SceneTabId;
activeSceneId: SceneTabId | null;
setContribution: (
sceneId: SceneTabId,
owner: symbol,
Expand All @@ -30,7 +30,7 @@ const SceneChromeContext = createContext<SceneChromeContextValue | null>(null);
const SceneChromeContentContext = createContext<ReactNode>(null);

interface SceneChromeProviderProps {
activeSceneId: SceneTabId;
activeSceneId: SceneTabId | null;
children: ReactNode;
}

Expand Down Expand Up @@ -73,7 +73,9 @@ export const SceneChromeProvider: React.FC<SceneChromeProviderProps> = ({
setContribution,
removeContribution,
}), [activeSceneId, removeContribution, setContribution]);
const activeContent = contributions.get(activeSceneId)?.content ?? null;
const activeContent = activeSceneId
? contributions.get(activeSceneId)?.content ?? null
: null;

return (
<SceneChromeContext.Provider value={registrationValue}>
Expand Down
2 changes: 1 addition & 1 deletion src/web-ui/src/app/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ const AppLayout: React.FC<AppLayoutProps> = ({ className = '' }) => {
}, [canUseNativeWindowControls, handleToggleFullscreen, isToolbarMode, showWindowFullscreenHint]);
const activeSceneId = useSceneStore(s => s.activeTabId);
const isAgentScene = activeSceneId === 'session';
const isWelcomeScene = activeSceneId === 'welcome';
const isWelcomeScene = activeSceneId === null;

const isTransitioning = false;
const transitionDir: TransitionDirection = null;
Expand Down
22 changes: 1 addition & 21 deletions src/web-ui/src/app/scenes/SceneViewport.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,7 @@
border-radius: inherit;
}

// ── Welcome overlay (app start) ──────────────────────

&__clip--welcome {
display: flex;
align-items: center;
justify-content: center;
}

// ── Empty state (all tabs closed) ─────────────────────

&__clip--empty {
display: flex;
align-items: center;
justify-content: center;
}

&__empty-hint {
color: var(--bf-appearance-token-color-text-muted);
font-size: var(--bf-appearance-token-font-size-sm);
margin: 0;
}
// ── Tabless welcome state ─────────────────────────────

&__empty {
display: flex;
Expand Down
27 changes: 26 additions & 1 deletion src/web-ui/src/app/scenes/SceneViewport.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const sceneHarness = vi.hoisted(() => {
return {
state: {
openTabs: [{ id: 'session', lastUsed: 0 }],
activeTabId: 'session',
activeTabId: 'session' as string | null,
navigationMotion: 'instant',
navigationSequence: 0,
},
Expand Down Expand Up @@ -55,6 +55,10 @@ vi.mock('./assistant/AssistantScene', () => ({
default: () => <div data-testid="assistant-scene-content" />,
}));

vi.mock('./welcome/WelcomeScene', () => ({
default: () => <div data-testid="welcome-scene" />,
}));

vi.mock('./agents/AgentsScene', () => ({
default: () => {
if (!sceneHarness.agentsAreReady()) {
Expand Down Expand Up @@ -85,6 +89,12 @@ describe('SceneViewport transitions', () => {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
sceneHarness.state = {
openTabs: [{ id: 'session', lastUsed: 0 }],
activeTabId: 'session',
navigationMotion: 'instant',
navigationSequence: 0,
};
});

afterEach(() => {
Expand All @@ -99,6 +109,21 @@ describe('SceneViewport transitions', () => {
.filter(scene => scene.classList.contains('bitfun-scene-viewport__scene--visible'));
}

it('renders the welcome surface when no tab is open', () => {
sceneHarness.state = {
openTabs: [],
activeTabId: null,
navigationMotion: 'instant',
navigationSequence: 0,
};

act(() => root.render(<SceneViewport />));

expect(visibleScenes()).toHaveLength(1);
expect(container.querySelector('[data-testid="welcome-scene"]')).not.toBeNull();
expect(container.querySelector('[role="tab"]')).toBeNull();
});

it('keeps one scene visible while a lazy pointer target becomes ready', async () => {
act(() => root.render(<SceneViewport />));
expect(visibleScenes().map(scene => scene.getAttribute('data-scene-id'))).toEqual(['session']);
Expand Down
16 changes: 6 additions & 10 deletions src/web-ui/src/app/scenes/SceneViewport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* All open scenes stay mounted, but only the active tab is visible, preserving
* state across tab switches until the user explicitly closes a scene.
*
* 'welcome' is a proper scene tab; it auto-closes when any other
* scene is explicitly opened.
* When no tabs are open, the viewport renders WelcomeScene as a shell-owned
* landing surface rather than manufacturing a tab for it.
*/

import React, {
Expand All @@ -25,6 +25,7 @@ import { DotMatrixLoader } from '@/component-library';
import SettingsScene from './settings/SettingsScene';
import AssistantScene from './assistant/AssistantScene';
import SessionScene from './session/SessionScene';
import WelcomeScene from './welcome/WelcomeScene';
import './SceneViewport.scss';

// Session is the primary interaction path. Keep it in the main scene bundle so
Expand All @@ -44,7 +45,6 @@ const BrowserScene = lazy(() => import('./browser/BrowserScene'));
const TodosScene = lazy(() => import('./todos/TodosScene'));
const InsightsScene = lazy(() => import('./my-agent/InsightsScene'));
const ShellScene = lazy(() => import('./shell/ShellScene'));
const WelcomeScene = lazy(() => import('./welcome/WelcomeScene'));
const MiniAppScene = lazy(() => import('./miniapps/MiniAppScene'));
const PanelViewScene = lazy(() => import('./panel-view/PanelViewScene'));

Expand Down Expand Up @@ -94,9 +94,7 @@ const SceneViewport: React.FC<SceneViewportProps> = ({ workspacePath, isEntering
navigationSequence,
} = useSceneManager();
const { t } = useI18n('common');
const activeRenderedSceneId: RenderedSceneId = openTabs.length === 0
? EMPTY_SCENE_ID
: activeTabId;
const activeRenderedSceneId: RenderedSceneId = activeTabId ?? EMPTY_SCENE_ID;
const [transition, setTransition] = useState<SceneTransition | null>(null);
const [readyVersion, setReadyVersion] = useState(0);
const readySceneIdsRef = useRef<Set<RenderedSceneId>>(new Set([EMPTY_SCENE_ID]));
Expand Down Expand Up @@ -243,7 +241,7 @@ const SceneViewport: React.FC<SceneViewportProps> = ({ workspacePath, isEntering
data-scene-active={isActive ? 'true' : 'false'}
data-bf-scene="workbench"
data-bf-part="scene"
data-bf-scene-id={isEmpty ? undefined : tabId}
data-bf-scene-id={isEmpty ? 'welcome' : tabId}
data-bf-state={[
isActive && 'active',
isEmpty && 'empty',
Expand All @@ -257,7 +255,7 @@ const SceneViewport: React.FC<SceneViewportProps> = ({ workspacePath, isEntering
data-bf-part="empty"
data-bf-state="empty"
>
<p className="bitfun-scene-viewport__empty-hint">{t('welcomeScene.emptyHint')}</p>
<WelcomeScene />
</div>
) : (
<Suspense
Expand Down Expand Up @@ -296,8 +294,6 @@ function renderScene(
isActive: boolean = false
) {
switch (id) {
case 'welcome':
return <WelcomeScene />;
case 'session':
return <SessionScene workspacePath={workspacePath} isEntering={isEntering} isActive={isActive} />;
case 'terminal':
Expand Down
5 changes: 5 additions & 0 deletions src/web-ui/src/app/scenes/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { describe, expect, it } from 'vitest';
import { SCENE_TAB_REGISTRY, getSceneDef } from './registry';

describe('scene tab icon registry', () => {
it('does not register a default welcome tab', () => {
expect(SCENE_TAB_REGISTRY.map(scene => scene.id)).not.toContain('welcome');
expect(SCENE_TAB_REGISTRY.some(scene => scene.defaultOpen)).toBe(false);
});

it('uses the design-system SessionIcon only for the session tab', () => {
expect(getSceneDef('session')?.Icon).toBe(SessionIcon);
expect(
Expand Down
9 changes: 0 additions & 9 deletions src/web-ui/src/app/scenes/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,6 @@ function catalogSceneIcon(name: IconName): SceneTabIcon {
}

export const SCENE_TAB_REGISTRY: SceneTabDef[] = [
{
id: 'welcome' as SceneTabId,
label: 'Welcome',
labelKey: 'welcomeScene.tabLabel',
Icon: catalogSceneIcon('side-chat'),
pinned: false,
singleton: true,
defaultOpen: true,
},
{
id: 'session' as SceneTabId,
label: 'Session',
Expand Down
6 changes: 2 additions & 4 deletions src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
/**
* WelcomeScene — the lightweight landing scene shown inside SceneViewport.
*
* It remains a regular scene tab. This phase establishes the greeting region;
* composer integration is deliberately deferred to the next shell iteration.
* WelcomeScene — the lightweight, tabless landing surface shown by
* SceneViewport until the user opens a scene.
*/

import React, { useState } from 'react';
Expand Down
26 changes: 17 additions & 9 deletions src/web-ui/src/app/stores/sceneStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,17 @@ describe('sceneStore transition snapshots', () => {
vi.restoreAllMocks();
});

it('publishes the first scene switch atomically without a blank active scene', () => {
const snapshots: Array<{ activeTabId: string; openTabIds: string[] }> = [];
it('starts on the welcome surface without creating a tab', () => {
const state = useSceneStore.getState();

expect(state.openTabs).toEqual([]);
expect(state.activeTabId).toBeNull();
expect(state.navHistory).toEqual([]);
expect(state.navCursor).toBe(-1);
});

it('publishes the first scene switch atomically from the tabless welcome surface', () => {
const snapshots: Array<{ activeTabId: string | null; openTabIds: string[] }> = [];
const unsubscribe = useSceneStore.subscribe(state => {
snapshots.push({
activeTabId: state.activeTabId,
Expand All @@ -26,8 +35,7 @@ describe('sceneStore transition snapshots', () => {

expect(snapshots).toHaveLength(1);
expect(snapshots[0].activeTabId).toBe('settings');
expect(snapshots[0].openTabIds).toContain('settings');
expect(snapshots[0].openTabIds).not.toContain('welcome');
expect(snapshots[0].openTabIds).toEqual(['session', 'settings']);
});

it('records pointer scene navigation without animating keyboard activation', () => {
Expand Down Expand Up @@ -104,7 +112,7 @@ describe('sceneStore transition snapshots', () => {
expect(useSceneStore.getState().activeTabId).toBe('terminal');
});

it('resets an expanded tab set when the peer host changes', () => {
it('resets an expanded tab set to the tabless welcome surface when the peer host changes', () => {
useSceneStore.getState().openScene('settings');
useSceneStore.getState().openScene('terminal');
useSceneStore.getState().openScene('git');
Expand All @@ -113,9 +121,9 @@ describe('sceneStore transition snapshots', () => {
useSceneStore.getState().resetForPeerSwitch();

const state = useSceneStore.getState();
expect(state.openTabs.map(tab => tab.id)).toEqual(['welcome']);
expect(state.activeTabId).toBe('welcome');
expect(state.navHistory).toEqual(['welcome']);
expect(state.navCursor).toBe(0);
expect(state.openTabs).toEqual([]);
expect(state.activeTabId).toBeNull();
expect(state.navHistory).toEqual([]);
expect(state.navCursor).toBe(-1);
});
});
Loading
Loading