diff --git a/apps/web/index.html b/apps/web/index.html index 1ce5ff35ff..75c4e5bac6 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -5,7 +5,7 @@ - DeepSeek Harness + DSH Local Build
diff --git a/apps/web/tests/built-boot.snapshot.ts b/apps/web/tests/built-boot.snapshot.ts index 2b5cb35886..0e48e08ec0 100644 --- a/apps/web/tests/built-boot.snapshot.ts +++ b/apps/web/tests/built-boot.snapshot.ts @@ -42,6 +42,8 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn // The sidebar renders from the boot graph: every inject layer activated. const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 }) + expect(document.querySelector('svg[viewBox="26 0 156 24"]')).not.toBeNull() + expect(screen.queryByText('DSH Local Build')).toBeNull() // The compact layout dropped group session counts; the fixture workspace // group row renders immediately with its sessions beneath it. const fixtureGroup = (await within(tree).findAllByText('fixture')) diff --git a/apps/web/tests/smoke-real.e2e.ts b/apps/web/tests/smoke-real.e2e.ts index 2f3df14071..3ac6706a6f 100644 --- a/apps/web/tests/smoke-real.e2e.ts +++ b/apps/web/tests/smoke-real.e2e.ts @@ -541,6 +541,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke await connectFreshWorkspace(page, sessionsDir) const input = page.locator('textarea').first() await input.waitFor({ timeout: 10_000 }) + const productTitle = await page.title() await screen(page, '02-empty-state') const prompt = `Please answer this request carefully: explain event sourcing in two sentences, ending with exactly ${ROUND_DONE_MARKER}.` await input.fill(prompt) @@ -550,8 +551,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke await page.waitForFunction(() => document.body.innerText.length > 50, undefined, { timeout: 15_000 }) expect(pageErrors).toEqual([]) await page.waitForFunction( - () => document.title !== 'DeepSeek Harness' && document.title.endsWith(' — DeepSeek Harness'), - undefined, + expected => document.title !== expected && document.title.endsWith(` — ${expected}`), + productTitle, { timeout: 15_000 }, ) await expect.poll(async () => (await rpc<{ items: { sessionId: string }[] }>(baseUrl, 'session.list', {})).items.length, { @@ -562,8 +563,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke if (sessionId === undefined) throw new Error('created Web session was not listed') const durableTitle = await waitForProviderTitle(baseUrl, sessionId) await page.waitForFunction( - expected => document.title === `${expected} — DeepSeek Harness`, - durableTitle, + ({ expected, product }) => document.title === `${expected} — ${product}`, + { expected: durableTitle, product: productTitle }, { timeout: 15_000 }, ) const sessionTree = page.getByRole('tree', { name: 'Sessions' }) diff --git a/apps/web/tests/startup-auto-selection.e2e.ts b/apps/web/tests/startup-auto-selection.e2e.ts index 23771924bc..b39e720600 100644 --- a/apps/web/tests/startup-auto-selection.e2e.ts +++ b/apps/web/tests/startup-auto-selection.e2e.ts @@ -69,8 +69,8 @@ describe('web e2e: startup auto-selection', () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-first-workspace-stable-tree')) await page.locator(`${ROOT_PHASE}[data-phase="hero"]`).waitFor({ timeout: 15_000 }) const headline = page.getByText('Into the Unknown', { exact: true }) - const fish = headline.locator('xpath=preceding-sibling::span[1]/*[name()="svg"]') - const fishHitbox = fish.locator('..') + const fishHitbox = headline.locator('xpath=preceding-sibling::span[1]') + const fish = fishHitbox.locator('svg') expect(await fish.evaluate(node => getComputedStyle(node).color)) .toBe(await headline.evaluate(node => getComputedStyle(node).color)) await fishHitbox.hover() diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 5e57eee177..cd27136cb7 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -8,6 +8,23 @@ const src = (rel: string): string => fileURLToPath(new URL(rel, import.meta.url) const STANDALONE_ERROR = 'apps/web is not a standalone application: bare Vite cannot inject window.__DSH_BOOT__. ' + 'From a repository checkout, run `pnpm dsh web`; an installed package uses `dsh web`. ' + 'For client-plugin HMR, run `pnpm dsh web` together with `pnpm run dev:web`.' +const DEFAULT_CLIENT_TITLE = 'DSH Local Build' + +/** Escape build-time text before placing it in the HTML title element. */ +function escapeHtmlText(value: string): string { + return value.replace(/&/g, '&').replace(//g, '>') +} + +/** Project the public build title into the initial HTML document. */ +function clientDocumentTitle(): Plugin { + const title = escapeHtmlText(process.env.DSH_CLIENT_TITLE ?? DEFAULT_CLIENT_TITLE) + return { + name: 'dsh-client-document-title', + transformIndexHtml(html) { + return html.replace('DSH Local Build', `${title}`) + }, + } +} /** Fail before a Vite dev or preview server can expose the boot-manifest-free shell. */ function rejectStandaloneServe(): Plugin { @@ -91,7 +108,7 @@ function npmPackageOf(id: string): string | undefined { } export default defineConfig({ - plugins: [rejectStandaloneServe(), react()], + plugins: [rejectStandaloneServe(), clientDocumentTitle(), react()], build: { sourcemap: true, rollupOptions: { diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 3194d54998..9243513ecb 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -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 docs/config-catalog.md -config-catalog.md: b865806bcc4d3e494a0ebf2a9331928991f9e6d1 -config-catalog.zh.md: 6e4ce4cc4d3bfa856987c0d698fddc1c8c7712a9 +config-catalog.md: d9c4eff5a206ac17b54957d5364fb7b3d175d97c +config-catalog.zh.md: 010878c582eec6263ea604c880a15407ebd777b6 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index b865806bcc..d9c4eff5a2 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -3214,6 +3214,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@deepseek-ai/dsh-client-runtime` ([`packages/client/runtime/src/index.ts`](../packages/client/runtime/src/index.ts)) - `@deepseek-ai/dsh-client-ui-agent-preset` ([`packages/client/ui-agent-preset/src/index.ts`](../packages/client/ui-agent-preset/src/index.ts)) - `@deepseek-ai/dsh-client-ui-attachment` ([`packages/client/ui-attachment/src/index.ts`](../packages/client/ui-attachment/src/index.ts)) +- `@deepseek-ai/dsh-client-ui-brand-official` ([`packages/client/ui-brand-official/src/index.ts`](../packages/client/ui-brand-official/src/index.ts)) - `@deepseek-ai/dsh-client-ui-commands` ([`packages/client/ui-commands/src/index.ts`](../packages/client/ui-commands/src/index.ts)) - `@deepseek-ai/dsh-client-ui-conversation` ([`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts)) - `@deepseek-ai/dsh-client-ui-cordis` ([`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts)) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 6e4ce4cc4d..010878c582 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -3218,6 +3218,7 @@ export interface Config { - `@deepseek-ai/dsh-client-runtime`([`packages/client/runtime/src/index.ts`](../packages/client/runtime/src/index.ts)) - `@deepseek-ai/dsh-client-ui-agent-preset`([`packages/client/ui-agent-preset/src/index.ts`](../packages/client/ui-agent-preset/src/index.ts)) - `@deepseek-ai/dsh-client-ui-attachment`([`packages/client/ui-attachment/src/index.ts`](../packages/client/ui-attachment/src/index.ts)) +- `@deepseek-ai/dsh-client-ui-brand-official`([`packages/client/ui-brand-official/src/index.ts`](../packages/client/ui-brand-official/src/index.ts)) - `@deepseek-ai/dsh-client-ui-commands`([`packages/client/ui-commands/src/index.ts`](../packages/client/ui-commands/src/index.ts)) - `@deepseek-ai/dsh-client-ui-conversation`([`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts)) - `@deepseek-ai/dsh-client-ui-cordis`([`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts)) diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index 9ef34df65a..83709a6f2b 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -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 docs/module-graph.md -module-graph.md: ac5faf74ca156b859859c08d0c9872c332b7fd1c -module-graph.zh.md: 71de2ac3d566bec5f878619ca68ea779ddabc387 +module-graph.md: 69d751f69ee254f4c2842216179f0072b208a49d +module-graph.zh.md: c93c95aae022d040498025530304344e001de2f0 diff --git a/docs/module-graph.md b/docs/module-graph.md index ac5faf74ca..69d751f69e 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -125,6 +125,7 @@ flowchart TD pkg_client_runtime["client-runtime"] pkg_client_ui_agent_preset["client-ui-agent-preset"] pkg_client_ui_attachment["client-ui-attachment"] + pkg_client_ui_brand_official["client-ui-brand-official"] pkg_client_ui_commands["client-ui-commands"] pkg_client_ui_conversation["client-ui-conversation"] pkg_client_ui_deliverables["client-ui-deliverables"] @@ -1306,6 +1307,10 @@ flowchart TD pkg_client_ui_attachment --> pkg_client_runtime pkg_client_ui_attachment --> pkg_client_ui_conversation pkg_client_ui_attachment --> pkg_invariants + pkg_client_ui_brand_official --> pkg_client_runtime + pkg_client_ui_brand_official --> pkg_client_ui_conversation + pkg_client_ui_brand_official --> pkg_client_ui_sidebar + pkg_client_ui_brand_official --> pkg_invariants pkg_client_ui_commands --> pkg_api_remotes pkg_client_ui_commands --> pkg_client_locale pkg_client_ui_commands --> pkg_client_runtime @@ -1652,6 +1657,7 @@ flowchart TD | [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-layout`](../packages/client/ui-layout), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-attachment`](../packages/client/ui-attachment) | `client` | [`attachment`](../packages/attachment/attachment), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`client-ui-brand-official`](../packages/client/ui-brand-official) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-commands`](../packages/client/ui-commands) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-deliverables`](../packages/client/ui-deliverables) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`invariants`](../packages/runtime-diagnostics/invariants), [`system-prompt`](../packages/core/system-prompt) | | [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 71de2ac3d5..c93c95aae0 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -127,6 +127,7 @@ flowchart TD pkg_client_runtime["client-runtime"] pkg_client_ui_agent_preset["client-ui-agent-preset"] pkg_client_ui_attachment["client-ui-attachment"] + pkg_client_ui_brand_official["client-ui-brand-official"] pkg_client_ui_commands["client-ui-commands"] pkg_client_ui_conversation["client-ui-conversation"] pkg_client_ui_deliverables["client-ui-deliverables"] @@ -1308,6 +1309,10 @@ flowchart TD pkg_client_ui_attachment --> pkg_client_runtime pkg_client_ui_attachment --> pkg_client_ui_conversation pkg_client_ui_attachment --> pkg_invariants + pkg_client_ui_brand_official --> pkg_client_runtime + pkg_client_ui_brand_official --> pkg_client_ui_conversation + pkg_client_ui_brand_official --> pkg_client_ui_sidebar + pkg_client_ui_brand_official --> pkg_invariants pkg_client_ui_commands --> pkg_api_remotes pkg_client_ui_commands --> pkg_client_locale pkg_client_ui_commands --> pkg_client_runtime @@ -1654,6 +1659,7 @@ flowchart TD | [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-layout`](../packages/client/ui-layout), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-attachment`](../packages/client/ui-attachment) | `client` | [`attachment`](../packages/attachment/attachment), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`client-ui-brand-official`](../packages/client/ui-brand-official) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-commands`](../packages/client/ui-commands) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-deliverables`](../packages/client/ui-deliverables) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`invariants`](../packages/runtime-diagnostics/invariants), [`system-prompt`](../packages/core/system-prompt) | | [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index 03d04782bb..61151bdc65 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -209,6 +209,10 @@ - id: ui-conversation name: '@deepseek-ai/dsh-client-ui-conversation' + # Official occupants for the generic sidebar and conversation brand slots. + - id: ui-brand-official + name: '@deepseek-ai/dsh-client-ui-brand-official' + - id: ui-attachment name: '@deepseek-ai/dsh-client-ui-attachment' diff --git a/packages/bundle/web-app/package.json b/packages/bundle/web-app/package.json index 1f6ede4009..c753882a98 100644 --- a/packages/bundle/web-app/package.json +++ b/packages/bundle/web-app/package.json @@ -55,6 +55,7 @@ "@deepseek-ai/dsh-client-runtime": "workspace:^", "@deepseek-ai/dsh-client-ui-agent-preset": "workspace:^", "@deepseek-ai/dsh-client-ui-attachment": "workspace:^", + "@deepseek-ai/dsh-client-ui-brand-official": "workspace:^", "@deepseek-ai/dsh-client-ui-commands": "workspace:^", "@deepseek-ai/dsh-client-ui-conversation": "workspace:^", "@deepseek-ai/dsh-client-ui-cordis": "workspace:^", diff --git a/packages/client/README.i18n.yaml b/packages/client/README.i18n.yaml index 3928f30f1b..6036e9160e 100644 --- a/packages/client/README.i18n.yaml +++ b/packages/client/README.i18n.yaml @@ -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/client/README.md -README.md: fe57d58a0f4fa1c9bff2699ffb363c80197fc5ed -README.zh.md: c721710c9f20ba10f20392c207e1de4169ec0e12 +README.md: b18aad486cd7d3fafe8261fee61fa9e26a7feaa6 +README.zh.md: e840493b0135cfa4f8e294efe8c612471a94e026 diff --git a/packages/client/README.md b/packages/client/README.md index fe57d58a0f..b18aad486c 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -20,6 +20,7 @@ The browser side of the dsh web GUI: shell boot, browser-host communication, sha | [`ui-attachment/`](ui-attachment/README.md) | Registers composer and message-image attachment presentation. | | [`ui-layout/`](ui-layout/README.md) | Arranges the main application regions. | | [`ui-sidebar/`](ui-sidebar/README.md) | Presents workspace and session navigation. | +| [`ui-brand-official/`](ui-brand-official/README.md) | Fills the generic browser-brand slots with the official name and marks. | | [`ui-workspace/`](ui-workspace/README.md) | Provides workspace selection and creation surfaces. | | [`ui-conversation/`](ui-conversation/README.md) | Presents the active conversation and its input surface. | | [`ui-tool/`](ui-tool/README.md) | Composes Tool call trees and keyed per-Tool views. | diff --git a/packages/client/README.zh.md b/packages/client/README.zh.md index c721710c9f..e840493b01 100644 --- a/packages/client/README.zh.md +++ b/packages/client/README.zh.md @@ -20,6 +20,7 @@ dsh web GUI 的浏览器侧:shell 启动、浏览器与宿主通信、共享 U | [`ui-attachment/`](ui-attachment/README.md) | 注册输入框与消息图片的附件呈现。 | | [`ui-layout/`](ui-layout/README.md) | 排列应用的主要区域。 | | [`ui-sidebar/`](ui-sidebar/README.md) | 展示工作区与会话导航。 | +| [`ui-brand-official/`](ui-brand-official/README.md) | 使用官方名称和标记填充通用浏览器品牌 slot。 | | [`ui-workspace/`](ui-workspace/README.md) | 提供工作区选择与创建界面。 | | [`ui-conversation/`](ui-conversation/README.md) | 展示当前对话及其输入界面。 | | [`ui-tool/`](ui-tool/README.md) | 编排工具调用树和按工具键控的视图。 | diff --git a/packages/client/ui-brand-official/README.i18n.yaml b/packages/client/ui-brand-official/README.i18n.yaml new file mode 100644 index 0000000000..9d438ad955 --- /dev/null +++ b/packages/client/ui-brand-official/README.i18n.yaml @@ -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 packages/client/ui-brand-official/README.md +README.md: 7197bc7f4731cd3e6471549900e4868ad6d4796d +README.zh.md: 14d3ee3f1aa2f12ddd20b851d4034e1417baf5c9 diff --git a/packages/client/ui-brand-official/README.md b/packages/client/ui-brand-official/README.md new file mode 100644 index 0000000000..7197bc7f47 --- /dev/null +++ b/packages/client/ui-brand-official/README.md @@ -0,0 +1,20 @@ +# @deepseek-ai/dsh-client-ui-brand-official + +English | [中文](README.zh.md) + +This package fills `sidebar.brand.mark`, `sidebar.brand.name`, and `conversation.hero.brand.mark` only when `DSH_CLIENT_BUILD_PROFILE` is `official`. Other builds load the plugin but register no occupants, leaving the shell fallbacks visible. + +The three occupants install as one declaration-aware registration set through nested `slots.inject()` calls. The package therefore works whether its row activates before or after the sidebar and conversation declarers, withdraws all occupants when either declaration collapses, and leaves no partial brand mix during HMR. It retains no runtime state. The node half is an empty Loader seat, and the browser title remains a build-environment concern outside this package. + +## Model Experience + +None, as the package contributes browser presentation only; nothing here reaches a model request. + +#### KV Cache effect + +None; this package neither assembles nor sends a provider request. + +## Known Limitations and Deferred Work + +- **The package supplies one occupant set** — alternative presentation belongs in another Cordis package occupying the same slots. +- **The browser title is independent** — `DSH_CLIENT_TITLE` selects title text at build time rather than through a UI slot. diff --git a/packages/client/ui-brand-official/README.zh.md b/packages/client/ui-brand-official/README.zh.md new file mode 100644 index 0000000000..14d3ee3f1a --- /dev/null +++ b/packages/client/ui-brand-official/README.zh.md @@ -0,0 +1,20 @@ +# @deepseek-ai/dsh-client-ui-brand-official + +[English](README.md) | 中文 + +仅当 `DSH_CLIENT_BUILD_PROFILE` 为 `official` 时,本包才填充 `sidebar.brand.mark`、`sidebar.brand.name` 和 `conversation.hero.brand.mark`。其他构建仍会加载插件,但不注册 occupant,因此显示 shell fallback。 + +三个占位者通过嵌套的 `slots.inject()` 作为一组声明感知注册安装。因此无论该包的条目先于还是后于侧边栏和会话声明方激活,它都能工作;任一声明折叠时会撤回全部占位者,HMR 期间不会留下混合品牌。它不保留运行时状态。node 半边是空的 Loader seat;浏览器标题仍属于本包之外的构建环境事项。 + +## 模型体验 + +无,因为本包只贡献浏览器呈现;这里没有任何内容进入模型请求。 + +#### KV Cache 影响 + +无;本包既不组装也不发送 provider 请求。 + +## 已知限制与暂缓事项 + +- **本包只提供一组 occupant** —— 其他呈现应由占用相同 slot 的另一个 Cordis 包提供。 +- **浏览器标题相互独立** —— `DSH_CLIENT_TITLE` 在构建期选择标题文字,而不经过 UI slot。 diff --git a/packages/client/ui-brand-official/package.json b/packages/client/ui-brand-official/package.json new file mode 100644 index 0000000000..25c1c93cab --- /dev/null +++ b/packages/client/ui-brand-official/package.json @@ -0,0 +1,72 @@ +{ + "name": "@deepseek-ai/dsh-client-ui-brand-official", + "description": "Official DeepSeek Harness brand occupants for the Web client's sidebar and conversation Hero slots", + "version": "0.1.0-rc.7", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/client/ui-brand-official" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, + "./client": { + "types": "./lib/types/client/index.d.ts", + "default": "./lib/client.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "dsh": { + "client": { + "inject": [ + "@deepseek-ai/dsh-client-runtime", + "@deepseek-ai/dsh-client-ui-conversation", + "@deepseek-ai/dsh-client-ui-sidebar" + ], + "platform": "web" + } + }, + "scripts": { + "bundle": "tsdown", + "watch": "tsdown --watch" + }, + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/dsh-client-runtime": "workspace:^", + "@deepseek-ai/dsh-client-ui-conversation": "workspace:^", + "@deepseek-ai/dsh-client-ui-sidebar": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/cordis": "workspace:^" + }, + "devDependencies": { + "@deepseek-ai/dsh-client-runtime": "workspace:^", + "@deepseek-ai/dsh-client-ui-conversation": "workspace:^", + "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", + "@deepseek-ai/dsh-client-ui-sidebar": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/cordis": "workspace:^", + "@testing-library/react": "^16.1.0", + "@types/react": "~18.3.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "files": [ + "lib/index.js", + "lib/invariant.js", + "lib/client.js", + "lib/types/**/*.d.ts" + ] +} diff --git a/packages/client/ui-brand-official/src/client/Brand.tsx b/packages/client/ui-brand-official/src/client/Brand.tsx new file mode 100644 index 0000000000..4e0a60fd26 --- /dev/null +++ b/packages/client/ui-brand-official/src/client/Brand.tsx @@ -0,0 +1,22 @@ +import { BrandWordmark, FishLogo } from '@deepseek-ai/dsh-client-ui-primitives' +import type { HeroBrandMarkOwnerProps } from '@deepseek-ai/dsh-client-ui-conversation/client' +import type { SidebarBrandMarkOwnerProps } from '@deepseek-ai/dsh-client-ui-sidebar/client' + +type OfficialBrandMarkProps = HeroBrandMarkOwnerProps & SidebarBrandMarkOwnerProps + +/** + * Render the official mark with the presentation requested by its host surface. + * @param props - Host-supplied mark presentation. + * @returns the official whale mark. + */ +export function OfficialBrandMark({ size, className }: OfficialBrandMarkProps) { + return +} + +/** + * Render the official name artwork without its independently slotted mark. + * @returns the official name wordmark. + */ +export function OfficialBrandName() { + return +} diff --git a/packages/client/ui-brand-official/src/client/index.ts b/packages/client/ui-brand-official/src/client/index.ts new file mode 100644 index 0000000000..b291bda3e7 --- /dev/null +++ b/packages/client/ui-brand-official/src/client/index.ts @@ -0,0 +1,23 @@ +/** Official DeepSeek Harness occupants for the generic browser-brand slots. */ +import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client' +import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' +import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client' +import { OfficialBrandMark, OfficialBrandName } from './Brand.tsx' + +/** Required service: the UI slot registry. */ +export const inject = ['slots'] + +/** + * Fill every shipped brand slot as one declaration-aware registration set. + * @param ctx - Client root context. + */ +export function apply(ctx: ClientContext): void { + if (process.env.DSH_CLIENT_BUILD_PROFILE !== 'official') return + ctx.slots.inject('sidebar.brand.mark', () => + ctx.slots.inject('sidebar.brand.name', () => + ctx.slots.inject('conversation.hero.brand.mark', function* () { + yield ctx.slots.register({ name: 'sidebar.brand.mark' }, OfficialBrandMark) + yield ctx.slots.register({ name: 'sidebar.brand.name' }, OfficialBrandName) + yield ctx.slots.register({ name: 'conversation.hero.brand.mark' }, OfficialBrandMark) + }))) +} diff --git a/packages/client/ui-brand-official/src/index.ts b/packages/client/ui-brand-official/src/index.ts new file mode 100644 index 0000000000..df38f3cfa5 --- /dev/null +++ b/packages/client/ui-brand-official/src/index.ts @@ -0,0 +1,7 @@ +/** + * Official browser-brand plugin, node half. The empty apply gives Loader a + * host-side row while the browser half ships through `exports["./client"]`. + */ + +/** Host plugin body — this package contributes browser presentation only. */ +export function apply(): void {} diff --git a/packages/client/ui-brand-official/src/invariant.ts b/packages/client/ui-brand-official/src/invariant.ts new file mode 100644 index 0000000000..574054c383 --- /dev/null +++ b/packages/client/ui-brand-official/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-brand-official`. + * @module @deepseek-ai/dsh-client-ui-brand-official/invariant + */ + +/* jscpd:ignore-start */ +import type { Context } from '@deepseek-ai/cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-client-ui-brand-official' + +/** Cordis companion plugin name. */ +export const name = 'client-ui-brand-official-invariant' +/** Service required before the companion can reserve package ownership. */ +export const inject = ['invariants'] + +/** + * No runtime invariant: the package retains no mutable state, and its three + * slot occupants install and leave through one transactional effect. + */ +const install: InvariantInstaller = () => {} + +/** + * Register this package's invariant companion. + * @param ctx - Cordis context carrying the invariant service. + * @returns the installed registration's disposer after setup succeeds. + */ +export const apply = (ctx: Context): Promise<() => void> => + Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) +/* jscpd:ignore-end */ diff --git a/packages/client/ui-brand-official/tests/browser-plugin.client.spec.tsx b/packages/client/ui-brand-official/tests/browser-plugin.client.spec.tsx new file mode 100644 index 0000000000..ee5275b9b7 --- /dev/null +++ b/packages/client/ui-brand-official/tests/browser-plugin.client.spec.tsx @@ -0,0 +1,79 @@ +// @vitest-environment jsdom +import { Context } from '@deepseek-ai/cordis' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { cleanup, render } from '@testing-library/react' +import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client' +import { apply, inject } from '../src/client/index.ts' +import { OfficialBrandMark, OfficialBrandName } from '../src/client/Brand.tsx' + +afterEach(() => { + cleanup() + vi.unstubAllEnvs() +}) + +const HOLES = [ + 'sidebar.brand.mark', + 'sidebar.brand.name', + 'conversation.hero.brand.mark', +] as const + +async function bench(declare = true) { + const ctx = new Context() + await ctx.plugin(SlotRegistry).await() + const slots = ctx.get('slots') as SlotRegistry + const declareHoles = () => slots.register({ + name: 'root', + children: Object.fromEntries(HOLES.map(name => [name, { kind: 'single', scope: 'root' }])), + } as never, () => null) + const disposeHoles = declare ? declareHoles() : undefined + return { ctx, slots, declareHoles, disposeHoles } +} + +describe('official browser-brand plugin', () => { + it('declares only the slot service it uses', () => { + expect(inject).toEqual(['slots']) + }) + + it('leaves every slot empty outside the official build profile', async () => { + vi.stubEnv('DSH_CLIENT_BUILD_PROFILE', 'local') + const subject = await bench() + await subject.ctx.plugin({ inject: [...inject], apply }).await() + for (const hole of HOLES) expect(subject.slots.entries(hole)).toHaveLength(0) + }) + + it('fills declarations before or after apply and removes every occupant on teardown', async () => { + vi.stubEnv('DSH_CLIENT_BUILD_PROFILE', 'official') + const before = await bench() + const fiber = before.ctx.plugin({ inject: [...inject], apply }) + await fiber.await() + for (const hole of HOLES) expect(before.slots.entries(hole)).toHaveLength(1) + + before.disposeHoles?.() + for (const hole of HOLES) expect(before.slots.entries(hole)).toHaveLength(0) + before.declareHoles() + await Promise.resolve() + for (const hole of HOLES) expect(before.slots.entries(hole)).toHaveLength(1) + + await fiber.dispose() + for (const hole of HOLES) expect(before.slots.entries(hole)).toHaveLength(0) + + const after = await bench(false) + await after.ctx.plugin({ inject: [...inject], apply }).await() + for (const hole of HOLES) expect(after.slots.entries(hole)).toHaveLength(0) + after.declareHoles() + await Promise.resolve() + for (const hole of HOLES) expect(after.slots.entries(hole)).toHaveLength(1) + }) + + it('renders the official name independently from both requested mark sizes', () => { + const name = render() + expect(name.container.querySelector('svg')?.getAttribute('viewBox')).toBe('26 0 156 24') + name.unmount() + + const mark = render() + expect(mark.container.querySelector('svg')?.getAttribute('width')).toBe('34') + expect(mark.container.querySelector('svg')?.getAttribute('class')).toBe('hero-mark') + mark.rerender() + expect(mark.container.querySelector('svg')?.getAttribute('width')).toBe('24') + }) +}) diff --git a/packages/client/ui-brand-official/tests/invariant.client.spec.ts b/packages/client/ui-brand-official/tests/invariant.client.spec.ts new file mode 100644 index 0000000000..d8d3dd2d95 --- /dev/null +++ b/packages/client/ui-brand-official/tests/invariant.client.spec.ts @@ -0,0 +1,18 @@ +import { Context } from '@deepseek-ai/cordis' +import InvariantRegistry from '@deepseek-ai/dsh-invariants' +import { describe, expect, it } from 'vitest' +import * as BrandInvariant from '../src/invariant.ts' +import { apply as nodeApply } from '../src/index.ts' + +describe('official brand invariant companion', () => { + it('reserves package ownership with an empty installer', async () => { + const ctx = new Context() + await ctx.plugin(InvariantRegistry, { enabled: true }) + + await expect(ctx.plugin(BrandInvariant).await()).resolves.toBeDefined() + }) + + it('keeps the node half as an inert Loader seat', () => { + expect(() => { nodeApply() }).not.toThrow() + }) +}) diff --git a/packages/client/ui-brand-official/tsconfig.json b/packages/client/ui-brand-official/tsconfig.json new file mode 100644 index 0000000000..f98c0a8b2f --- /dev/null +++ b/packages/client/ui-brand-official/tsconfig.json @@ -0,0 +1,30 @@ +{ + "extends": "../../../tsconfig.base.client.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../runtime-diagnostics/invariants" + }, + { + "path": "../runtime" + }, + { + "path": "../ui-conversation" + }, + { + "path": "../ui-primitives" + }, + { + "path": "../ui-sidebar" + }, + { + "path": "../ui-slots" + } + ] +} diff --git a/packages/client/ui-brand-official/tsdown.config.ts b/packages/client/ui-brand-official/tsdown.config.ts new file mode 100644 index 0000000000..abc830c1f6 --- /dev/null +++ b/packages/client/ui-brand-official/tsdown.config.ts @@ -0,0 +1,3 @@ +import { clientBundle } from '../tsdown.client.ts' + +export default clientBundle('@deepseek-ai/dsh-client-ui-brand-official', ['lib/types/index.js', 'lib/types/invariant.js']) diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index 216c60587a..8131af3495 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -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/client/ui-conversation/README.md -README.md: cbd57ba05a96da36ac9198817e78594c1a2cefff -README.zh.md: cbe13efd43abda020edae41a0adee09d991314fe +README.md: dae46f4398dcbb6dcd50c0d7bdfac13be14d5dc2 +README.zh.md: 0b7af0b42eee060f7c827cdc9e06bc07b7bcc4ef diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index cbd57ba05a..dae46f4398 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -6,7 +6,7 @@ Conversation domain: skeleton (header/tabs/composer/empty state), chat view (gro Compaction renders as one collapsed row at the checkpoint's flow position without replacing the transcript above it. Automatic compaction uses the context-compacted title. Every completed marker with a loaded `compaction/summary` event shows the replaced-item and estimated-token counts and discloses the summary on click. Manual `/compact` starts as a running `compact` row; on successful settlement its explicit summary-event reference folds that command into the checkpoint row under the same React key. A completed checkpoint keeps the context-compaction icon at rest and replaces it with the collapsed or expanded disclosure only on hover or keyboard focus. Input rejection, no compactable history, cancellation, and failure retain the generic command row and its handler-authored text. Pairing never depends on adjacency because durable context may be injected while compaction is running. The framed checkpoint payload is model-facing and never renders; when the cited `compaction/summary` event is outside the loaded window, the checkpoint remains visible but non-expandable. -The resident conversation shell survives no-session and session transitions. Without a current session it locks message actions and presents the whole dashed composer card as a trigger for the root-scoped `conversation.hero.workspace` Workspace picker; the textarea remains read-only and keyboard-accessible. Selecting a Workspace connects or reuses its Host-owned blank session and opens that session without replacing the shell. The root always owns the same scrollport and Hero/composer subtree; separate strict-session header and body outlets fill their regions when the first Session arrives, so the Workspace picker, scroll body, composer seat, and textarea retain their React and DOM identity. Blank sessions render the same composer body as active sessions, while the InputHub carries drafts across Workspace switches and mirrors them into the session store. In the active phase the session header shows only the current session title and view tabs as ordinary column chrome; fork lineage remains session data and is not projected into the header. Beneath it the scrollport (`data-conversation-scroll`) holds the flowing views and the sticky composer stack (stats dock + input docks + bar). That scrollport reserves its scrollbar gutter unconditionally, and a view opting into a composer overlay leaves it a scroll container, so the input card keeps one horizontal position whether or not the transcript scrolls and whichever view tab is shown ([decision](../../../.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md)). Wheel over the textarea chains: the capped draft scrolls locally until its edge, then forwards to that host. Safari alone receives a pre-paint recovery when a native edit shortens the draft and leaves stale soft-wrap overflow; draft growth, programmatic updates, and other browsers never read layout for that recovery ([decision](../../../.agents/notes/implemented/bug-fix/2026-08-13-safari-textarea-soft-wrap-reflow.md)). +The resident conversation shell survives no-session and session transitions. Without a current session it locks message actions and presents the whole dashed composer card as a trigger for the root-scoped `conversation.hero.workspace` Workspace picker; the textarea remains read-only and keyboard-accessible. The Hero's leading mark is the independent root-scoped `conversation.hero.brand.mark` slot, with the fish mark as its fallback. Selecting a Workspace connects or reuses its Host-owned blank session and opens that session without replacing the shell. The root always owns the same scrollport and Hero/composer subtree; separate strict-session header and body outlets fill their regions when the first Session arrives, so the Workspace picker, scroll body, composer seat, and textarea retain their React and DOM identity. Blank sessions render the same composer body as active sessions, while the InputHub carries drafts across Workspace switches and mirrors them into the session store. In the active phase the session header shows only the current session title and view tabs as ordinary column chrome; fork lineage remains session data and is not projected into the header. Beneath it the scrollport (`data-conversation-scroll`) holds the flowing views and the sticky composer stack (stats dock + input docks + bar). That scrollport reserves its scrollbar gutter unconditionally, and a view opting into a composer overlay leaves it a scroll container, so the input card keeps one horizontal position whether or not the transcript scrolls and whichever view tab is shown ([decision](../../../.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md)). Wheel over the textarea chains: the capped draft scrolls locally until its edge, then forwards to that host. Safari alone receives a pre-paint recovery when a native edit shortens the draft and leaves stale soft-wrap overflow; draft growth, programmatic updates, and other browsers never read layout for that recovery ([decision](../../../.agents/notes/implemented/bug-fix/2026-08-13-safari-textarea-soft-wrap-reflow.md)). Another plugin can make one session's composer inert through `ctx.conversation.blocks`: it sets a block carrying its own localized reason, and the bar renders the same disabled textarea with that reason as the placeholder — the no-workspace posture, reused. The push direction is the constraint, not a preference: the plugins that know a session cannot send (ui-model-selection, when no adapter serves its route) already depend on this package, so this package cannot read them. The model seat is the one control a block leaves live — every block this contract has is cleared by choosing a model, so locking it too would leave the composer asking for the only thing it prevents. A block is an affordance only; the Host refuses a prompt it cannot route regardless of what any client disables. The no-workspace state wins when both hold, because picking a workspace is the earlier prerequisite. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index cbe13efd43..0b7af0b42e 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -6,7 +6,7 @@ 压缩(compaction)在检查点自身的消息流位置渲染为一行折叠标记,不替换其上方的 transcript(文本记录)。自动压缩使用「上下文已压缩」标题。每个已加载对应 `compaction/summary` 事件的完成标记都会显示被替换条目数量和估算 token 数量,并可点击展开摘要。手动 `/compact` 开始时显示为运行中的 `compact` 行;成功结算后,其显式摘要事件引用会在保持同一 React key 的前提下把该命令折叠进检查点行。完成的检查点静止时保留上下文压缩(context compaction)图标,仅在悬停或键盘聚焦时将其替换为收起/展开指示图标。输入被拒绝、没有可压缩历史、取消和失败时仍使用通用命令行及处理器撰写的文本。配对绝不依赖相邻关系,因为压缩运行期间可能注入持久上下文。面向模型的带框检查点载荷绝不渲染;被引用的 `compaction/summary` 事件位于已加载窗口之外时,检查点仍然可见但不可展开。 -常驻会话壳会跨无会话与会话状态切换而保留。没有当前会话时,它会锁定消息操作,并让整张虚线编辑器卡片成为根作用域 `conversation.hero.workspace` Workspace picker 的入口;textarea 保持只读且支持键盘操作。选择 Workspace 会连接或复用由 Host 拥有的空白会话,并在不替换会话壳的情况下打开该会话。根组件始终拥有同一个滚动容器与 Hero/编辑器子树;首个会话到达时,彼此独立的严格会话页头和主体 outlet 只填入各自区域,因此 Workspace picker、滚动主体、编辑器 seat 与 textarea 都保留原有 React 和 DOM identity。空白会话与活跃会话渲染相同的输入区主体;InputHub 则在 Workspace 切换间携带草稿,并将草稿镜像到会话 store。活跃阶段,会话标题栏作为普通列 chrome,仅显示当前会话标题和视图标签;fork 谱系仍保留为会话数据,不投影到标题栏。其下滚动容器(`data-conversation-scroll`)承载流动排版的各视图与 sticky 编辑器栈(统计 dock+输入区 dock+输入栏)。该滚动容器无条件预留自己的滚动条槽,选用编辑器 overlay 的视图也仍把它保留为滚动容器,因此无论对话记录是否滚动、无论展示哪个视图标签,输入卡片都保持同一个横向位置([决策](../../../.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md))。textarea 上的滚轮会链式处理:限高草稿先在本地滚动,到达边缘后再转交给该宿主。只有 Safari 会在原生编辑缩短草稿并留下陈旧软换行溢出时执行绘制前恢复;草稿增长、程序化更新与其他浏览器都不会为这项恢复读取布局([决策](../../../.agents/notes/implemented/bug-fix/2026-08-13-safari-textarea-soft-wrap-reflow.md))。 +常驻会话壳会跨无会话与会话状态切换而保留。没有当前会话时,它会锁定消息操作,并让整张虚线编辑器卡片成为根作用域 `conversation.hero.workspace` Workspace picker 的入口;textarea 保持只读且支持键盘操作。Hero 前方的标记是独立的根作用域 `conversation.hero.brand.mark` slot,未被占用时回退到鱼形标记。选择 Workspace 会连接或复用由 Host 拥有的空白会话,并在不替换会话壳的情况下打开该会话。根组件始终拥有同一个滚动容器与 Hero/编辑器子树;首个会话到达时,彼此独立的严格会话页头和主体 outlet 只填入各自区域,因此 Workspace picker、滚动主体、编辑器 seat 与 textarea 都保留原有 React 和 DOM identity。空白会话与活跃会话渲染相同的输入区主体;InputHub 则在 Workspace 切换间携带草稿,并将草稿镜像到会话 store。活跃阶段,会话标题栏作为普通列 chrome,仅显示当前会话标题和视图标签;fork 谱系仍保留为会话数据,不投影到标题栏。其下滚动容器(`data-conversation-scroll`)承载流动排版的各视图与 sticky 编辑器栈(统计 dock+输入区 dock+输入栏)。该滚动容器无条件预留自己的滚动条槽,选用编辑器 overlay 的视图也仍把它保留为滚动容器,因此无论对话记录是否滚动、无论展示哪个视图标签,输入卡片都保持同一个横向位置([决策](../../../.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md))。textarea 上的滚轮会链式处理:限高草稿先在本地滚动,到达边缘后再转交给该宿主。只有 Safari 会在原生编辑缩短草稿并留下陈旧软换行溢出时执行绘制前恢复;草稿增长、程序化更新与其他浏览器都不会为这项恢复读取布局([决策](../../../.agents/notes/implemented/bug-fix/2026-08-13-safari-textarea-soft-wrap-reflow.md))。 别的插件可以经 `ctx.conversation.blocks` 让某个会话的编辑器变为惰性:它设置一个携带自己本地化理由的 block,输入栏就渲染同一个禁用的 textarea,并把该理由作为 placeholder——复用无 Workspace 时的那套姿态。推送方向是约束而非偏好:知道某会话发不出消息的插件(ui-model-selection,在没有适配器服务其路由时)本就依赖本包,因此本包读不到它们。模型 seat 是 block 唯一保留可用的控件——这份约定里的每个 block 都靠选模型来解除,把它一起锁上会让编辑器索要它自己拦下的那件事。block 只是提示性设计;无论客户端禁用了什么,宿主都会拒绝一个它无法路由的提示词。两者同时成立时以无 Workspace 姿态为准,因为选 Workspace 是更靠前的前提。 diff --git a/packages/client/ui-conversation/src/client/apply.ts b/packages/client/ui-conversation/src/client/apply.ts index 59b010f93a..294009a09b 100644 --- a/packages/client/ui-conversation/src/client/apply.ts +++ b/packages/client/ui-conversation/src/client/apply.ts @@ -206,6 +206,7 @@ export function apply(ctx: Context): void { 'conversation.composer.dock': { kind: 'list', scope: 'session' }, 'conversation.input.left': { kind: 'list', scope: 'session' }, 'conversation.input.right': { kind: 'list', scope: 'session' }, + 'conversation.hero.brand.mark': { kind: 'single', scope: 'root' }, 'conversation.hero.workspace': { kind: 'single', scope: 'root' }, 'conversation.hero.agentPreset': { kind: 'single', scope: 'root' }, }, diff --git a/packages/client/ui-conversation/src/client/contract/slots.ts b/packages/client/ui-conversation/src/client/contract/slots.ts index 6eb3442925..4ea0c79402 100644 --- a/packages/client/ui-conversation/src/client/contract/slots.ts +++ b/packages/client/ui-conversation/src/client/contract/slots.ts @@ -166,6 +166,11 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { * reads the global workspace list. */ 'conversation.hero.workspace': { kind: 'single'; scope: 'root'; owner: EmptyWorkspaceOwnerProps } + /** + * Brand mark leading the blank-session headline. Declared by this + * package's `conversation` entry; the shell supplies a fish fallback. + */ + 'conversation.hero.brand.mark': { kind: 'single'; scope: 'root'; owner: HeroBrandMarkOwnerProps } /** * The agent-preset chip beside the workspace picker on the new-session * screen. Root scope: no session exists yet, so the choice is staged for @@ -598,6 +603,14 @@ export interface ComposerChainProps { session: ConversationSnapshot | undefined } +/** Presentation props supplied to the blank-session brand-mark occupant. */ +export interface HeroBrandMarkOwnerProps { + /** Requested square edge in pixels. */ + size: number + /** Host CSS class for preserving the default hero mark color and hover motion. */ + className?: string | undefined +} + /** * Full conversation-slot component props: runtime & child-render (view ring * + composer chain/bar + input-region + hero picker slots) & store & injected @@ -610,6 +623,7 @@ export type ConversationSlotProps = | 'conversation.input.overlay' | 'conversation.input.dock' | 'conversation.composer.dock' | 'conversation.input.left' | 'conversation.input.right' + | 'conversation.hero.brand.mark' | 'conversation.hero.workspace' | 'conversation.hero.agentPreset' > diff --git a/packages/client/ui-conversation/src/client/index.ts b/packages/client/ui-conversation/src/client/index.ts index 4a8b27acbb..814734411b 100644 --- a/packages/client/ui-conversation/src/client/index.ts +++ b/packages/client/ui-conversation/src/client/index.ts @@ -32,7 +32,7 @@ export type { ChatStore, ChatViewInjected, ChatViewSlotProps, CommandRowOwnerProps, CommandRowProps, ComposerBarInjected, ComposerAttachment, ComposerAttachmentsOwnerProps, ComposerAttachmentsProps, ComposerChainProps, ConversationInjected, ConversationSessionHeaderInjected, ConversationSessionInjected, ConversationSlotProps, ConvViewOwnerProps, - ConvViewProps, DetailsInjected, DetailsSlotProps, DetailsToolOwnerProps, EmptyWorkspaceOwnerProps, + ConvViewProps, DetailsInjected, DetailsSlotProps, DetailsToolOwnerProps, EmptyWorkspaceOwnerProps, HeroBrandMarkOwnerProps, MessageImagesOwnerProps, MessageImagesProps, RenderMessageImages, TurnTailOwnerProps, UseChatNodeTurnData, } from './contract/slots.ts' // Export discipline: packages/client/AGENTS.md. diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx index ef1221cd99..07655d7dba 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx @@ -159,7 +159,7 @@ export function ConversationRoot({ const composerBar = (
{hero && } - {hero && } + {hero && } {hero && heroWorkspaceRow} {zone !== undefined && renderSlot('conversation.input.dock', zone)} {inputBar} diff --git a/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx b/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx index 4865ceecfa..9db09e3141 100644 --- a/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx @@ -102,6 +102,8 @@ export function HeroGlow({ className }: { className?: string | undefined }) { export interface HeroShellProps { /** The owner's locale seat, passed down as a plain prop. */ t: HeroTranslate + /** Authorized renderer for the hero brand-mark slot. */ + renderSlot: ConversationSlotProps['renderSlot'] /** Overlay content after the stack (modals). */ children?: ReactNode } @@ -112,14 +114,16 @@ export interface HeroShellProps { * @param props - see {@link HeroShellProps}. * @returns the centered hero element tree. */ -export function HeroShell({ t, children }: HeroShellProps) { +export function HeroShell({ t, renderSlot, children }: HeroShellProps) { return (
{/* figma 34:10412: fish 34×25 leading the headline, gap 10. */} - + {renderSlot('conversation.hero.brand.mark', { size: 34, className: css.fish }, { + fallback: , + })} {t('hero.headline')} {t('hero.preview')} diff --git a/packages/client/ui-conversation/tests/chat-apply.client.spec.tsx b/packages/client/ui-conversation/tests/chat-apply.client.spec.tsx index c9f2dedb14..64f4dadb96 100644 --- a/packages/client/ui-conversation/tests/chat-apply.client.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-apply.client.spec.tsx @@ -91,6 +91,7 @@ describe('apply wiring', () => { // The hero holes ride the conversation entry's children declaration (the // empty-state occupant is gone). Both are root-scoped: the new-session // screen precedes the session either would belong to. + expect(b.slots.spec('conversation.hero.brand.mark')).toEqual({ kind: 'single', scope: 'root' }) expect(b.slots.spec('conversation.hero.workspace')).toEqual({ kind: 'single', scope: 'root' }) expect(b.slots.spec('conversation.hero.agentPreset')).toEqual({ kind: 'single', scope: 'root' }) expect(b.slots.entries('settings.general.item').map(entry => entry.options.id)).toEqual(['composer-enter']) diff --git a/packages/client/ui-conversation/tests/skeleton.client.spec.tsx b/packages/client/ui-conversation/tests/skeleton.client.spec.tsx index 20a72b1503..2db2db153b 100644 --- a/packages/client/ui-conversation/tests/skeleton.client.spec.tsx +++ b/packages/client/ui-conversation/tests/skeleton.client.spec.tsx @@ -22,6 +22,7 @@ import { en, zh } from '../src/client/locales.ts' import { ConversationRoot } from '../src/client/skeleton/ConversationRoot.tsx' import { ConversationSession, ConversationSessionHeader } from '../src/client/skeleton/ConversationSession.tsx' import { HeroShell } from '../src/client/skeleton/EmptyHero.tsx' +import type { HeroShellProps } from '../src/client/skeleton/EmptyHero.tsx' import { InputBar } from '../src/client/skeleton/InputBar.tsx' import type { InputBarProps } from '../src/client/skeleton/InputBar.tsx' import type { @@ -259,9 +260,14 @@ function mount( describe('Hero chrome', () => { it('renders the English preview badge through the hero locale seat', () => { - const view = render() + const renderSlot = vi.fn(() => null) + const view = render() expect(view.getByText('Into the Unknown')).toBeTruthy() expect(view.getByText('Preview')).toBeTruthy() + expect(renderSlot).toHaveBeenCalledOnce() + expect(renderSlot.mock.calls[0]?.[0]).toBe('conversation.hero.brand.mark') + expect(renderSlot.mock.calls[0]?.[1]).toEqual({ size: 34 }) + expect(renderSlot.mock.calls[0]?.[2]?.fallback).toBeTruthy() }) }) diff --git a/packages/client/ui-primitives/src/BrandWordmark.tsx b/packages/client/ui-primitives/src/BrandWordmark.tsx index 768fcdf92c..ed86980646 100644 --- a/packages/client/ui-primitives/src/BrandWordmark.tsx +++ b/packages/client/ui-primitives/src/BrandWordmark.tsx @@ -5,19 +5,27 @@ import type { IconProps } from './icons/props.ts' +/** Display options for the official brand wordmark. */ +export interface BrandWordmarkProps extends IconProps { + /** Whether to include the leading whale mark; defaults to true. */ + includeMark?: boolean | undefined +} + /** * Render the full brand wordmark. - * @param props.size - height in px (default 24; width keeps the 182:24 ratio). + * @param props.size - height in px (default 24; width follows the selected artwork). * @param props.className - extra class for layout placement. + * @param props.includeMark - whether to include the leading whale mark. * @returns the wordmark svg (aria-hidden decorative brand art). */ -export function BrandWordmark({ size = 24, className }: IconProps) { +export function BrandWordmark({ size = 24, className, includeMark = true }: BrandWordmarkProps) { + const width = includeMark ? 182 : 156 return (
- {/* Expanded, the wordmark doubles as a New Session shortcut; the + {/* Expanded, the brand doubles as a New Session shortcut; the collapsed rail's logo is the expand toggle below instead. */} {wide && ( )} {/* Rail resting state is the whale mark; hovering swaps in the panel @@ -149,7 +163,11 @@ export function SidebarRoot({ aria-label={collapsed ? t('toggle.open') : t('toggle.collapse')} onClick={() => { toggleSidebar() }} > - {!wide && } + {!wide && ( + + )} {/* Rail icons render at 18 (figma rail spec); expanded keeps the glyph-native sizes. */} diff --git a/packages/client/ui-sidebar/src/client/contract/slots.ts b/packages/client/ui-sidebar/src/client/contract/slots.ts index a8c4f2894d..65f0102543 100644 --- a/packages/client/ui-sidebar/src/client/contract/slots.ts +++ b/packages/client/ui-sidebar/src/client/contract/slots.ts @@ -15,6 +15,17 @@ import type { WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client' declare module '@deepseek-ai/dsh-client-ui-slots' { interface SlotMap { + /** + * Brand mark rendered in the expanded brand row and collapsed rail. + * Declared by this package's `sidebar` entry; deployments may replace + * the shell's fish fallback without replacing the surrounding controls. + */ + 'sidebar.brand.mark': { kind: 'single'; scope: 'root'; owner: SidebarBrandMarkOwnerProps } + /** + * Brand name rendered beside the expanded mark. Declared by this + * package's `sidebar` entry; the shell supplies a generic text fallback. + */ + 'sidebar.brand.name': { kind: 'single'; scope: 'root'; owner: SidebarBrandNameOwnerProps } /** * The workspace/session browsing region: section header, search, the * grouped/flat session list, and every workspace dialog. Declared by this @@ -36,6 +47,18 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { } } +/** Geometry supplied to the sidebar brand-mark occupant. */ +export interface SidebarBrandMarkOwnerProps { + /** Requested square edge in pixels. */ + size: number +} + +/** Empty owner share for the sidebar brand-name occupant. */ +export interface SidebarBrandNameOwnerProps { + /** Marker field: the occupant owns its own content and width. */ + children?: never +} + /** * Owner share of the browser hole — the only facts crossing the shell/region * boundary. Business data and actions arrive through the region's own inject. @@ -85,5 +108,11 @@ export type SidebarRootInjected = { */ export type SidebarRootComponentProps = PropsRuntime<'sidebar'> - & PropsRenderSlots<'sidebar.workspaces' | 'sidebar.settings' | 'sidebar.footer.action'> + & PropsRenderSlots< + | 'sidebar.brand.mark' + | 'sidebar.brand.name' + | 'sidebar.workspaces' + | 'sidebar.settings' + | 'sidebar.footer.action' + > & SidebarRootInjected & PropsLocale<'sidebar'> diff --git a/packages/client/ui-sidebar/src/client/index.ts b/packages/client/ui-sidebar/src/client/index.ts index 0bd2c98295..5b8ac288f7 100644 --- a/packages/client/ui-sidebar/src/client/index.ts +++ b/packages/client/ui-sidebar/src/client/index.ts @@ -7,8 +7,8 @@ import { SidebarRoot } from './SidebarRoot.tsx' import { en, zh, type SidebarKey } from './locales.ts' export type { - SidebarFooterActionOwnerProps, SidebarRootComponentProps, SidebarRootInjected, - SidebarSectionOwnerProps, SidebarSettingsOwnerProps, + SidebarBrandMarkOwnerProps, SidebarBrandNameOwnerProps, SidebarFooterActionOwnerProps, + SidebarRootComponentProps, SidebarRootInjected, SidebarSectionOwnerProps, SidebarSettingsOwnerProps, } from './contract/slots.ts' export type { SidebarKey } from './locales.ts' @@ -45,6 +45,8 @@ export function apply(ctx: ClientContext): void { // region (header, search, session list, workspace dialogs), ui-settings // registers the foot trigger + settings panel. children: { + 'sidebar.brand.mark': { kind: 'single', scope: 'root' }, + 'sidebar.brand.name': { kind: 'single', scope: 'root' }, 'sidebar.workspaces': { kind: 'single', scope: 'root' }, 'sidebar.settings': { kind: 'single', scope: 'root' }, 'sidebar.footer.action': { kind: 'list', scope: 'root' }, diff --git a/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap b/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap index f82fbc1656..a8ef9f5d4e 100644 --- a/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap +++ b/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap @@ -17,15 +17,24 @@ exports[`sidebar shell snapshots > renders the collapsed rail after the crossfad class="iconButton toggle" type="button" > - + class="railMark" + > +
+ +
+ renders the expanded column (wordmark, capsul class="brand wide" type="button" > - + class="brandIdentity" + > + +
+ +
+
+ +
+ + DSH Local Build + + + abc1234 + +
+
+