Kaynağa Gözat

Merge pull request #1533 from deepseek-harness/feat/web-preview-badge

feat(web): add preview badge to empty hero
Ziya 1 ay önce
ebeveyn
işleme
cb6fa78c8a

+ 6 - 0
.agents/notes/implemented/feature/2026-08-05-web-preview-product-badge.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 .agents/notes/implemented/feature/2026-08-05-web-preview-product-badge.md
+2026-08-05-web-preview-product-badge.md: c20dedf8caa497d17a577cf46261c6a24c09a1ce
+2026-08-05-web-preview-product-badge.zh.md: c428dabf2a9a2ece2f90b0c3a527835a494c7cb7

+ 33 - 0
.agents/notes/implemented/feature/2026-08-05-web-preview-product-badge.md

@@ -0,0 +1,33 @@
+# Agent Note: Web preview product badge
+
+Status: implemented
+
+English | [中文](2026-08-05-web-preview-product-badge.zh.md)
+
+## Problem
+
+The Web empty state does not identify the product as a preview. Users can enter the main session surface without seeing that the product is pre-release, while a deployment setting would misrepresent a product-wide lifecycle decision as an operator choice.
+
+## Decision
+
+The empty hero always renders a localized `Preview` / `预览版` badge beneath the headline. It has no configuration switch: preview status is one product identity shared by every deployment, not a deployment-varying tunable.
+
+The badge keeps the business-tertiary background so both themes retain the product-blue context, and uses the theme's primary label token for text. That pairing gives ordinary 12px text sufficient contrast in both light and dark themes; the business-primary foreground is reserved for larger or non-text accents because it does not reach the required contrast on this background.
+
+The badge leaves the product when the first tagged release removes the repository's pre-release stance, or when the owning product decision declares the preview phase complete. That change removes the badge and its locale key together rather than adding a runtime toggle.
+
+## Alternatives considered
+
+**Make preview status configurable.** Rejected because two deployments of the same pre-release product must not present different lifecycle identities, and a configuration field would turn product release state into an unsupported operator choice.
+
+**Use business-primary text on the business-tertiary background.** Rejected because the resulting light- and dark-theme contrast is below the 4.5:1 requirement for the badge's 12px text.
+
+**Hide the badge from the accessibility tree.** Rejected because preview status is product information rather than decoration; the accessible headline therefore includes the badge text.
+
+## Consequences
+
+Every new session exposes the same localized preview identity in visual and accessibility output. Removing preview status is an explicit product-release edit, and the badge favors readable neutral text over an all-blue treatment while retaining the business-tinted background.
+
+## Testing
+
+The conversation component test covers both localized badge values, and the Web lifecycle snapshots pin the English badge in the assembled empty hero.

+ 33 - 0
.agents/notes/implemented/feature/2026-08-05-web-preview-product-badge.zh.md

@@ -0,0 +1,33 @@
+# Agent Note:Web 预览版产品徽标
+
+状态:已实现
+
+[English](2026-08-05-web-preview-product-badge.md) | 中文
+
+## 问题
+
+Web 空状态没有标明产品处于预览版阶段。用户可以在未看到产品尚未正式发布的情况下进入主会话界面;若改用部署设置,则会把面向整个产品的生命周期决策误表述为操作者的选择。
+
+## 决策
+
+空状态主视觉区始终在标题下方渲染本地化的 `Preview` / `预览版` 徽标。它没有配置开关:预览状态是所有部署共同的一项产品身份,而不是随部署变化的可调参数。
+
+徽标沿用 business-tertiary 背景,使两套主题都保留产品蓝的视觉语境;文字则使用主题的 primary label token。这一组合让普通 12px 文字在浅色与暗色主题下都有足够的对比度。business-primary 前景色仅留给较大字号文本或非文本强调元素,因为它在该背景上达不到要求的对比度。
+
+首个 tagged release 取消仓库的预发布立场时,或归属产品方明确决定预览阶段结束时,产品会移除该徽标。这一改动会同时移除徽标及其 locale key,而不是增加运行时开关。
+
+## 曾考虑的替代方案
+
+**让预览状态可配置。** 不予采纳:同一个预发布产品的两套部署不得展示不同的生命周期身份,配置字段还会把产品发布状态变成一项不受支持的操作者选择。
+
+**在 business-tertiary 背景上使用 business-primary 文字。** 不予采纳:由此产生的浅色与暗色主题对比度低于徽标 12px 文字所要求的 4.5:1。
+
+**在无障碍树中隐藏徽标。** 不予采纳:预览状态是产品信息而非装饰,因此无障碍标题会包含徽标文字。
+
+## 后果
+
+每个新会话都会在视觉与无障碍输出中呈现相同的本地化预览版身份。移除预览状态是一项显式的产品发布改动;徽标保留业务蓝色调背景,同时采用可读的中性色文字,而不是全蓝色处理方案。
+
+## 测试
+
+会话组件测试覆盖两个本地化徽标值,Web 生命周期快照则固定组装后空状态主视觉区中的英文徽标。

+ 1 - 1
apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md

@@ -20,7 +20,7 @@
 - button "Settings":
   - img
   - text: Settings
-- text: Let's start building
+- text: Let's start building Preview
 - button "Choose workspace":
   - img
   - text: workspace

+ 1 - 1
apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md

@@ -20,7 +20,7 @@
 - button "Settings":
   - img
   - text: Settings
-- text: Let's start building
+- text: Let's start building Preview
 - button "Choose workspace":
   - img
   - text: workspace

+ 1 - 1
apps/web/tests/snapshots/markdown-cjk-strong/ui.expected.md

@@ -40,7 +40,7 @@
   - img
 - button "Branch into a new conversation":
   - img
-- text: {{clock}}Ran for {{duration}}
+- text: {{clock}} Ran for {{duration}}
 - textbox "Message the agent"
 - button "Commands":
   - img

+ 1 - 1
apps/web/tests/snapshots/markdown-inline-code-links/ui.expected.md

@@ -31,7 +31,7 @@
   - img
 - button "Branch into a new conversation":
   - img
-- text: {{clock}}Ran for {{duration}}
+- text: {{clock}} Ran for {{duration}}
 - textbox "Message the agent"
 - button "Commands":
   - img

+ 2 - 0
packages/client/ui-conversation/src/client/locales.ts

@@ -45,6 +45,7 @@ export const zh = {
   'access.confirm.cancel': '取消',
   'access.confirm.enable': '启用 Full access',
   'hero.headline': '开始构建吧',
+  'hero.preview': '预览版',
   'hero.chooseWorkspace': '选择工作区',
   'session.hierarchy': '会话层级',
   'details.title': '详情',
@@ -184,6 +185,7 @@ export const en = {
   'access.confirm.cancel': 'Cancel',
   'access.confirm.enable': 'Enable Full access',
   'hero.headline': 'Let\'s start building',
+  'hero.preview': 'Preview',
   'hero.chooseWorkspace': 'Choose workspace',
   'session.hierarchy': 'Session hierarchy',
   'details.title': 'Details',

+ 2 - 1
packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx

@@ -119,7 +119,8 @@ export function HeroShell({ t, children }: HeroShellProps) {
         <div className={css.headline}>
           {/* figma 34:10412: fish 34×25 leading the headline, gap 10. */}
           <FishLogo size={34} className={css.fish} />
-          {t('hero.headline')}
+          <span className={css.headlineText}>{t('hero.headline')}</span>
+          <span className={css.previewBadge}>{t('hero.preview')}</span>
         </div>
         <div className={css.body}>
           {/* The resident composer (ConversationRoot wrapActiveBody seat; the

+ 27 - 4
packages/client/ui-conversation/src/client/skeleton/HeroShell.module.css

@@ -23,21 +23,44 @@
   overflow: visible;
 }
 
-/* figma 34:10411: fish + title, gap 10, centered; 26/32 wt500. */
+/* figma 34:10411: fish + title, gap 10, centered; 26/32 wt500. The preview
+   badge is a product addition outside that source and aligns to the title. */
 .headline {
-  display: flex;
+  display: grid;
+  grid-template-columns: 34px auto;
+  column-gap: 10px;
+  row-gap: 4px;
   align-items: center;
   justify-content: center;
-  gap: 10px;
   font-size: 26px;
   line-height: 32px;
   font-weight: 500;
   color: var(--dsw-alias-label-primary);
 }
 
+.headlineText {
+  grid-row: 1;
+  grid-column: 2;
+}
+
+.previewBadge {
+  grid-row: 2;
+  grid-column: 2;
+  justify-self: start;
+  padding: 0 4px;
+  border-radius: 4px;
+  background: var(--dsw-alias-state-business-tertiary);
+  color: var(--dsw-alias-label-primary);
+  font-size: 12px;
+  line-height: 18px;
+  font-weight: 500;
+  white-space: nowrap;
+}
+
 /* figma fish fill rides business blue. */
 .fish {
-  flex: none;
+  grid-row: 1;
+  grid-column: 1;
   color: var(--dsw-alias-state-business-primary);
 }
 

+ 12 - 1
packages/client/ui-conversation/tests/skeleton.spec.tsx

@@ -12,12 +12,14 @@ import type {
 import type { ConversationRootProps } from '../src/client/skeleton/ConversationRoot.tsx'
 import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
 import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
+import { en as commonEn } from '@deepseek-ai/dsh-client-locale/src/locales/en.ts'
 import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
 import { createChatStore } from '../src/client/stores.ts'
 import { SessionInputShell } from '../src/client/input/facade.ts'
-import { zh } from '../src/client/locales.ts'
+import { en, zh } from '../src/client/locales.ts'
 import { ConversationRoot } from '../src/client/skeleton/ConversationRoot.tsx'
 import { ConversationSession } from '../src/client/skeleton/ConversationSession.tsx'
+import { HeroShell } 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 {
@@ -213,6 +215,14 @@ function mount(
   }
 }
 
+describe('Hero chrome', () => {
+  it('renders the English preview badge through the hero locale seat', () => {
+    const view = render(<HeroShell t={makeTranslate(en, commonEn)} />)
+    expect(view.getByText('Let\'s start building')).toBeTruthy()
+    expect(view.getByText('Preview')).toBeTruthy()
+  })
+})
+
 describe('ConversationRoot resident composer', () => {
   it('keeps composer text in the machine, mirrors to the chat store, and submits through the sink', () => {
     const b = mount(conversationSnapshot())
@@ -273,6 +283,7 @@ describe('ConversationRoot resident composer', () => {
     expect(host).not.toBeNull()
     expect(header?.getAttribute('aria-hidden')).toBe('true')
     expect(b.view.getByText('开始构建吧')).toBeTruthy()
+    expect(b.view.getByText('预览版')).toBeTruthy()
     expect(b.view.queryByTestId('view-chat')).toBeNull()
     // The same machine-backed textarea is live in the hero, and the
     // persistence mirror stays bound (ConversationSession mounts chrome-hidden