From c6cab2aadadb929854a497d48e94730f2453686c Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Thu, 20 Aug 2026 11:14:59 +0800 Subject: [PATCH] fix(agent-team): isolate generated Remote browser entry --- .../client-ui-agent-team/src/client/index.ts | 92 ++---------------- .../client-ui-agent-team/src/client/mount.ts | 97 +++++++++++++++++++ .../tests/browser-plugin.client.spec.ts | 18 ++-- vitest.config.ts | 3 + 4 files changed, 119 insertions(+), 91 deletions(-) create mode 100644 packages/experimental/client-ui-agent-team/src/client/mount.ts diff --git a/packages/experimental/client-ui-agent-team/src/client/index.ts b/packages/experimental/client-ui-agent-team/src/client/index.ts index 10c09bb255..37ca28acdd 100644 --- a/packages/experimental/client-ui-agent-team/src/client/index.ts +++ b/packages/experimental/client-ui-agent-team/src/client/index.ts @@ -1,92 +1,14 @@ -/** Browser plugin for the Agent Teams roster, task board, and Team-routed teammate navigation. */ +/** Browser entry binding the generated Team Remote artifact to its Client UI. */ -import teamsRemote from '@deepseek-ai/dsh-team/remote' -import type { - TeamMemberView as TeamRosterMember, - TeamTaskView as TeamTask, - TeamView, -} from '@deepseek-ai/dsh-team/client' -import type {} from '@deepseek-ai/dsh-team/remote' -import type {} from '@deepseek-ai/dsh-api-remotes/client' -import type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client' -import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' -import type {} from '@deepseek-ai/dsh-client-locale/client' -import { TeamAction, type TeamActionInjected, type TeamActionResult } from './TeamAction.tsx' -import { en, zh, type TeamKey } from './locales.ts' +import agentTeamsRemote from '@deepseek-ai/dsh-experimental-agent-team/remote' +import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client' +import { mountAgentTeamUi } from './mount.ts' +export { inject } from './mount.ts' export type { TeamActionInjected, TeamActionProps, TeamActionResult } from './TeamAction.tsx' export type { TeamKey } from './locales.ts' -declare module '@deepseek-ai/dsh-client-ui-slots' { - interface LocaleNamespaceMap { - /** Agent Teams roster and task-board copy. */ - team: TeamKey - } -} - -/** Required browser services for RPC, navigation, slots, and localized copy. */ -export const inject = ['sessions', 'remote', 'slots', 'locale'] - -function registerUi(ctx: ClientContext): void { - ctx.effect(() => ctx.locale.register('team', { zh, en }), 'ui-team: dictionaries') - const sessions = ctx.sessions - const leadSessionId = (sessionId: SessionId): SessionId => { - const address = sessions.binding(sessionId)?.session.getSnapshot().subagent?.address - return address?.parentSessionId ?? sessionId - } - - const actions: TeamActionInjected = { - async load(sessionId): Promise> { - return await ctx.remote.teams.view(leadSessionId(sessionId)) - }, - async createTask(sessionId, input): Promise> { - return await ctx.remote.teams.createTask(leadSessionId(sessionId), input) - }, - async updateTask(sessionId, input) { - const { owner, ...rest } = input - return await ctx.remote.teams.updateTask(leadSessionId(sessionId), { - ...rest, - ...owner === undefined ? {} : { owner }, - }) - }, - async openTeammate(sessionId: SessionId, member: TeamRosterMember): Promise { - if (member.role !== 'teammate') return - const parentSessionId = leadSessionId(sessionId) - await sessions.refreshSubagents(parentSessionId) - if (sessions.list.getSnapshot().current !== sessionId) return - sessions.openSubagent({ - parentSessionId, - childSessionId: member.id, - mode: 'continuable', - }) - }, - } - - ctx.slots.inject( - 'conversation.session.header.actions', - () => ctx.slots.register({ - name: 'conversation.session.header.actions', - id: 'agent-team', - order: 20, - locale: 'team', - inject: () => actions, - }, TeamAction), - ) -} - -/** Mount the generated Team Remote contribution, then register its browser UI. */ +/** Mount the generated Team Remote contribution and its browser UI. */ export async function apply(ctx: ClientContext): Promise<() => Promise> { - const disposeRemote = await ctx.remote.$mount(teamsRemote) - const ui = ctx.inject(['sessions', 'remote.teams', 'slots', 'locale'], registerUi) - try { - await ui - } catch (error) { - await ui.dispose() - await disposeRemote() - throw error - } - return async () => { - await ui.dispose() - await disposeRemote() - } + return await mountAgentTeamUi(ctx, agentTeamsRemote) } diff --git a/packages/experimental/client-ui-agent-team/src/client/mount.ts b/packages/experimental/client-ui-agent-team/src/client/mount.ts new file mode 100644 index 0000000000..4dae63a1cb --- /dev/null +++ b/packages/experimental/client-ui-agent-team/src/client/mount.ts @@ -0,0 +1,97 @@ +/** Source-safe Agent Teams browser registration and Remote mount lifecycle. */ + +import type { + TeamMemberView as TeamRosterMember, + TeamTaskView as TeamTask, + TeamView, +} from '@deepseek-ai/dsh-experimental-agent-team/client' +import type {} from '@deepseek-ai/dsh-experimental-agent-team/remote' +import type {} from '@deepseek-ai/dsh-api-remotes/client' +import type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client' +import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' +import type {} from '@deepseek-ai/dsh-client-locale/client' +import type { TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol' +import { TeamAction, type TeamActionInjected, type TeamActionResult } from './TeamAction.tsx' +import { en, zh, type TeamKey } from './locales.ts' + +declare module '@deepseek-ai/dsh-client-ui-slots' { + interface LocaleNamespaceMap { + /** Agent Teams roster and task-board copy. */ + team: TeamKey + } +} + +/** Required browser services for RPC, navigation, slots, and localized copy. */ +export const inject = ['sessions', 'remote', 'slots', 'locale'] + +function registerUi(ctx: ClientContext): void { + ctx.effect(() => ctx.locale.register('team', { zh, en }), 'ui-team: dictionaries') + const sessions = ctx.sessions + const leadSessionId = (sessionId: SessionId): SessionId => { + const address = sessions.binding(sessionId)?.session.getSnapshot().subagent?.address + return address?.parentSessionId ?? sessionId + } + + const actions: TeamActionInjected = { + async load(sessionId): Promise> { + return await ctx.remote.agentTeams.view(leadSessionId(sessionId)) + }, + async createTask(sessionId, input): Promise> { + return await ctx.remote.agentTeams.createTask(leadSessionId(sessionId), input) + }, + async updateTask(sessionId, input) { + const { owner, ...rest } = input + return await ctx.remote.agentTeams.updateTask(leadSessionId(sessionId), { + ...rest, + ...owner === undefined ? {} : { owner }, + }) + }, + async openTeammate(sessionId: SessionId, member: TeamRosterMember): Promise { + if (member.role !== 'teammate') return + const parentSessionId = leadSessionId(sessionId) + await sessions.refreshSubagents(parentSessionId) + if (sessions.list.getSnapshot().current !== sessionId) return + sessions.openSubagent({ + parentSessionId, + childSessionId: member.id, + mode: 'continuable', + }) + }, + } + + ctx.slots.inject( + 'conversation.session.header.actions', + () => ctx.slots.register({ + name: 'conversation.session.header.actions', + id: 'agent-team', + order: 20, + locale: 'team', + inject: () => actions, + }, TeamAction), + ) +} + +/** + * Mount one generated Team Remote contribution, then register its browser UI. + * @param ctx - Client Context carrying navigation, locale, slot, and Remote services. + * @param contribution - generated Team descriptors selected by the browser entry. + * @returns disposer for both the UI registrations and Remote namespace. + */ +export async function mountAgentTeamUi( + ctx: ClientContext, + contribution: TypertRemoteContribution, +): Promise<() => Promise> { + const disposeRemote = await ctx.remote.$mount(contribution) + const ui = ctx.inject(['sessions', 'remote.agentTeams', 'slots', 'locale'], registerUi) + try { + await ui + } catch (error) { + await ui.dispose() + await disposeRemote() + throw error + } + return async () => { + await ui.dispose() + await disposeRemote() + } +} diff --git a/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts b/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts index ebfec797fe..ee749e987e 100644 --- a/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts +++ b/packages/experimental/client-ui-agent-team/tests/browser-plugin.client.spec.ts @@ -3,15 +3,20 @@ import { describe, expect, it, vi } from 'vitest' import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client' import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client' -import type { TeamMemberView as TeamRosterMember, TeamTaskId } from '@deepseek-ai/dsh-team/client' -import type {} from '@deepseek-ai/dsh-team/remote' +import type { TeamMemberView as TeamRosterMember, TeamTaskId } from '@deepseek-ai/dsh-experimental-agent-team/client' +import type {} from '@deepseek-ai/dsh-experimental-agent-team/remote' +import type { TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol' import { TeamAction, type TeamActionInjected } from '../src/client/TeamAction.tsx' -import { apply, inject } from '../src/client/index.ts' +import { inject, mountAgentTeamUi } from '../src/client/mount.ts' import { apply as nodeApply } from '../src/index.ts' const SESSION = 'team-session' as SessionId const CHILD = 'team-child' as SessionId const TASK_ID = 'task-1' as TeamTaskId +const REMOTE: TypertRemoteContribution = { + package: '@deepseek-ai/dsh-experimental-agent-team', + descriptors: [], +} async function bench(options: { addressed?: boolean @@ -53,7 +58,7 @@ async function bench(options: { id: SESSION, name: 'lead', role: 'lead' as const, status: 'idle' as const, diagnostics: [], }], tasks: [task], } - ctx.provide('remote.teams', { + ctx.provide('remote.agentTeams', { view: (...args: unknown[]) => { calls.push({ method: 'teams/view', args }) return Promise.resolve(options.remoteFailure === 'view' @@ -111,9 +116,9 @@ async function bench(options: { } const fiber = options.registrationFailure === true ? ctx.plugin({ apply() {} }) - : ctx.plugin({ inject: [...inject], apply }) + : ctx.plugin({ inject: [...inject], apply: clientCtx => mountAgentTeamUi(clientCtx, REMOTE) }) const activation: Promise = options.registrationFailure === true - ? apply(ctx).catch((error: unknown) => error) + ? mountAgentTeamUi(ctx, REMOTE).catch((error: unknown) => error) : fiber.await() if (options.registrationFailure !== true) { await activation @@ -144,6 +149,7 @@ describe('ui-team browser plugin', () => { locale: 'team', }) expect(b.remote.mount).toHaveBeenCalledOnce() + expect(b.remote.mount).toHaveBeenCalledWith(REMOTE) const actions = (b.entry()!.inject as unknown as () => TeamActionInjected)() expect((await actions.load(SESSION)).ok).toBe(true) expect((await actions.createTask(SESSION, { diff --git a/vitest.config.ts b/vitest.config.ts index 584562420c..133089e651 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -268,6 +268,9 @@ export default defineConfig({ // only in lib; the post-build built-bin smoke executes both entries. 'packages/api/remotes/src/index.ts', 'packages/api/remotes/src/client/index.ts', + // The Team browser entry binds its source-covered mount lifecycle to + // the generated Team Remote contribution, which likewise exists only in lib. + 'packages/experimental/client-ui-agent-team/src/client/index.ts', // Slash/command/input round: per-file gaps deferred with the same // client-lane debt. TODO(gui): cover and remove with the lane above. 'packages/client/connection/src/client/fixture.ts',