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
47 changes: 45 additions & 2 deletions src/web-ui/src/app/components/NavPanel/MainNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { useMyAgentStore } from '../../scenes/my-agent/myAgentStore';
import { useMiniAppCatalogSync } from '../../scenes/miniapps/hooks/useMiniAppCatalogSync';
import { flowChatManager } from '@/flow_chat/services/FlowChatManager';
import { resolveAgentTypeForSessionCreation } from '@/flow_chat/services/flow-chat-manager';
import { openMainSession } from '@/flow_chat/services/sessionActivation';
import { workspaceManager } from '@/infrastructure/services/business/workspaceManager';
import { useWorkspaceContext } from '@/infrastructure/contexts/WorkspaceContext';
import { createLogger } from '@/shared/utils/logger';
Expand All @@ -37,6 +38,7 @@ import { WorkspaceKind, isRemoteWorkspace } from '@/shared/types';
import {
findReusableEmptySessionId,
flowChatSessionConfigForWorkspace,
pickPrimaryAssistantWorkspace,
pickWorkspaceForProjectChatSession,
} from '@/app/utils/projectSessionWorkspace';
import { getRecentWorkspaceLineParts } from '@/shared/utils/recentWorkspaceDisplay';
Expand Down Expand Up @@ -172,11 +174,14 @@ const MainNav: React.FC<MainNavProps> = ({
const setSessionMode = useSessionModeStore(s => s.setMode);
const isAssistantWorkspaceActive = currentWorkspace?.workspaceKind === WorkspaceKind.Assistant;

const defaultAssistantWorkspace = useMemo(
() => assistantWorkspacesList.find(w => !w.assistantId) ?? assistantWorkspacesList[0] ?? null,
const primaryAssistantWorkspace = useMemo(
() => pickPrimaryAssistantWorkspace(assistantWorkspacesList),
[assistantWorkspacesList]
);

const defaultAssistantWorkspace =
primaryAssistantWorkspace ?? assistantWorkspacesList[0] ?? null;

const toggleNavSearch = useCallback(() => {
setSearchOpen((v) => !v);
}, []);
Expand Down Expand Up @@ -251,6 +256,30 @@ const MainNav: React.FC<MainNavProps> = ({
void handleCreateProjectSession('Cowork');
}, [handleCreateProjectSession, setSessionMode]);

const handleCreatePrimaryAssistantSession = useCallback(async () => {
if (!primaryAssistantWorkspace) {
notificationService.warning(t('nav.workspaces.createSessionFailed'), { duration: 4000 });
return;
}

try {
const sessionId = await flowChatManager.createChatSession(
flowChatSessionConfigForWorkspace(primaryAssistantWorkspace),
'Claw'
);
await openMainSession(sessionId, {
workspaceId: primaryAssistantWorkspace.id,
activateWorkspace: setActiveWorkspace,
});
} catch (error) {
log.error('Failed to create primary assistant session', { error });
notificationService.error(
error instanceof Error ? error.message : t('nav.workspaces.createSessionFailed'),
{ duration: 4000 }
);
}
}, [primaryAssistantWorkspace, setActiveWorkspace, t]);

const handleOpenProject = useCallback(async () => {
try {
const { pickWorkspaceDirectory } = await import(
Expand Down Expand Up @@ -455,6 +484,7 @@ const MainNav: React.FC<MainNavProps> = ({

const createCodeTooltip = t('nav.sessions.newCodeSession');
const createCoworkTooltip = t('nav.sessions.newCoworkSession');
const createAssistantSessionTooltip = t('nav.sessions.newPrimaryAssistantSession');
const assistantTooltip = t('nav.items.persona');
const addWorkspaceTooltip = t('nav.tooltips.addWorkspace');
const isAssistantActive = activeTabId === 'assistant';
Expand Down Expand Up @@ -618,6 +648,19 @@ const MainNav: React.FC<MainNavProps> = ({
collapsible
isOpen={expandedSections.has('assistant-sessions')}
onToggle={() => toggleSection('assistant-sessions')}
actions={
<Tooltip content={createAssistantSessionTooltip} placement="right" followCursor>
<button
type="button"
className="bitfun-nav-panel__section-action"
aria-label={createAssistantSessionTooltip}
onClick={() => { void handleCreatePrimaryAssistantSession(); }}
data-testid="nav-primary-assistant-session-add-btn"
>
<Plus size={13} />
</button>
</Tooltip>
}
/>
<div className={`bitfun-nav-panel__collapsible${expandedSections.has('assistant-sessions') ? '' : ' is-collapsed'}`}>
<div className="bitfun-nav-panel__collapsible-inner">
Expand Down
33 changes: 32 additions & 1 deletion src/web-ui/src/app/utils/projectSessionWorkspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { afterEach, describe, expect, it } from 'vitest';
import { flowChatStore } from '@/flow_chat/store/FlowChatStore';
import type { FlowChatState, Session } from '@/flow_chat/types/flow-chat';
import { WorkspaceKind, type WorkspaceInfo } from '@/shared/types';
import { findReusableEmptySessionId } from './projectSessionWorkspace';
import {
findReusableEmptySessionId,
pickPrimaryAssistantWorkspace,
} from './projectSessionWorkspace';

const resetStore = () => {
flowChatStore.setState((): FlowChatState => ({
Expand Down Expand Up @@ -58,3 +61,31 @@ describe('findReusableEmptySessionId', () => {
expect(findReusableEmptySessionId(workspace, 'agentic')).toBeNull();
});
});

describe('pickPrimaryAssistantWorkspace', () => {
const createAssistantWorkspace = (
id: string,
assistantId?: string,
): WorkspaceInfo => ({
id,
name: id,
rootPath: `/assistants/${id}`,
workspaceKind: WorkspaceKind.Assistant,
assistantId,
});

it('selects the primary assistant even when a named assistant appears first', () => {
const namedAssistant = createAssistantWorkspace('named', 'assistant-1');
const primaryAssistant = createAssistantWorkspace('primary');

expect(
pickPrimaryAssistantWorkspace([namedAssistant, primaryAssistant])
).toBe(primaryAssistant);
});

it('does not fall back to a named assistant', () => {
const namedAssistant = createAssistantWorkspace('named', 'assistant-1');

expect(pickPrimaryAssistantWorkspace([namedAssistant])).toBeNull();
});
});
15 changes: 15 additions & 0 deletions src/web-ui/src/app/utils/projectSessionWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ export function pickWorkspaceForProjectChatSession(
return normalWorkspacesList[0] ?? null;
}

/**
* The primary assistant workspace is the built-in assistant without an
* assistant id. Named assistants must never receive sessions created from the
* global "Assistant Sessions" action.
*/
export function pickPrimaryAssistantWorkspace(
assistantWorkspacesList: WorkspaceInfo[]
): WorkspaceInfo | null {
return assistantWorkspacesList.find(
workspace =>
workspace.workspaceKind === WorkspaceKind.Assistant &&
!workspace.assistantId
) ?? null;
}

/**
* Build create_session config from the live workspace. After Peer Device Mode
* switch, callers must pass this (not `{}`) so the peer host never sees a
Expand Down
1 change: 1 addition & 0 deletions src/web-ui/src/locales/en-US/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
"newCoworkSession": "New Cowork session",
"newExternalAgentSessionShort": "{{agentName}} Session",
"newClawSession": "New Claw session",
"newPrimaryAssistantSession": "New primary assistant session",
"modeCode": "Code",
"modeCowork": "Cowork",
"noSessions": "No sessions",
Expand Down
1 change: 1 addition & 0 deletions src/web-ui/src/locales/zh-CN/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
"newCoworkSession": "新建 Cowork 会话",
"newExternalAgentSessionShort": "{{agentName}} 会话",
"newClawSession": "新建 Claw 会话",
"newPrimaryAssistantSession": "新建主助理会话",
"modeCode": "Code",
"modeCowork": "Cowork",
"noSessions": "暂无会话",
Expand Down
1 change: 1 addition & 0 deletions src/web-ui/src/locales/zh-TW/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
"newCoworkSession": "新增 Cowork 會話",
"newExternalAgentSessionShort": "{{agentName}} 會話",
"newClawSession": "新增 Claw 會話",
"newPrimaryAssistantSession": "新增主助理會話",
"modeCode": "Code",
"modeCowork": "Cowork",
"noSessions": "暫無會話",
Expand Down