fix(agent-team): isolate generated Remote browser entry

This commit is contained in:
Dudu-0223
2026-08-26 10:56:57 +08:00
committed by fengsy
parent 61dea35bd2
commit c6cab2aada
4 changed files with 119 additions and 91 deletions
@@ -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<TeamActionResult<TeamView>> {
return await ctx.remote.teams.view(leadSessionId(sessionId))
},
async createTask(sessionId, input): Promise<TeamActionResult<TeamTask>> {
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<void> {
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<void>> {
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)
}
@@ -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<TeamActionResult<TeamView>> {
return await ctx.remote.agentTeams.view(leadSessionId(sessionId))
},
async createTask(sessionId, input): Promise<TeamActionResult<TeamTask>> {
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<void> {
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<void>> {
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()
}
}
@@ -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<unknown> = 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, {
+3
View File
@@ -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',