Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-14-xyai-desktop-shell-chrome.md
2026-09-14-xyai-desktop-shell-chrome.md: b621aa06d43537e7bebec373d753dcd6aec25c5f
2026-09-14-xyai-desktop-shell-chrome.zh.md: 2ac5d4d9fed84de7809d04f329cfd28d8c3caa77
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Agent Note: XYAI desktop shell chrome

Status: implemented

English | [中文](2026-09-14-xyai-desktop-shell-chrome.zh.md)

## Problem

XYAI Studio 0.4 desktop cold-starts with the window title already branded, but the renderer shows two overlapping product UIs: official DSH hero copy under the XYAI welcome, a leftover boot ring, a floating 模型广场 chip in the sidebar, and product space tabs that do nothing except 关于我们.

## Decision

Hero layout CSS keys off `[data-xyai-hero-welcome]`: it expands the fish hitbox, hides the adjacent official headline group, and hides leftover `[data-dsh-boot]` only when `[data-slot="root"]` exists. Sidebar footer `action` slots that hold product-nav or the interact list override `display: contents` into a column so 模型广场 cannot sit beside an empty interact block. Space overlays are siblings of the top bar, not children of a zero-height pointer-events-none wrapper. Electron `dsh-app:` windows cannot host remote `https:` iframes, so 业务/生态/浏览器 render an in-shell copyable address; 关于我们 keeps its `srcDoc` document. Model hub and knowledge stay in `@xyai/dsh-product-base` because their Host inject lists do not wait on `webServer`. AI employees stay in `@xyai/dsh-product-collab`; Host `agentTeams` is optional at load so the employee library activates on desktop, while team spawn, mailbox, and task RPC refuse with `AGENT_TEAMS_UNAVAILABLE` when Agent Teams is absent. Each of those three client plugins sets `data-xyai-surface-*` on `<html>`; product nav disables a destination until that attribute appears.

## Alternatives considered

**Hashed CSS-module class selectors.** Rebuilds change `HeroShell.module.css` hashes, so `.pXSMma_headline` and the removed `.pXSMma_headlineText` cannot hide official copy after a DSH UI rebuild.

**Emptying `xyai-collab/cordis.patch.yml` on desktop.** The web XYAI profile still mounts AI employees through that bundle, and composition tests require the insert. Optional `agentTeams` keeps the library alive without deleting the collab layer.

**`shell.openExternal` IPC for remote spaces.** The desktop Electron main process has no such channel yet. A copyable in-shell address is honest without inventing a Host API in this change.

**Forking DSH EmptyHero.** XYAI remains a patch-layer product; data-attribute CSS plus a locale-owned kicker keep the official conversation hero intact for other profiles.

## Consequences

开发空间 keeps DSH new-session, workspace, and composer chrome without a second headline. 关于我们 continues to work offline. 业务/生态/浏览器 are reachable as copyable URLs on desktop and as iframes on ordinary `http(s)` pages. Sidebar destinations degrade to disabled controls instead of stacked dead chips when a plugin is not in the composition. Desktop still does not compose experimental Agent Teams, so collaboration team operations remain unavailable there until a later profile change. Electron still has no `openExternal` path.

## Testing

Brand-pack `hero-layout.spec.ts` pins selector stability. Dev-shell `chrome.spec.ts` and `plugin.client.spec.tsx` cover remote-embed refusal, About `srcDoc`, nav disable/enable, and empty interact compaction. AI-employees `backend.spec.ts` covers library RPC without Agent Teams.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Agent Note: XYAI 桌面壳层 chrome

Status: implemented

[English](2026-09-14-xyai-desktop-shell-chrome.md) | 中文

## Problem

XYAI Studio 0.4 桌面冷启动后窗口标题已经品牌化,但渲染层叠了两套产品界面:XYAI 欢迎区下面露出官方 DSH 首屏文案、残留启动环、侧栏中悬浮的「模型广场」,以及除「关于我们」外无响应的产品空间页。

## Decision

首屏布局 CSS 以 `[data-xyai-hero-welcome]` 为键:撑开鱼标命中盒,隐藏相邻官方标题组,并且仅在存在 `[data-slot="root"]` 时隐藏残留的 `[data-dsh-boot]`。侧栏底部承载产品导航或互动列表的 `action` 插槽把 `display: contents` 改回纵向排列,避免「模型广场」贴在空的互动块旁边。空间浮层是顶栏的兄弟节点,而不是零高度且 `pointer-events: none` 包裹器的子节点。Electron `dsh-app:` 窗口无法承载远程 `https:` iframe,因此业务/生态/浏览器渲染可复制地址的壳内面板;关于我们继续使用 `srcDoc` 文档。模型广场与知识库留在 `@xyai/dsh-product-base`,因为它们的 Host inject 并不等待 `webServer`。AI 员工留在 `@xyai/dsh-product-collab`;Host 加载时 `agentTeams` 可选,以便桌面上激活员工库,而在缺少 Agent Teams 时队友拉起、邮箱和任务 RPC 以 `AGENT_TEAMS_UNAVAILABLE` 拒绝。这三个客户端插件各自在 `<html>` 上设置 `data-xyai-surface-*`;产品导航在该属性出现之前禁用对应入口。

## Alternatives considered

**哈希后的 CSS module 类选择器。** 重新构建会改变 `HeroShell.module.css` 哈希,因此 `.pXSMma_headline` 以及已经不存在的 `.pXSMma_headlineText` 无法在 DSH UI 重建后隐藏官方文案。

**在桌面上清空 `xyai-collab/cordis.patch.yml`。** Web 版 XYAI Profile 仍通过该包挂载 AI 员工,组合测试也要求这条 insert。让 `agentTeams` 可选可以保住员工库,而不删除协作层。

**为远程空间增加 `shell.openExternal` IPC。** 桌面 Electron 主进程尚无该通道。可复制的壳内地址在这次改动里不虚构 Host API。

**分叉 DSH EmptyHero。** XYAI 仍是补丁层产品;数据属性 CSS 加上语言包所有的导语,让其他 Profile 继续使用官方对话首屏。

## Consequences

开发空间保留 DSH 的新建会话、工作区和输入框,而不再出现第二套标题。「关于我们」在离线时仍可用。业务/生态/浏览器在桌面上以可复制 URL 到达,在普通 `http(s)` 页面上仍用 iframe。插件不在组合中时,侧栏入口降级为禁用控件,而不是叠在一起的失效芯片。桌面仍不组合实验性 Agent Teams,因此在后续 Profile 变更之前,协作中的团队操作在桌面上仍不可用。Electron 仍没有 `openExternal` 路径。

## Testing

品牌包 `hero-layout.spec.ts` 锁定选择器稳定性。开发壳 `chrome.spec.ts` 与 `plugin.client.spec.tsx` 覆盖拒绝远程嵌入、关于我们 `srcDoc`、导航禁用/启用,以及空互动列表的压缩。AI 员工 `backend.spec.ts` 覆盖没有 Agent Teams 时的员工库 RPC。
4 changes: 2 additions & 2 deletions packages-xyai/xyai-ai-employees/README.i18n.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages-xyai/xyai-ai-employees/README.md
README.md: 90bc8a6a10f99b16d2983a523626bf5a73d2001b
README.zh.md: 9e339e738a5f147113b6673f327f2eba75741df3
README.md: 17cd99e7e59809b2b52cef259dfa32a8bd2c4088
README.zh.md: 2b910e0907c196db36a724f4fb6b9a9ca44a2ea4
5 changes: 3 additions & 2 deletions packages-xyai/xyai-ai-employees/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Choose this plugin when an XYAI desktop profile already composes DSH Agent Teams
name: '@xyai/dsh-ai-employees'
```

This package has no plugin config fields. It requires the `connection`, `settings`, `agents`, and `agentTeams` Host services; the browser half requires slots, locale, connection, and Session controllers. The XYAI profile supplies these dependencies.
This package has no plugin config fields. It requires the `connection`, `settings`, and `agents` Host services; the browser half requires slots, locale, connection, and Session controllers. Team spawn, mailbox, and task RPC need Host `agentTeams` and refuse with a clear error when that service is absent. The XYAI web profile supplies Agent Teams; the desktop seed does not.

-----

Expand All @@ -48,7 +48,7 @@ This package has no plugin config fields. It requires the `connection`, `setting
<details>
<summary>Implementation internals — click to expand</summary>

The Host stores the employee library, draft revisions, and single-chat bindings in the `xyai-ai-team` settings namespace. Its Connection RPC validates browser payloads and delegates teammate, mailbox, and task operations to `ctx.agentTeams`. The browser registers a full conversation View, a composer entry that opens the collaboration dialog, and a team-aware rename control.
The Host stores the employee library, draft revisions, and single-chat bindings in the `xyai-ai-team` settings namespace. Its Connection RPC validates browser payloads and, when `agentTeams` is present, delegates teammate, mailbox, and task operations to it. The browser registers a full conversation View, a composer entry that opens the collaboration dialog, and a team-aware rename control.

| File | Purpose |
|---|---|
Expand Down Expand Up @@ -92,6 +92,7 @@ Stable published employee instructions remain at the front of a fresh teammate S
- Scheduled routines remain employee configuration; this package does not register a scheduler job.
- Marketplace installation and online employee synchronization require their owning commerce or online adapters.
- Single-chat reuse applies while the bound Session remains in the browser Session list; a deleted Session creates a new binding target on the next start.
- Team spawn, mailbox, and task operations require Host `agentTeams` and refuse when the desktop seed omits experimental Agent Teams.
- The package publishes no `./invariant` because Loader composition tests and the DSH registries directly observe every owned registration.

<a id="dev-note"></a>
Expand Down
5 changes: 3 additions & 2 deletions packages-xyai/xyai-ai-employees/README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ kind: "package-reference"
name: '@xyai/dsh-ai-employees'
```

本包没有插件配置字段。它需要 Host 侧的 `connection`、`settings`、`agents` 和 `agentTeams` 服务;浏览器半需要插槽、语言、连接和 Session 控制器。XYAI Profile 提供这些依赖。
本包没有插件配置字段。它需要 Host 侧的 `connection`、`settings` 和 `agents` 服务;浏览器半需要插槽、语言、连接和 Session 控制器。队友拉起、邮箱和任务 RPC 需要 Host `agentTeams`,该服务缺失时以明确错误拒绝。XYAI Web Profile 提供 Agent Teams;桌面种子不提供。

-----

Expand All @@ -48,7 +48,7 @@ kind: "package-reference"
<details>
<summary>实现内部——点击展开</summary>

Host 把员工库、草稿版本和单聊绑定存入 `xyai-ai-team` 设置命名空间。Connection RPC 校验浏览器载荷,并把队友、邮箱和任务操作委托给 `ctx.agentTeams`。浏览器注册完整的对话 View、打开协作对话框的输入框入口,以及团队感知的改名控件。
Host 把员工库、草稿版本和单聊绑定存入 `xyai-ai-team` 设置命名空间。Connection RPC 校验浏览器载荷,并在存在 `agentTeams` 时把队友、邮箱和任务操作委托给它。浏览器注册完整的对话 View、打开协作对话框的输入框入口,以及团队感知的改名控件。

| 文件 | 用途 |
|---|---|
Expand Down Expand Up @@ -92,6 +92,7 @@ Host 把员工库、草稿版本和单聊绑定存入 `xyai-ai-team` 设置命
- 例行任务仍是员工配置;本包不注册调度器任务。
- 市场安装和线上员工同步需要所属的商业或线上适配器。
- 当绑定的 Session 仍在浏览器 Session 列表中时才复用单聊;删除 Session 后,下次启动会创建新的绑定目标。
- 队友拉起、邮箱和任务操作需要 Host `agentTeams`;桌面种子省略实验性 Agent Teams 时这些操作会被拒绝。
- 本包不发布 `./invariant`,因为 Loader 组合测试和 DSH 注册表直接观测每项自有注册。

<a id="dev-note"></a>
Expand Down
5 changes: 5 additions & 0 deletions packages-xyai/xyai-ai-employees/src/client/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,4 +233,9 @@ export function apply(ctx: Context): void {
ctx.slots.inject('conversation.view', () => ctx.slots.register({ name: 'conversation.view', id: 'xyai-ai-team', order: 35, locale: NS, label: () => ctx.locale.bind(NS)('view.title'), inject: shared }, function AiTeamView(props) { return <CollaborationSurface {...props} embedded sessionId={props.sessionId as string}/> }))
ctx.slots.inject('conversation.input.left', () => ctx.slots.register({ name: 'conversation.input.left', id: 'xyai-ai-employee-picker', order: 10, locale: NS }, function PickerChip(props) { return <button type="button" className="xyt-chip" onClick={() => window.dispatchEvent(new CustomEvent('xyai:open-ai-collaboration'))}>{props.t('employee.pick')}</button> }))
ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({ name: 'conversation.session.header.utilities', id: 'xyai-ai-team-rename', order: 15, locale: NS, inject: shared }, RenameGroup))
ctx.effect(() => {
if (typeof document === 'undefined') return () => {}
document.documentElement.setAttribute('data-xyai-surface-employees', '')
return () => document.documentElement.removeAttribute('data-xyai-surface-employees')
}, 'xyai-ai-employees: surface flag')
}
41 changes: 32 additions & 9 deletions packages-xyai/xyai-ai-employees/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ const MAX_DOCUMENT_BYTES = 1_000_000
const MAX_EMPLOYEES_PER_TEAM = 6
const MAX_OUTCOMES = 200

/** Thrown when team spawn/send/task RPC runs without a Host Agent Teams service. */
export const AGENT_TEAMS_UNAVAILABLE = 'Agent Teams is not available on this Host'

const defaults = (instructions: string, knowledge: string[], tools: string[]): EmployeeSettings => ({
instructions,
memory: '',
Expand Down Expand Up @@ -140,11 +143,29 @@ export class AiTeamBackend {
private readonly section: SettingsScope<AiTeamSection>
private writes: Promise<void> = Promise.resolve()

/** @param ctx - Host context providing settings, live agents, and Agent Teams. */
/** @param ctx - Host context providing settings, live agents, and optional Agent Teams. */
constructor(private readonly ctx: Context) {
this.section = ctx.settings.register('xyai-ai-team', sectionSchema)
}

private agentTeams() {
let teams: Context['agentTeams'] | undefined
if (typeof this.ctx.get === 'function') {
teams = this.ctx.get('agentTeams') as Context['agentTeams'] | undefined
}
if (teams === undefined) {
try {
teams = this.ctx.agentTeams
} catch {
// Host did not provide Agent Teams; employee library RPC still runs.
}
}
if (teams === undefined || typeof teams.listMembers !== 'function') {
throw new Error(AGENT_TEAMS_UNAVAILABLE)
}
return teams
}

/** Dispatch one validated browser request. */
async dispatch(endpoint: string, payload: unknown): Promise<ConnectionRpcResult<AiTeamAnswer>> {
try {
Expand Down Expand Up @@ -245,7 +266,8 @@ export class AiTeamBackend {

private teamView(payload: unknown) {
const agent = this.agent(payload)
return { members: this.ctx.agentTeams.listMembers(agent), tasks: this.ctx.agentTeams.listTasks(agent) }
const teams = this.agentTeams()
return { members: teams.listMembers(agent), tasks: teams.listTasks(agent) }
}

private async start(payload: unknown): Promise<TeamStartResult> {
Expand All @@ -256,7 +278,8 @@ export class AiTeamBackend {
const context = fields.context === 'fork' ? 'fork' : 'fresh'
const library = this.library()
const byId = new Map(library.employees.map(employee => [employee.id, employee]))
const existingNames = new Set(this.ctx.agentTeams.listMembers(agent).map(member => member.name))
const teams = this.agentTeams()
const existingNames = new Set(teams.listMembers(agent).map(member => member.name))
const result: TeamStartResult = { started: [], existing: [], failed: [] }
for (const id of ids) {
const employee = byId.get(id)
Expand All @@ -265,7 +288,7 @@ export class AiTeamBackend {
const settings = employee.published
const prompt = [settings.instructions, settings.memory && `项目记忆:\n${settings.memory}`, settings.skills.length > 0 && `优先技能:${settings.skills.join('、')}`, settings.routines.length > 0 && `例行职责:${settings.routines.join(';')}`, settings.integrations.length > 0 && `可用集成:${settings.integrations.join('、')}`].filter(Boolean).join('\n\n')
try {
await this.ctx.agentTeams.spawnTeammate(agent, { name: employee.id, description: employee.description, prompt: [{ type: 'text', text: prompt }], context, provider: context === 'fork' ? 'fork' : 'spawn', signal: AbortSignal.timeout(60_000) })
await teams.spawnTeammate(agent, { name: employee.id, description: employee.description, prompt: [{ type: 'text', text: prompt }], context, provider: context === 'fork' ? 'fork' : 'spawn', signal: AbortSignal.timeout(60_000) })
result.started.push(id); existingNames.add(employee.id)
} catch (error) { result.failed.push({ employeeId: id, message: error instanceof Error ? error.message : String(error) }) }
}
Expand All @@ -274,12 +297,12 @@ export class AiTeamBackend {

private async send(payload: unknown) {
const fields = fieldsOf(payload)
return await this.ctx.agentTeams.sendMessage(this.agent(payload), { target: requiredString(fields, 'target', 80), content: [{ type: 'text', text: requiredString(fields, 'message', 65_000) }], signal: AbortSignal.timeout(30_000) })
return await this.agentTeams().sendMessage(this.agent(payload), { target: requiredString(fields, 'target', 80), content: [{ type: 'text', text: requiredString(fields, 'message', 65_000) }], signal: AbortSignal.timeout(30_000) })
}

private async createTask(payload: unknown) {
const fields = fieldsOf(payload)
return await this.ctx.agentTeams.createTask(this.agent(payload), { subject: requiredString(fields, 'subject', 200), description: requiredString(fields, 'description', 4_000), writeScopes: strings(fields.writeScopes ?? [], 'writeScopes') })
return await this.agentTeams().createTask(this.agent(payload), { subject: requiredString(fields, 'subject', 200), description: requiredString(fields, 'description', 4_000), writeScopes: strings(fields.writeScopes ?? [], 'writeScopes') })
}

private async updateTask(payload: unknown) {
Expand All @@ -293,7 +316,7 @@ export class AiTeamBackend {
action: action as typeof allowed[number],
...(action === 'edit' ? { subject: requiredString(fields, 'subject', 200), description: requiredString(fields, 'description', 4_000) } : {}),
}
return await this.ctx.agentTeams.updateTask(this.agent(payload), request)
return await this.agentTeams().updateTask(this.agent(payload), request)
}

private outcomes(payload: unknown): TeamOutcome[] {
Expand Down Expand Up @@ -341,8 +364,8 @@ export class AiTeamBackend {
}
}

/** Services required for employee persistence and executable team operations. */
export const inject = ['connection', 'settings', 'agents', 'agentTeams']
/** Services required for employee persistence. Agent Teams is optional at load. */
export const inject = ['connection', 'settings', 'agents']

/** Register the AI Team RPC channel on the plugin lifetime. */
export function apply(ctx: Context): void {
Expand Down
Loading
Loading