From 4014f54cb2df669ecd169210edd3953f866664fe Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Wed, 29 Jul 2026 23:13:18 -0700 Subject: [PATCH] feat(web): add primary assistant session action --- .../src/app/components/NavPanel/MainNav.tsx | 47 ++++++++++++++++++- .../app/utils/projectSessionWorkspace.test.ts | 33 ++++++++++++- .../src/app/utils/projectSessionWorkspace.ts | 15 ++++++ src/web-ui/src/locales/en-US/common.json | 1 + src/web-ui/src/locales/zh-CN/common.json | 1 + src/web-ui/src/locales/zh-TW/common.json | 1 + 6 files changed, 95 insertions(+), 3 deletions(-) diff --git a/src/web-ui/src/app/components/NavPanel/MainNav.tsx b/src/web-ui/src/app/components/NavPanel/MainNav.tsx index 41e2d1af26..0c2a3a12d9 100644 --- a/src/web-ui/src/app/components/NavPanel/MainNav.tsx +++ b/src/web-ui/src/app/components/NavPanel/MainNav.tsx @@ -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'; @@ -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'; @@ -172,11 +174,14 @@ const MainNav: React.FC = ({ 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); }, []); @@ -251,6 +256,30 @@ const MainNav: React.FC = ({ 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( @@ -455,6 +484,7 @@ const MainNav: React.FC = ({ 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'; @@ -618,6 +648,19 @@ const MainNav: React.FC = ({ collapsible isOpen={expandedSections.has('assistant-sessions')} onToggle={() => toggleSection('assistant-sessions')} + actions={ + + + + } />
diff --git a/src/web-ui/src/app/utils/projectSessionWorkspace.test.ts b/src/web-ui/src/app/utils/projectSessionWorkspace.test.ts index 644755d78e..ec63e7de4e 100644 --- a/src/web-ui/src/app/utils/projectSessionWorkspace.test.ts +++ b/src/web-ui/src/app/utils/projectSessionWorkspace.test.ts @@ -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 => ({ @@ -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(); + }); +}); diff --git a/src/web-ui/src/app/utils/projectSessionWorkspace.ts b/src/web-ui/src/app/utils/projectSessionWorkspace.ts index 47f80850ad..b6557f6cfc 100644 --- a/src/web-ui/src/app/utils/projectSessionWorkspace.ts +++ b/src/web-ui/src/app/utils/projectSessionWorkspace.ts @@ -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 diff --git a/src/web-ui/src/locales/en-US/common.json b/src/web-ui/src/locales/en-US/common.json index 74e3eb69c4..8cbe8deb83 100644 --- a/src/web-ui/src/locales/en-US/common.json +++ b/src/web-ui/src/locales/en-US/common.json @@ -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", diff --git a/src/web-ui/src/locales/zh-CN/common.json b/src/web-ui/src/locales/zh-CN/common.json index 51c2acb153..d3156dddb1 100644 --- a/src/web-ui/src/locales/zh-CN/common.json +++ b/src/web-ui/src/locales/zh-CN/common.json @@ -178,6 +178,7 @@ "newCoworkSession": "新建 Cowork 会话", "newExternalAgentSessionShort": "{{agentName}} 会话", "newClawSession": "新建 Claw 会话", + "newPrimaryAssistantSession": "新建主助理会话", "modeCode": "Code", "modeCowork": "Cowork", "noSessions": "暂无会话", diff --git a/src/web-ui/src/locales/zh-TW/common.json b/src/web-ui/src/locales/zh-TW/common.json index 2a6423f38c..085f649ac5 100644 --- a/src/web-ui/src/locales/zh-TW/common.json +++ b/src/web-ui/src/locales/zh-TW/common.json @@ -178,6 +178,7 @@ "newCoworkSession": "新增 Cowork 會話", "newExternalAgentSessionShort": "{{agentName}} 會話", "newClawSession": "新增 Claw 會話", + "newPrimaryAssistantSession": "新增主助理會話", "modeCode": "Code", "modeCowork": "Cowork", "noSessions": "暫無會話",