mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
fix(agent-team): isolate generated Remote browser entry
This commit is contained in:
@@ -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, {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user