diff --git a/packages/client/ui-attachment/src/FileCard.tsx b/packages/client/ui-attachment/src/FileCard.tsx
index 3692302325..a8359cae61 100644
--- a/packages/client/ui-attachment/src/FileCard.tsx
+++ b/packages/client/ui-attachment/src/FileCard.tsx
@@ -1,4 +1,4 @@
-import { fileExtension, FileTypeIcon, fileSizeText, IconCloseFill14 } from '@deepseek-ai/dsh-client-ui-primitives'
+import { fileExtension, FileTypeIcon, fileSizeText, IconCloseFillRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import css from './FileCard.module.css'
/** Localized strings consumed by one pending-file card. */
@@ -66,7 +66,7 @@ export function FileCard({
aria-label={labels.remove}
onClick={onRemove}
>
-
+
{state === 'uploading' && (
diff --git a/packages/client/ui-attachment/src/ImageLightbox.tsx b/packages/client/ui-attachment/src/ImageLightbox.tsx
index 1f1207e026..c2aff2eeb3 100644
--- a/packages/client/ui-attachment/src/ImageLightbox.tsx
+++ b/packages/client/ui-attachment/src/ImageLightbox.tsx
@@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react'
import { createPortal } from 'react-dom'
-import { IconCloseOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
+import { IconCloseOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import css from './ImageLightbox.module.css'
/** Lightbox strings the owner resolves from its own locale namespace. */
@@ -56,7 +56,7 @@ export function ImageLightbox({ src, alt, labels, onClose }: {
,
document.body,
diff --git a/packages/client/ui-attachment/src/MessageImage.tsx b/packages/client/ui-attachment/src/MessageImage.tsx
index be35aa650c..caf8fe08f8 100644
--- a/packages/client/ui-attachment/src/MessageImage.tsx
+++ b/packages/client/ui-attachment/src/MessageImage.tsx
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
-import { IconLoadingOutline16, IconRefreshOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
+import { IconLoadingOutlineRegular, IconRefreshOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import { ImageLightbox } from './ImageLightbox.tsx'
import type { ImageLightboxLabels } from './ImageLightbox.tsx'
import css from './MessageImage.module.css'
@@ -135,7 +135,7 @@ export function MessageImage({ image, load, variant, labels }: {
onClick={request}
>
{variant === 'thumbnail'
- ?
+ ?
: labels.loadFailed}
)
@@ -154,7 +154,7 @@ export function MessageImage({ image, load, variant, labels }: {
{src === null
? (
- {loadingThumbnail ? : labels.loading}
+ {loadingThumbnail ? : labels.loading}
)
: }
diff --git a/packages/client/ui-attachment/src/client/ComposerAttachments.tsx b/packages/client/ui-attachment/src/client/ComposerAttachments.tsx
index ca2e1e1aa7..7c9054890d 100644
--- a/packages/client/ui-attachment/src/client/ComposerAttachments.tsx
+++ b/packages/client/ui-attachment/src/client/ComposerAttachments.tsx
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type {
ComposerAttachment, ComposerAttachmentsProps, ComposerImageAttachment,
} from '@deepseek-ai/dsh-client-ui-conversation/client'
-import { IconCloseFill14 } from '@deepseek-ai/dsh-client-ui-primitives'
+import { IconCloseFillRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import { AttachmentRail } from '../AttachmentRail.tsx'
import type { AttachmentRailItem } from '../AttachmentRail.tsx'
import { DropOverlay } from '../DropOverlay.tsx'
@@ -87,7 +87,7 @@ export function ComposerAttachments({
aria-label={t('image.remove', { name: attachment.file.name })}
onClick={() => { onRemoveAttachment(attachment.id) }}
>
-
+
)
diff --git a/packages/client/ui-brand-official/package.json b/packages/client/ui-brand-official/package.json
index 24c8702e21..3fd7446384 100644
--- a/packages/client/ui-brand-official/package.json
+++ b/packages/client/ui-brand-official/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-brand-official",
"description": "Official DeepSeek Harness brand occupants for the Web client's sidebar slots",
- "version": "0.1.6-alpha.1",
+ "version": "0.1.6-alpha.2",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-chat/README.i18n.yaml b/packages/client/ui-chat/README.i18n.yaml
index 1d6df37e4d..78519947ae 100644
--- a/packages/client/ui-chat/README.i18n.yaml
+++ b/packages/client/ui-chat/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-chat/README.md
-README.md: e7d3efe37e089f4b7bcbb7d954e6791899cebfc0
-README.zh.md: 8b49332ad39d373fcc2aae66af2e7c284bfee9bb
+README.md: 92c71663e9193e24657a31e57204a9bf076e19c3
+README.zh.md: 702042384b81d028b7db40275e5ee705dc949fb2
diff --git a/packages/client/ui-chat/README.md b/packages/client/ui-chat/README.md
index e7d3efe37e..92c71663e9 100644
--- a/packages/client/ui-chat/README.md
+++ b/packages/client/ui-chat/README.md
@@ -16,6 +16,7 @@ File-mention providers receive the viewed Session ID with the closing-turn owner
- [Reference previews](#reference-previews)
- [System prompt row](#system-prompt-row)
+- [Command and failure rows](#command-and-failure-rows)
- [Turn token usage](#turn-token-usage)
- [Completed-turn footer](#completed-turn-footer)
- [Turn Process Folding](#turn-process-folding)
@@ -38,6 +39,13 @@ HTTP(S) links in Assistant Markdown open a new right-Sidebar Browser tab on ordi
Each nonempty appended `system/message` owns a collapsed prompt row, including a complete prompt at the start of a headerless window; the same-step header does not duplicate it. Chat also shows a collapsed `System prompt` row for a non-empty initial request, explicit message-series start, or `system/message` surface node replacement whose text differs, reading the last nonempty surviving system node in surface order at the `request/header`; a non-initial request whose preceding header is outside the loaded history window also shows one. A resume repeats the row even when its system text is unchanged, including after pagination supplies the preceding header and system node; same-series config-only or tool-only changes, tool steps, and retries create no repetition, and a `system/message` event is never rendered as a transcript message. The row appears before that request's user messages, matching the provider envelope, and expands to the exact model-visible text with its original line breaks. A request whose system node is empty or outside the loaded window creates no row until the page holding the node arrives.
+
+## Command and failure rows
+
+Generic command rows retain the ordinary command glyph in every lifecycle state; failure remains explicit through the row state and summary. A terminal Turn failure remains a separate red-dot notice; intermediate model retries do not create that notice, and an output-token limit uses the amber warning dot.
+
+-----
+
## Turn token usage
diff --git a/packages/client/ui-chat/README.zh.md b/packages/client/ui-chat/README.zh.md
index 8b49332ad3..702042384b 100644
--- a/packages/client/ui-chat/README.zh.md
+++ b/packages/client/ui-chat/README.zh.md
@@ -16,6 +16,7 @@ kind: "package-reference"
- [引用预览](#reference-previews)
- [系统提示词行](#system-prompt-row)
+- [指令与失败行](#command-and-failure-rows)
- [轮次 token 用量](#turn-token-usage)
- [已完成轮次的页脚](#completed-turn-footer)
- [轮次过程折叠](#turn-process-folding)
@@ -38,6 +39,11 @@ Assistant Markdown 中的 HTTP(S) 链接在普通点击时会在该类型已注
每个非空追加的 `system/message` 都拥有一行折叠提示词,包括无 header 窗口起点的完整提示词;同一步骤的 header 不会重复它。Chat 也会为非空的初始请求、显式消息序列起点、文本发生变化的 `system/message` surface 节点替换(文本读取自 `request/header` 处 surface 顺序中最后一个非空存活系统节点),或前序 header 尚未进入已加载历史窗口的非初始请求显示一行默认折叠的 `系统提示词`。即使系统文本未变,恢复也会重复该行,包括分页补齐前序 header 和系统节点后;同一序列内仅配置或仅工具变化、工具步骤与重试不会重复,且 `system/message` 事件绝不会渲染为对话消息。该行位于请求的用户消息之前,与提供方 envelope 顺序一致;展开后显示模型所见的确切文本,并保留其原始换行。系统节点为空或位于已加载窗口之外的请求不创建该行,直到包含该节点的分页到达。
+
+## 指令与失败行
+
+通用指令行在所有生命周期状态中都保留普通指令图标;失败仍通过行状态与摘要明确表达。终止轮次的错误仍是独立的红点提示;模型的中间重试不会创建该提示,达到输出 token 上限时使用琥珀色警告点。
+
-----
diff --git a/packages/client/ui-chat/package.json b/packages/client/ui-chat/package.json
index d716f7b89b..46c3422a78 100644
--- a/packages/client/ui-chat/package.json
+++ b/packages/client/ui-chat/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-chat",
"description": "Chat Conversation target, node definitions, renderers, and details surface",
- "version": "0.1.6-alpha.1",
+ "version": "0.1.6-alpha.2",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-chat/src/client/chat/AssistantMarkdown.module.css b/packages/client/ui-chat/src/client/chat/AssistantMarkdown.module.css
index 106ac889db..43af8243e1 100644
--- a/packages/client/ui-chat/src/client/chat/AssistantMarkdown.module.css
+++ b/packages/client/ui-chat/src/client/chat/AssistantMarkdown.module.css
@@ -73,6 +73,6 @@
/* Finalized footer offset (figma 43:32997); chrome lives in MessageIconActions. */
.actions {
margin-top: 16px;
- /* Optical align with 28px icon hit targets that pad 6px past the glyph. */
+ /* Optical align with the 28px action hit targets. */
margin-left: -6px;
}
diff --git a/packages/client/ui-chat/src/client/chat/ChatView.tsx b/packages/client/ui-chat/src/client/chat/ChatView.tsx
index 4c407c8e8a..12a6f42546 100644
--- a/packages/client/ui-chat/src/client/chat/ChatView.tsx
+++ b/packages/client/ui-chat/src/client/chat/ChatView.tsx
@@ -7,7 +7,9 @@ import type {
} from '@deepseek-ai/dsh-client-ui-conversation/client'
import type { SessionSeq } from '@deepseek-ai/dsh-session/types'
import type { InboxState } from '@deepseek-ai/dsh-agent/types'
-import { Button, IconChevronDownOutline14, MarkdownDelegateProvider, Modal } from '@deepseek-ai/dsh-client-ui-primitives'
+import {
+ Button, IconChevronDownOutlineRegular, MarkdownDelegateProvider, Modal,
+} from '@deepseek-ai/dsh-client-ui-primitives'
import type { ChatViewSlotProps, OpenFileOptions } from '../contract/slots.ts'
import type { ChatSnapshot } from '../contract/snapshot.ts'
import { PendingSteeringBubble, PendingSubmissionBubble } from './MessageItem.tsx'
@@ -838,7 +840,7 @@ export function ChatView({
if (local !== null) toBottom(scrollerOf(local))
}}
>
-
+
)}
diff --git a/packages/client/ui-chat/src/client/chat/CompactionItem.tsx b/packages/client/ui-chat/src/client/chat/CompactionItem.tsx
index b6108daf1a..006b570557 100644
--- a/packages/client/ui-chat/src/client/chat/CompactionItem.tsx
+++ b/packages/client/ui-chat/src/client/chat/CompactionItem.tsx
@@ -3,9 +3,9 @@
import { memo, useMemo, useState } from 'react'
import {
- IconApiOutline14,
- IconChevronDownOutline14,
- IconChevronRightOutline14,
+ IconApiOutlineRegular,
+ IconChevronDownOutlineRegular,
+ IconChevronRightOutlineRegular,
MarkdownText,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { ChatViewSlotProps } from '../contract/slots.ts'
@@ -56,13 +56,13 @@ export const CompactionItem = memo(function CompactionItem({
>
-
+
- {open ? : }
+ {open ? : }
{title ?? t('message.compaction')}
diff --git a/packages/client/ui-chat/src/client/chat/ContextInjectionRow.tsx b/packages/client/ui-chat/src/client/chat/ContextInjectionRow.tsx
index c905035119..49704cca03 100644
--- a/packages/client/ui-chat/src/client/chat/ContextInjectionRow.tsx
+++ b/packages/client/ui-chat/src/client/chat/ContextInjectionRow.tsx
@@ -1,6 +1,6 @@
import { useState } from 'react'
import type { ChatViewSlotProps } from '../contract/slots.ts'
-import { DisclosureRow, IconContextInjectionOutline16, ReferenceIcon } from '@deepseek-ai/dsh-client-ui-primitives'
+import { DisclosureRow, IconContextInjectionOutlineRegular, ReferenceIconRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import type { ContextMessageNode } from '../contract/snapshot.ts'
import { contextBody } from './ContextBody.tsx'
import css from './ContextInjectionRow.module.css'
@@ -38,8 +38,8 @@ export function ContextInjectionRow({ content, source, producer, form, t }: Cont
- : }
+ ?
+ : }
chevronClassName={css.chevron}
title={t(producer.role === 'recall' ? 'message.contextRecall' : 'message.contextInjection')}
collapsedContent={producer.label === null ? undefined : (
diff --git a/packages/client/ui-chat/src/client/chat/GenericCommandCard.tsx b/packages/client/ui-chat/src/client/chat/GenericCommandCard.tsx
index f12992a043..2864836301 100644
--- a/packages/client/ui-chat/src/client/chat/GenericCommandCard.tsx
+++ b/packages/client/ui-chat/src/client/chat/GenericCommandCard.tsx
@@ -1,6 +1,6 @@
-import { useState, type ReactNode } from 'react'
+import { useState } from 'react'
import type { ChatViewSlotProps, CommandRowOwnerProps } from '../contract/slots.ts'
-import { DisclosureRow, IconApiOutline14, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
+import { DisclosureRow, IconApiOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import a11yCss from './accessibility.module.css'
import css from './GenericCommandCard.module.css'
@@ -12,10 +12,6 @@ function stateOf(outcome: CommandRowOwnerProps['node']['outcome']): CommandRowSt
return outcome.kind === 'error' ? 'error' : 'ok'
}
-function leadingFor(state: CommandRowState): ReactNode {
- return state === 'error' ? :
-}
-
/** Card props: the owner payload plus the render site's locale seat (plain prop). */
export interface GenericCommandCardProps extends CommandRowOwnerProps {
t: ChatViewSlotProps['t']
@@ -44,7 +40,7 @@ export function GenericCommandCard({ node, t, runningSummary }: GenericCommandCa
leadingClassName={css.leading}
titleClassName={css.title}
chevronClassName={css.chevron}
- icon={leadingFor(state)}
+ icon={}
title={title}
open={open}
expandable={body !== null}
diff --git a/packages/client/ui-chat/src/client/chat/MessageIconActions.module.css b/packages/client/ui-chat/src/client/chat/MessageIconActions.module.css
index ad8f6edb6c..c5234236f6 100644
--- a/packages/client/ui-chat/src/client/chat/MessageIconActions.module.css
+++ b/packages/client/ui-chat/src/client/chat/MessageIconActions.module.css
@@ -9,9 +9,7 @@
height: calc(28px + var(--dsh-content-font-delta, 0px));
}
-/* Clock before icons (user figma 388:20051) / after (assistant 43:32997).
- Both read the secondary tier so the user row's clock matches the assistant
- tail's meta line. */
+/* The user clock keeps the secondary tier. */
.timeStart {
padding-right: 12px;
font-size: var(--dsh-content-font-size-secondary, 13px);
@@ -20,16 +18,24 @@
white-space: nowrap;
}
-/* The assistant tail's clock sits at the row end after the stat pills; the
- row's 8px flex gap spaces it, and it reads the secondary tier so it matches
- the pill labels beside it. */
+/* Assistant-tail metadata keeps the smaller information size while sharing
+ the action icons' resting color. */
.timeEnd {
- font-size: var(--dsh-content-font-size-secondary, 13px);
+ font-size: calc(var(--dsh-content-font-size-secondary, 13px) - 1px);
line-height: calc(24px + var(--dsh-content-font-delta, 0px));
- color: var(--dsw-alias-label-tertiary);
+ color: inherit;
white-space: nowrap;
}
+.endInfo {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+ margin-left: 8px;
+ color: var(--dsw-alias-label-tertiary);
+}
+
/* Earlier user-authored flow rows and opted-in turn tails reveal the complete
actions row on hover or focus-within. Opacity keeps layout stable, and
devices without hover keep every row visible. */
@@ -68,13 +74,18 @@
cursor: pointer;
}
-/* The 15px action glyphs follow the same px delta as the text they serve;
- the CSS edge overrides each svg's own width/height attributes. */
+/* Action glyphs follow the same px delta as the text they serve; the
+ assistant completion row gives them two extra pixels. */
.action svg {
width: calc(15px + var(--dsh-content-font-delta, 0px));
height: calc(15px + var(--dsh-content-font-delta, 0px));
}
+.actions[data-clock='end'] .action svg {
+ width: calc(17px + var(--dsh-content-font-delta, 0px));
+ height: calc(17px + var(--dsh-content-font-delta, 0px));
+}
+
.action:hover {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-secondary);
diff --git a/packages/client/ui-chat/src/client/chat/MessageIconActions.tsx b/packages/client/ui-chat/src/client/chat/MessageIconActions.tsx
index 5476f1a5c0..fd2b674edd 100644
--- a/packages/client/ui-chat/src/client/chat/MessageIconActions.tsx
+++ b/packages/client/ui-chat/src/client/chat/MessageIconActions.tsx
@@ -3,7 +3,7 @@
import { useCallback, useEffect, useId, useRef, useState, type ReactNode } from 'react'
import {
- IconBranchOutline16, IconCheckOutline16, IconCopyOutline16, Tooltip, writeClipboard,
+ IconBranchOutlineRegular, IconCheckOutlineRegular, IconCopyOutlineRegular, Tooltip, writeClipboard,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { ChatViewSlotProps } from '../contract/slots.ts'
import { formatMessageClock } from './message-chrome.ts'
@@ -80,11 +80,11 @@ export function MessageIconActions({
)
return (
-
}
diff --git a/packages/client/ui-conversation/src/client/settings/EnterBehaviorRow.tsx b/packages/client/ui-conversation/src/client/settings/EnterBehaviorRow.tsx
index 07ebd615b3..cf8588067f 100644
--- a/packages/client/ui-conversation/src/client/settings/EnterBehaviorRow.tsx
+++ b/packages/client/ui-conversation/src/client/settings/EnterBehaviorRow.tsx
@@ -2,7 +2,7 @@
import { useState } from 'react'
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
-import { IconChevronDownOutline14, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
+import { IconChevronDownOutlineRegular, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
import type { BusyEnterBehavior } from '../contract/composer-submission.ts'
import type { ConversationKey } from '../locales.ts'
import css from './EnterBehaviorRow.module.css'
@@ -67,7 +67,7 @@ export function EnterBehaviorRow({ useBusyEnter, setBusyEnter, t }: EnterBehavio
onClick={() => { setOpen(value => !value) }}
>
{t(selectedLabel)}
-
+
)}
/>
diff --git a/packages/client/ui-conversation/src/client/skeleton/ContextMeter.module.css b/packages/client/ui-conversation/src/client/skeleton/ContextMeter.module.css
index a6a3e4c189..989377497d 100644
--- a/packages/client/ui-conversation/src/client/skeleton/ContextMeter.module.css
+++ b/packages/client/ui-conversation/src/client/skeleton/ContextMeter.module.css
@@ -52,6 +52,7 @@
border: 0;
border-radius: 12px;
background: var(--dsw-specific-menu);
+ backdrop-filter: var(--dsw-menu-backdrop-filter);
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
box-shadow: var(--dsw-elevation-prominent);
font-size: 12px;
diff --git a/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx b/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx
index 22be826845..40df87269b 100644
--- a/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx
+++ b/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx
@@ -4,7 +4,7 @@
import { useState } from 'react'
import type { ReactNode, RefObject } from 'react'
import {
- FISH_LOGO_PATH, FISH_LOGO_VIEWBOX, IconChevronDownOutline14, IconFolderClose16, IconFolderOpen16,
+ FISH_LOGO_PATH, FISH_LOGO_VIEWBOX, IconChevronDownOutlineRegular, IconFolderCloseRegular, IconFolderOpenRegular,
} from '@deepseek-ai/dsh-client-ui-primitives'
import { workspaceTitleOf } from '@deepseek-ai/dsh-util-workspace-path'
import type { ConversationContentProps } from '../contract/slots.ts'
@@ -53,10 +53,10 @@ export function WorkspaceChip({ buttonRef, label, menuOpen = false, onClick, t }
onClick={onClick}
>
{label === undefined
- ?
- : }
+ ?
+ : }
{label ?? t('hero.chooseWorkspace')}
-
+
)
}
diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css
index 8d321137c3..2ee0f6d16d 100644
--- a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css
+++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css
@@ -76,12 +76,9 @@
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
}
-/* No-workspace trigger state: dashed l4 stroke marks the card as a pick-a-
- workspace affordance rather than a live composer; hover answers in the
- business blue to invite the click. Native `dashed` has a fixed browser
- pattern, so the stroke is an ::after overlay: theme-token background masked
- by an SVG dash ring (stroke-width 2 centered on the box edge = 1px visible
- inside), which keeps the 22px radius and both themes. */
+/* No-workspace trigger state: an inset 1px dashed outline marks the card as a
+ workspace picker rather than a live composer. Keeping the complete stroke
+ inside the rounded box prevents clipped corner pixels from reading heavier. */
.cardWorkspaceTrigger {
--dsw-elevation-stroke-color: transparent;
cursor: pointer;
@@ -90,12 +87,12 @@
.cardWorkspaceTrigger::after {
content: '';
position: absolute;
- inset: -1px;
+ inset: 0;
+ box-sizing: border-box;
+ border: 1px dashed currentColor;
border-radius: 22px;
- background: var(--dsw-alias-border-l4);
- transition: background-color 100ms ease;
- -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25' height='100%25' fill='none' rx='22' ry='22' stroke='black' stroke-width='2' stroke-dasharray='4 4'/%3E%3C/svg%3E");
- mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25' height='100%25' fill='none' rx='22' ry='22' stroke='black' stroke-width='2' stroke-dasharray='4 4'/%3E%3C/svg%3E");
+ color: var(--dsw-alias-border-l4);
+ transition: color 100ms ease;
pointer-events: none;
}
@@ -107,7 +104,7 @@
}
.cardWorkspaceTrigger:hover::after {
- background: var(--dsw-alias-state-business-primary);
+ color: var(--dsw-alias-state-business-primary);
}
.accessory {
diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
index 319628d266..6a4ea12740 100644
--- a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
+++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
@@ -17,7 +17,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { ChangeEvent, KeyboardEvent, MouseEvent } from 'react'
import clsx from 'clsx'
import {
- IconPlusOutline16, IconWarningOutline16, Toast, Tooltip,
+ IconPlusOutlineMedium, IconWarningOutlineRegular, Toast, Tooltip,
} from '@deepseek-ai/dsh-client-ui-primitives'
// Type-only: the `plan` projection key merge (the TodoDock posture — the
// composer reads a host-computed value; the domain owns the key).
@@ -347,7 +347,7 @@ export const InputBar = memo(function InputBar({
}
+ icon={}
anchor={cardRef.current}
onDone={dismissToast}
/>
@@ -418,7 +418,7 @@ export const InputBar = memo(function InputBar({
onMouseDown={keepFocus}
onClick={onToggleCommandMenu}
>
-
+
-
-
-
- )
-}
-
-/** In-progress: business-blue ring fading out; CSS spins the svg. */
-function ProgressGlyph() {
- const gradientId = useId()
- return (
-
- )
-}
-
-/** Pending: dashed unstarted ring (figma dash 2.4 2.4). */
-function PendingGlyph() {
- return (
-
- )
-}
-
-function StatusGlyph({ status }: { status: TodoItem['status'] }) {
+/** Map Todo lifecycle state onto the shared compact status language. */
+function statusDotState(status: TodoItem['status']): StateDotState {
switch (status) {
- case 'completed': return
- case 'in_progress': return
- case 'pending': return
+ case 'completed': return 'done'
+ case 'in_progress': return 'ongoing'
+ case 'pending': return 'idle'
+ /* v8 ignore next -- closed TodoItem status union */
+ default: return assertNever(status)
+ }
+}
+
+/** Return the localized status announced beside one decorative marker. */
+function statusLabel(status: TodoItem['status'], t: TodoPanelProps['t']): string {
+ switch (status) {
+ case 'completed': return t('todo.status.completed')
+ case 'in_progress': return t('todo.status.inProgress')
+ case 'pending': return t('todo.status.pending')
/* v8 ignore next -- closed TodoItem status union */
default: return assertNever(status)
}
@@ -98,18 +75,20 @@ export function TodoPanel({ todos, t }: TodoPanelProps) {
aria-expanded={!collapsed}
onClick={() => { setCollapsed(v => !v) }}
>
-
+ {t('todo.title')}{progressLabel(todos, t)}
- {collapsed ? : }
+ {collapsed ? : }
{!collapsed && (
{todos.map(item => (
-
+
+
+ {item.content}
))}
diff --git a/packages/client/ui-conversation/tests/todo-panel.client.spec.tsx b/packages/client/ui-conversation/tests/todo-panel.client.spec.tsx
index e79081b003..248c3cd92f 100644
--- a/packages/client/ui-conversation/tests/todo-panel.client.spec.tsx
+++ b/packages/client/ui-conversation/tests/todo-panel.client.spec.tsx
@@ -64,8 +64,10 @@ describe('TodoPanel', () => {
expect(items.map(li => li.getAttribute('data-status'))).toEqual(['completed', 'in_progress', 'pending'])
expect(screen.getByText('搭骨架')).toBeTruthy()
expect(screen.getByText('写组件')).toBeTruthy()
- // Each status row carries an SVG glyph (not a text bullet).
- expect(items.every(li => li.querySelector('svg') !== null)).toBe(true)
+ expect(items.map(li => li.querySelector('[data-state]')?.getAttribute('data-state')))
+ .toEqual(['done', 'ongoing', 'idle'])
+ expect(screen.getAllByRole('img').map(node => node.getAttribute('aria-label')))
+ .toEqual(['已完成', '进行中', '待处理'])
})
it('collapse hides an expanded list; expand restores; header keeps the count summary', () => {
diff --git a/packages/client/ui-deliverables/README.i18n.yaml b/packages/client/ui-deliverables/README.i18n.yaml
index 041ec3fa6a..4544ccd159 100644
--- a/packages/client/ui-deliverables/README.i18n.yaml
+++ b/packages/client/ui-deliverables/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-deliverables/README.md
-README.md: 46e289ddb572928baaee7ac1a370087fd789aebe
-README.zh.md: 804ee5c7613510df96ac48cfe08f9b0fd39f7bbc
+README.md: 8a2da4e763d3682dcf45c4a6f9529804c5d1bcc8
+README.zh.md: 7af5571e0fa5aeda323424fd54df9fa7e3878365
diff --git a/packages/client/ui-deliverables/README.md b/packages/client/ui-deliverables/README.md
index 46e289ddb5..8a2da4e763 100644
--- a/packages/client/ui-deliverables/README.md
+++ b/packages/client/ui-deliverables/README.md
@@ -32,7 +32,7 @@ Mount this plugin alongside `ui-conversation` and the Host [workspace-changes](.
The Web `standard`, `ptc`, and `cordis` presets expose `present` for final files accessible through the Session filesystem, including files created through Bash. Call it with `files: [{ path, description? }]` after creating the files. The [present tool](../../deliverables/tool-present/README.md) owns file-count limits and Session declarations. The closing turn shows one delivery as a full-width card and multiple deliveries in a two-column grid with 10px gaps. A list longer than four files starts collapsed and provides a control that reveals or hides the complete list. Each 60px-high card uses 8px vertical and 10px horizontal inset spacing, a 20px shared `FileTypeIcon` in a 40px frame, 13px filename text, 10px secondary text, and a 12px Open action. It shows the basename and description, or the file type when no description exists; a trailing parenthesized suffix in the description is omitted, and hovering the card replaces that line with the Sidebar-preview action. Clicking the card or the left side of its split Open control previews the file in the right Sidebar. The chevron opens the standard menu for the Host default application plus Show in Finder on macOS, Show in File Explorer on Windows and WSL, or Open containing folder through the default Linux file manager. Matching inline-code references preview the same source files in the right Sidebar; native opening requires an explicit card-menu action. Repeated declaration of a path selects its latest description before the closing reply.
-The `present` tool row shows running, delivered, failed, or interrupted status; expanding a settled row reveals its recorded result. The collapsible card grid retains every delivered file. Both menu actions share pending state and show progress, acknowledgement, or an action-specific retryable error. Desktop information is read when delivery cards appear and invalidated on connection replacement; responses from a replaced connection cannot publish metadata. Selecting a native menu action returns keyboard focus to the available Sidebar Open button. Pending actions close the menu until another explicit gesture. A missing desktop disables the Open menu; a failed desktop-information read offers Retry. It requires a desktop and a suitable default application on the serving Host; a remote browser does not open applications on its own device.
+The `present` tool row retains the delivery-document glyph while showing running, delivered, failed, or interrupted status; expanding a settled row reveals its recorded result. The collapsible card grid retains every delivered file. Both menu actions share pending state and show progress, acknowledgement, or an action-specific retryable error. Desktop information is read when delivery cards appear and invalidated on connection replacement; responses from a replaced connection cannot publish metadata. Selecting a native menu action returns keyboard focus to the available Sidebar Open button. Pending actions close the menu until another explicit gesture. A missing desktop disables the Open menu; a failed desktop-information read offers Retry. It requires a desktop and a suitable default application on the serving Host; a remote browser does not open applications on its own device.
### The changed-files card
diff --git a/packages/client/ui-deliverables/README.zh.md b/packages/client/ui-deliverables/README.zh.md
index 804ee5c761..7af5571e0f 100644
--- a/packages/client/ui-deliverables/README.zh.md
+++ b/packages/client/ui-deliverables/README.zh.md
@@ -32,7 +32,7 @@ kind: "package-reference"
Web 的 `standard`、`ptc` 与 `cordis` preset 提供 `present` 用于声明交付会话文件系统可访问的最终文件,包括通过 Bash 创建的文件。创建文件后,以 `files: [{ path, description? }]` 调用。[present 工具](../../deliverables/tool-present/README.zh.md)拥有文件数量限制和会话声明。收尾轮次把单个交付显示为横向占满内容区的卡片,把多个交付显示为间距 10px 的双列网格。文件超过四个时,列表默认收起,并提供显示或隐藏完整列表的控件。每张卡片高 60px,上下内边距为 8px、左右为 10px;40px 图标框内使用 20px 的共享 `FileTypeIcon`,文件名为 13px、次要文本为 10px,“打开”操作为 12px。卡片显示 basename 与说明;没有说明时显示文件类型,说明末尾的括号后缀会被省略,悬停卡片时该行切换为侧栏预览提示。点击卡片或分段“打开”控件的左侧会在右侧 Sidebar 中预览文件;右侧箭头打开标准菜单,其中提供 Host 默认应用,以及 macOS 上的“在 Finder 中显示”、Windows 和 WSL 上的“在文件资源管理器中显示”或 Linux 默认文件管理器的“打开所在文件夹”。匹配的行内代码引用在右侧 Sidebar 中预览相同源文件;原生打开需要显式选择卡片菜单中的操作。同一路径重复声明时,选择收尾回复之前最近一次的说明。
-`present` 工具行显示正在交付、已交付、失败或中断状态;展开已结束的调用可查看其记录的结果。可折叠卡片网格保留全部交付文件。菜单中的两个操作共享等待状态,并显示进度、成功确认或各自可重试的错误。交付卡片出现时读取桌面信息,连接更换时清除缓存,旧连接的响应不能更新元数据。选择原生菜单操作后,键盘焦点回到仍可用的侧边栏“打开”按钮。等待操作完成时关闭菜单,用户再次点击才会打开。Host 没有桌面时禁用“打开”菜单;桌面信息读取失败时提供“重试”。服务 Host 必须具备桌面和合适的默认应用;远程浏览器不会打开其所在设备上的应用。
+`present` 工具行在正在交付、已交付、失败或中断状态下都保留交付文档图标;展开已结束的调用可查看其记录的结果。可折叠卡片网格保留全部交付文件。菜单中的两个操作共享等待状态,并显示进度、成功确认或各自可重试的错误。交付卡片出现时读取桌面信息,连接更换时清除缓存,旧连接的响应不能更新元数据。选择原生菜单操作后,键盘焦点回到仍可用的侧边栏“打开”按钮。等待操作完成时关闭菜单,用户再次点击才会打开。Host 没有桌面时禁用“打开”菜单;桌面信息读取失败时提供“重试”。服务 Host 必须具备桌面和合适的默认应用;远程浏览器不会打开其所在设备上的应用。
### 改动文件卡片
diff --git a/packages/client/ui-deliverables/package.json b/packages/client/ui-deliverables/package.json
index 28ed49b7fe..707f7728ad 100644
--- a/packages/client/ui-deliverables/package.json
+++ b/packages/client/ui-deliverables/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-deliverables",
"description": "Changed-files card with per-file comparison tabs, delivery cards, and clickable final-response file references for Web",
- "version": "0.1.6-alpha.1",
+ "version": "0.1.6-alpha.2",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-deliverables/src/client/ChangedFiles.tsx b/packages/client/ui-deliverables/src/client/ChangedFiles.tsx
index cb0d5c1a05..6fd4e72ace 100644
--- a/packages/client/ui-deliverables/src/client/ChangedFiles.tsx
+++ b/packages/client/ui-deliverables/src/client/ChangedFiles.tsx
@@ -1,7 +1,7 @@
/** The changed-files card: a header and per-file rows that open the turn's review, and a three-row fold. */
import { useState } from 'react'
import { resolveWorkspacePath } from '@deepseek-ai/dsh-util-workspace-path'
-import { IconChevronDownOutline14, IconChevronUpOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
+import { IconChevronDownOutlineRegular, IconChevronUpOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
import type { ChangesSummary } from '../changes.ts'
import { IconCodeBracketsOutline16 } from './icons.tsx'
@@ -66,7 +66,7 @@ export function ChangedFiles({ changes, cwd, openReview, t }: {
aria-label={t(expanded ? 'changes.collapseAria' : 'changes.expandAria', { count: String(changes.files.length) })}
onClick={() => { setExpanded(value => !value) }}>
{t(expanded ? 'changes.collapse' : 'changes.all', { count: String(changes.files.length) })}
- {expanded ? : }
+ {expanded ? : }
}
}
diff --git a/packages/client/ui-deliverables/src/client/Deliverables.tsx b/packages/client/ui-deliverables/src/client/Deliverables.tsx
index 112bc68070..e463734319 100644
--- a/packages/client/ui-deliverables/src/client/Deliverables.tsx
+++ b/packages/client/ui-deliverables/src/client/Deliverables.tsx
@@ -1,7 +1,7 @@
/** The changed-files card, shown only while the Host serves the turn's summary, and explicitly declared files for a closing turn. */
import { useEffect, useState } from 'react'
import type { TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client'
-import { Button, IconChevronDownOutline14, IconChevronUpOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
+import { Button, IconChevronDownOutlineRegular, IconChevronUpOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import type { GlobalStandardProps, InjectFace, PropsLocale, PropsRuntime, SessionStandardProps } from '@deepseek-ai/dsh-client-ui-slots'
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
import type { PresentedOpenController } from './present-open.ts'
@@ -110,7 +110,7 @@ export function Deliverables({
aria-label={t(expanded ? 'presented.collapseAria' : 'presented.expandAria', { count: matched.presented.length })}
onClick={() => { setExpanded(value => !value) }}>
{t(expanded ? 'presented.collapse' : 'presented.all', { count: matched.presented.length })}
- {expanded ? : }
+ {expanded ? : }
}
}
>
diff --git a/packages/client/ui-deliverables/src/client/PresentRow.tsx b/packages/client/ui-deliverables/src/client/PresentRow.tsx
index 845d2b4dcc..04c37677fd 100644
--- a/packages/client/ui-deliverables/src/client/PresentRow.tsx
+++ b/packages/client/ui-deliverables/src/client/PresentRow.tsx
@@ -1,6 +1,6 @@
/** Present call status and expandable durable result text. */
import { useState } from 'react'
-import { DisclosureRow, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
+import { DisclosureRow, IconDeliverDocRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client'
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
import type { NS } from './locales.ts'
@@ -34,7 +34,7 @@ export function PresentRow({ block, inspect, t }: PresentRowProps) {
const [expanded, setExpanded] = useState(false)
return
diff --git a/packages/client/ui-input-trigger/package.json b/packages/client/ui-input-trigger/package.json
index 82b67bce9f..d2a3fac7ee 100644
--- a/packages/client/ui-input-trigger/package.json
+++ b/packages/client/ui-input-trigger/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-input-trigger",
"description": "Input trigger pipeline: '/' and '@' detection, candidate menu, pick routing to registered sources",
- "version": "0.1.6-alpha.1",
+ "version": "0.1.6-alpha.2",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-input-trigger/src/client/MenuView.module.css b/packages/client/ui-input-trigger/src/client/MenuView.module.css
index cb6ff3ec3a..7b0a2818e4 100644
--- a/packages/client/ui-input-trigger/src/client/MenuView.module.css
+++ b/packages/client/ui-input-trigger/src/client/MenuView.module.css
@@ -15,17 +15,18 @@
(see ui-theme styles/scrollbar.css for the rebinding contract). */
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
- /* A 10px draggable rail carries a quiet 4px thumb, inset from the card's
+ /* A 6px draggable rail carries a quiet 2px thumb, inset from the card's
rounded ends. ui-theme's global scrollbar sheet renders these values. */
- --dsh-scrollbar-width: 10px;
- --dsh-scrollbar-thumb-border: 3px;
+ --dsh-scrollbar-width: 6px;
+ --dsh-scrollbar-thumb-border: 2px;
--dsh-scrollbar-track-margin: 12px;
- padding: 4px;
+ padding: 3px;
display: flex;
flex-direction: column;
border: 0;
- border-radius: 20px;
+ border-radius: 16px;
background: var(--dsw-specific-menu);
+ backdrop-filter: var(--dsw-menu-backdrop-filter);
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
box-shadow: var(--dsw-elevation-prominent);
}
@@ -53,16 +54,16 @@
.item {
display: flex;
align-items: center;
- gap: 8px;
+ gap: 6px;
width: 100%;
- min-height: 40px;
- padding: 8px 10px;
+ min-height: 34px;
+ padding: 6px 8px;
border: none;
- border-radius: 10px;
+ border-radius: 8px;
background: transparent;
cursor: pointer;
- font-size: 14px;
- line-height: 22px;
+ font-size: 13px;
+ line-height: 20px;
color: var(--dsw-alias-label-primary);
text-align: left;
}
@@ -75,28 +76,33 @@
.sectionTitle {
flex: none;
- min-height: 26px;
- padding: 6px 10px 2px;
+ min-height: 23px;
+ padding: 5px 8px 2px;
color: var(--dsw-alias-label-tertiary);
- font-size: 12px;
+ font-size: 11px;
font-weight: 500;
- line-height: 18px;
+ line-height: 16px;
}
.sectionTitle:not(:first-child) {
- margin-top: 4px;
+ margin-top: 3px;
}
.itemIcon {
display: inline-flex;
flex: none;
- width: 16px;
- height: 16px;
+ width: 14px;
+ height: 14px;
align-items: center;
justify-content: center;
color: var(--dsw-alias-label-tertiary);
}
+.itemIcon svg {
+ width: 14px;
+ height: 14px;
+}
+
.itemName {
flex: none;
max-width: 40%;
@@ -114,6 +120,8 @@
text-overflow: ellipsis;
white-space: nowrap;
color: var(--dsw-alias-label-tertiary);
+ font-size: 12px;
+ line-height: 18px;
}
/* Right-aligned so every description ends at the same edge and the titles
@@ -126,6 +134,8 @@
white-space: nowrap;
text-align: right;
color: var(--dsw-alias-label-tertiary);
+ font-size: 12px;
+ line-height: 18px;
}
/* Trailing seat on drillable rows (directories): the Tab keycap hint plus the
@@ -134,7 +144,7 @@
flex: none;
display: inline-flex;
align-items: center;
- gap: 4px;
+ gap: 3px;
margin-left: auto;
}
@@ -144,20 +154,20 @@
.drillHintText {
display: none;
color: var(--dsw-alias-label-caption);
- font-size: 11px;
- line-height: 18px;
+ font-size: 10px;
+ line-height: 16px;
white-space: nowrap;
}
.drillHint {
display: none;
- padding: 0 5px;
+ padding: 0 4px;
border-radius: 4px;
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-caption);
font-family: inherit;
- font-size: 11px;
- line-height: 18px;
+ font-size: 10px;
+ line-height: 16px;
}
.item.active .drillHintText,
@@ -171,8 +181,8 @@
flex: none;
display: inline-grid;
place-items: center;
- width: 20px;
- height: 20px;
+ width: 18px;
+ height: 18px;
border-radius: 4px;
/* label-caption, the quiet tint the composer's access-mode chevron uses. */
color: var(--dsw-alias-label-caption);
@@ -186,9 +196,9 @@
/* Heading row above a source group: non-interactive small grey text,
* padding aligned with items (mirrors ui-primitives Menu .label). */
.groupTitle {
- padding: 8px 10px;
- font-size: 12px;
- line-height: 16px;
+ padding: 6px 8px;
+ font-size: 11px;
+ line-height: 15px;
color: var(--dsw-alias-label-tertiary);
}
@@ -198,15 +208,14 @@
.skeletonRow {
display: flex;
align-items: center;
- /* border-box so min-height matches the .item buttons, whose UA default
- box-sizing already counts the same 8px paddings inside their 40px. */
+ /* border-box keeps the skeleton row aligned with the compact item buttons. */
box-sizing: border-box;
- min-height: 40px;
- padding: 8px 10px;
+ min-height: 34px;
+ padding: 6px 8px;
}
.skeletonBar {
- height: 20px;
+ height: 18px;
border-radius: 4px;
background: var(--dsw-alias-bg-skeleton);
animation: dsh-menu-skeleton 2s cubic-bezier(0.36, 0, 0.64, 1) infinite;
@@ -219,16 +228,15 @@
}
/* Breadcrumb header of a drilled source: pinned outside .viewport so the
- descent stays reversible while the candidate list scrolls under it.
- 4px sides + the crumb's own 6px = the first crumb's text sits at 10px,
- flush with .sectionTitle and the item icons. */
+ descent stays reversible while the candidate list scrolls under it. The
+ compact shell and crumb padding align its text with item content. */
.crumbs {
flex: none;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 2px;
- padding: 4px 4px 6px;
+ padding: 3px 3px 5px;
margin-bottom: 2px;
/* l1, not border-inverted (the menu frame's dark-only stroke): the divider
needs to survive light mode too; in dark both resolve to white/0.06. */
@@ -240,15 +248,15 @@
flex: 0 1 auto;
max-width: 40%;
overflow: hidden;
- padding: 2px 6px;
+ padding: 2px 5px;
border: none;
border-radius: 6px;
background: transparent;
color: var(--dsw-alias-label-tertiary);
cursor: pointer;
font-family: inherit;
- font-size: 12px;
- line-height: 18px;
+ font-size: 11px;
+ line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
diff --git a/packages/client/ui-input-trigger/src/client/MenuView.tsx b/packages/client/ui-input-trigger/src/client/MenuView.tsx
index fc2bdc7cd1..a6395aff8b 100644
--- a/packages/client/ui-input-trigger/src/client/MenuView.tsx
+++ b/packages/client/ui-input-trigger/src/client/MenuView.tsx
@@ -14,7 +14,7 @@
*/
import { Fragment, useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react'
import clsx from 'clsx'
-import { IconChevronRightOutline14, ReferenceIcon, useAnchoredMaxHeight } from '@deepseek-ai/dsh-client-ui-primitives'
+import { IconChevronRightOutlineRegular, ReferenceIconRegular, useAnchoredMaxHeight } from '@deepseek-ai/dsh-client-ui-primitives'
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
import css from './MenuView.module.css'
import type { MenuViewInjected } from './slots.ts'
@@ -99,7 +99,7 @@ export function MenuView({ menu, headers, onPick, onCrumb, onHover, onDismiss, t
)}
diff --git a/packages/client/ui-open-in-app/tests/open-in-app-action.client.spec.tsx b/packages/client/ui-open-in-app/tests/open-in-app-action.client.spec.tsx
index 42d6ee0a02..bf8f6061f6 100644
--- a/packages/client/ui-open-in-app/tests/open-in-app-action.client.spec.tsx
+++ b/packages/client/ui-open-in-app/tests/open-in-app-action.client.spec.tsx
@@ -238,7 +238,7 @@ describe('OpenInAppAction launching', () => {
if (img !== null) fireEvent.error(img)
await waitFor(() => {
expect(container.querySelector('img')).toBeNull()
- expect(container.querySelector('svg rect')).not.toBeNull()
+ expect(container.querySelector('svg path[fill-opacity="0.05"]')).not.toBeNull()
})
})
})
diff --git a/packages/client/ui-permission-presets/package.json b/packages/client/ui-permission-presets/package.json
index b868fdb882..69879368f4 100644
--- a/packages/client/ui-permission-presets/package.json
+++ b/packages/client/ui-permission-presets/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-permission-presets",
"description": "Permission surfaces: a new-session default in General settings and a current-session /permission popup over the permissions projection",
- "version": "0.1.6-alpha.1",
+ "version": "0.1.6-alpha.2",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-permission-presets/src/client/PermissionRow.tsx b/packages/client/ui-permission-presets/src/client/PermissionRow.tsx
index ac0913f6d6..2136974522 100644
--- a/packages/client/ui-permission-presets/src/client/PermissionRow.tsx
+++ b/packages/client/ui-permission-presets/src/client/PermissionRow.tsx
@@ -8,7 +8,7 @@ import { useEffect, useState } from 'react'
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import {
- IconChevronDownOutline14, Menu, RiskConfirmation,
+ IconChevronDownOutlineRegular, Menu, RiskConfirmation,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { PermissionSettingsState } from './settings-store.ts'
import type { PermissionSettingsKey } from './locales.ts'
@@ -97,7 +97,7 @@ export function PermissionRow({ load, select, usePermission, t }: PermissionRowP
onClick={() => { setOpen(value => !value) }}
>
{label}
-
+
)}
/>
diff --git a/packages/client/ui-permission-presets/src/client/PermissionSelect.tsx b/packages/client/ui-permission-presets/src/client/PermissionSelect.tsx
index 1736fd86a4..f34e2b16e9 100644
--- a/packages/client/ui-permission-presets/src/client/PermissionSelect.tsx
+++ b/packages/client/ui-permission-presets/src/client/PermissionSelect.tsx
@@ -2,7 +2,8 @@ import { useEffect, useState } from 'react'
import type { ReactNode } from 'react'
import clsx from 'clsx'
import {
- IconChevronDownOutline14, Menu, RiskConfirmation, SHIELD_OUTLINE_PATH, SHIELD_OUTLINE_STROKE,
+ IconChevronDownOutlineRegular, Menu, PermissionIconFullAccessRegular,
+ PermissionIconReadOnlyRegular, PermissionIconWorkspaceWriteRegular, RiskConfirmation,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { MenuEntry } from '@deepseek-ai/dsh-client-ui-primitives'
import type {
@@ -21,34 +22,13 @@ import {
} from './presentation.ts'
import css from './PermissionSelect.module.css'
-/* Shield glyphs (design set 1556) over the ui-primitives shield contour:
- check = read-only, pencil = workspace write, exclamation = full access.
- currentColor so the trigger and menu rows tint them with their own text
- color. */
+/* Permission glyphs follow currentColor so the trigger and menu rows tint the
+ shared product artwork with their own text color. */
const permissionGlyphs = new Map([
- ['read-only', (
-
- )],
- ['workspace-write', (
-
- )],
- [FULL_ACCESS, (
-
- )],
+ ['read-only', ],
+ ['workspace-write', ],
+ [FULL_ACCESS, ],
])
/** Glyph for a permission option value; host-configured names outside the design set get none. */
@@ -209,7 +189,7 @@ export function PermissionSelect({
{currentBadge}
)}
-
+
}
diff --git a/packages/client/ui-plan/package.json b/packages/client/ui-plan/package.json
index 1a77508ca4..a9122c3f23 100644
--- a/packages/client/ui-plan/package.json
+++ b/packages/client/ui-plan/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-plan",
"description": "Plan mode controls, persistent transcript plan cards, and sidebar Markdown previews",
- "version": "0.1.6-alpha.1",
+ "version": "0.1.6-alpha.2",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-plan/src/client/PlanCard.tsx b/packages/client/ui-plan/src/client/PlanCard.tsx
index 74e65cdefe..3b2e03951b 100644
--- a/packages/client/ui-plan/src/client/PlanCard.tsx
+++ b/packages/client/ui-plan/src/client/PlanCard.tsx
@@ -1,6 +1,6 @@
/** Persistent transcript cards and pending-review sidebar navigation. */
import { useEffect } from 'react'
-import { FileTypeIcon, IconChevronRightOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
+import { FileTypeIcon, IconChevronRightOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import type { ChatNode } from '@deepseek-ai/dsh-client-ui-chat/client'
import { shallowEqual } from '@deepseek-ai/dsh-client-store'
import type {} from './plan-definition.ts'
@@ -64,5 +64,5 @@ export function PlanReviewOpen({ review, requestKey, openReview, t, useStore, ac
actions.markOpened(identity)
}, [identity, opened, openReview, review, requestKey, actions])
return
+ onClick={() => { openReview(review, requestKey) }}>{t('preview.full')}
}
diff --git a/packages/client/ui-plan/src/client/PlanModeControl.tsx b/packages/client/ui-plan/src/client/PlanModeControl.tsx
index f5494f106c..8cbd71d878 100644
--- a/packages/client/ui-plan/src/client/PlanModeControl.tsx
+++ b/packages/client/ui-plan/src/client/PlanModeControl.tsx
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react'
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
-import { IconCloseFill14 } from '@deepseek-ai/dsh-client-ui-primitives'
+import { IconCloseFillRegular } from '@deepseek-ai/dsh-client-ui-primitives'
// Type-only: pulls the ui-conversation SlotMap merge (the input.plan seat and
// its {locked} owner share).
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
@@ -60,7 +60,7 @@ export function PlanChip({ useProjection, locked, exitPlanMode, t }: PlanChipPro
>
{t('chip.label')}
-
+
{error !== null && {t('chip.exitFailed')}}
diff --git a/packages/client/ui-plan/src/client/PlanPreview.tsx b/packages/client/ui-plan/src/client/PlanPreview.tsx
index b417507c77..141dcf8e3c 100644
--- a/packages/client/ui-plan/src/client/PlanPreview.tsx
+++ b/packages/client/ui-plan/src/client/PlanPreview.tsx
@@ -1,6 +1,6 @@
/** Read-only Markdown viewer for logged plans and temporary review documents. */
import { useMemo } from 'react'
-import { IconCopyOutline16, IconPlanOutline14, MarkdownText, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives'
+import { IconCopyOutlineRegular, IconPlanOutlineRegular, MarkdownText, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives'
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client'
import type {} from './plan-resource.ts'
@@ -35,7 +35,7 @@ export function PlanPreview({ useTabInfo, useResource, t }: PlanPreviewProps) {
return (
+ onClick={() => { void writeClipboard(plan.markdown) }}>
)
@@ -52,5 +52,5 @@ export function PlanTitle({ useTabInfo, useResource }: PropsRuntime<'sidebar.rig
const params = tab.tab.navigation.params
const plan = isReviewPreviewAddress(tab.tab.navigation.address)
? (params !== undefined && 'planReview' in params ? params.planReview : undefined) : resource.value
- return <>{plan?.title ?? tab.tab.title}>
+ return <>{plan?.title ?? tab.tab.title}>
}
diff --git a/packages/client/ui-plugin-manager/README.i18n.yaml b/packages/client/ui-plugin-manager/README.i18n.yaml
index e50cdb00de..dd15598bf9 100644
--- a/packages/client/ui-plugin-manager/README.i18n.yaml
+++ b/packages/client/ui-plugin-manager/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-plugin-manager/README.md
-README.md: e7128dd2146cc0553ab36fdef5c31470be7877f2
-README.zh.md: ff7706f1d9747f553bf91f45a76fa5f50472330d
+README.md: 73209cb946567abdae88e41debdaab996269f749
+README.zh.md: 8dabeb8f12d463051189cd93a9e66a2bc4cfe70b
diff --git a/packages/client/ui-plugin-manager/README.md b/packages/client/ui-plugin-manager/README.md
index e7128dd214..73209cb946 100644
--- a/packages/client/ui-plugin-manager/README.md
+++ b/packages/client/ui-plugin-manager/README.md
@@ -41,7 +41,7 @@ A bundle's page shows its full package name under the title, the spec that insta
### Switching one row of a bundle
-A row's switch on the bundle's page calls `pluginManager.setPluginEnabled`, which writes the row's `disabled` override into the profile's `cordis.patch.yml`. The tree recomposes at once on a profile with HMR, so the row's host half unmounts or mounts while the rest of the bundle keeps running, and the page follows the client module graph without reloading. Rows show their fiber phase as the Host runs them. The switch appears only on a bundle that is on; a row without a live entry, or one the Host will not address through the profile patch, is locked with the Host's reason. A list longer than ten rows gets a filter over the row ids.
+A row's switch on the bundle's page calls `pluginManager.setPluginEnabled`, which writes the row's `disabled` override into the profile's `cordis.patch.yml`. The tree recomposes at once on a profile with HMR, so the row's host half unmounts or mounts while the rest of the bundle keeps running, and the page follows the client module graph without reloading. Rows use the shared status marker for their Host fiber phase: pending and disabled are idle, loading and unloading are ongoing, active is done, and failed is error. The switch appears only on a bundle that is on; a row without a live entry, or one the Host will not address through the profile patch, is locked with the Host's reason. A list longer than ten rows gets a filter over the row ids.
### Configuration pages
@@ -73,7 +73,7 @@ The browser plugin registers the `plugins` sidebar entry and its `main` panel th
### The store
-`PluginManagerController` owns the bundle views, busy keys, notices, install progress and the uninstall confirmation. Each read asks the inventory whether the Host manages a profile, then joins `listBundles` with `listPlugins` into one view per bundle, whose rows carry the live entry's enablement and fiber phase. It coalesces overlapping reads, refreshes after operations, on `plugin-manager/changed`, and on reconnect, and ignores late results after disposal. Install output is grouped by job id. The install dialog moves `idle → checking → starting → running → done | failed`, with `cancelling` and `applying` as the Host reports them. The check runs under an `AbortController` that going back or closing aborts, and its settlement is dropped; a run is stopped only through `pluginManager.cancelInstall`, whose answer the dialog waits for. A change the Host could not apply, a restart it waits for, and an override by a higher layer become toasts that retire on their own.
+`PluginManagerController` owns the bundle views, busy keys, notices, install progress and the uninstall confirmation. Each read asks the inventory whether the Host manages a profile, then joins `listBundles` with `listPlugins` into one view per bundle, whose rows carry the live entry's enablement and fiber phase. It coalesces overlapping reads, refreshes after operations, on `plugin-manager/changed`, and on reconnect, and ignores late results after disposal. Install output is grouped by job id. The install dialog moves `idle → checking → starting → running → done | failed`, with `cancelling` and `applying` as the Host reports them; checking and every active phase use ongoing, while the final screens use done or error. The check runs under an `AbortController` that going back or closing aborts, and its settlement is dropped; a run is stopped only through `pluginManager.cancelInstall`, whose answer the dialog waits for. A change the Host could not apply, a restart it waits for, and an override by a higher layer become toasts that retire on their own.
### Configuration slots
diff --git a/packages/client/ui-plugin-manager/README.zh.md b/packages/client/ui-plugin-manager/README.zh.md
index ff7706f1d9..8dabeb8f12 100644
--- a/packages/client/ui-plugin-manager/README.zh.md
+++ b/packages/client/ui-plugin-manager/README.zh.md
@@ -41,7 +41,7 @@ Agent Teams、Agent Teams Web UI 和 Auto Authorization Review 三个包使用
### 切换组合包里的一行
-组合包页面上行的开关调用 `pluginManager.setPluginEnabled`,往 profile 的 `cordis.patch.yml` 写入该行的 `disabled` 覆盖。启用了 HMR 的 profile 的树随即重组,该行的宿主半区卸下或挂上,组合包其余部分照常运行,页面无需重载即跟随客户端模块图。行按 Host 运行它们的 fiber 阶段显示状态。开关只出现在已打开的组合包上;没有存活条目的行,以及 Host 不通过 profile patch 寻址的行,带着 Host 的原因锁定。超过十行的列表带一个按行 id 筛选的输入框。
+组合包页面上行的开关调用 `pluginManager.setPluginEnabled`,往 profile 的 `cordis.patch.yml` 写入该行的 `disabled` 覆盖。启用了 HMR 的 profile 的树随即重组,该行的宿主半区卸下或挂上,组合包其余部分照常运行,页面无需重载即跟随客户端模块图。行使用共享状态标记表示 Host fiber 阶段:pending 与 disabled 为 idle,loading 与 unloading 为 ongoing,active 为 done,failed 为 error。开关只出现在已打开的组合包上;没有存活条目的行,以及 Host 不通过 profile patch 寻址的行,带着 Host 的原因锁定。超过十行的列表带一个按行 id 筛选的输入框。
### 配置页
@@ -73,7 +73,7 @@ ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
### store
-`PluginManagerController` 拥有组合包视图、忙碌键、提示、安装进度和卸载确认。每次读取先问清单 Host 是否管理着 profile,再把 `listBundles` 与 `listPlugins` 合成每个组合包一份视图,其行携带存活条目的启停状态与 fiber 阶段。它合并重叠读取,在操作后、收到 `plugin-manager/changed` 时以及重连后刷新,并在销毁后忽略晚到结果。安装输出按 job id 分组。安装对话框沿 `idle → checking → starting → running → done | failed` 推进,`cancelling` 与 `applying` 按 Host 的报告呈现。检查在一个 `AbortController` 下运行,返回编辑或关闭会中止它并丢弃其结果;运行只能通过 `pluginManager.cancelInstall` 停止,对话框等待其答复。Host 无法应用的变更、要等重启的变更、被更高层覆盖的变更,都是会自行消失的 toast。
+`PluginManagerController` 拥有组合包视图、忙碌键、提示、安装进度和卸载确认。每次读取先问清单 Host 是否管理着 profile,再把 `listBundles` 与 `listPlugins` 合成每个组合包一份视图,其行携带存活条目的启停状态与 fiber 阶段。它合并重叠读取,在操作后、收到 `plugin-manager/changed` 时以及重连后刷新,并在销毁后忽略晚到结果。安装输出按 job id 分组。安装对话框沿 `idle → checking → starting → running → done | failed` 推进,`cancelling` 与 `applying` 按 Host 的报告呈现;checking 与所有活动阶段使用 ongoing,最终页面使用 done 或 error。检查在一个 `AbortController` 下运行,返回编辑或关闭会中止它并丢弃其结果;运行只能通过 `pluginManager.cancelInstall` 停止,对话框等待其答复。Host 无法应用的变更、要等重启的变更、被更高层覆盖的变更,都是会自行消失的 toast。
### 配置 slot
diff --git a/packages/client/ui-plugin-manager/package.json b/packages/client/ui-plugin-manager/package.json
index 727cad9599..a999a0c537 100644
--- a/packages/client/ui-plugin-manager/package.json
+++ b/packages/client/ui-plugin-manager/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-plugin-manager",
"description": "Plugin management for the dsh web client: the sidebar Plugins panel installs, enables, disables, retries, and composes installed plugin packages",
- "version": "0.1.6-alpha.1",
+ "version": "0.1.6-alpha.2",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css
index 2971a42af9..38875cb7c6 100644
--- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css
+++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css
@@ -61,6 +61,12 @@
color: var(--dsw-alias-state-error-primary);
}
+.statusWithDot {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
.banner {
margin: 0;
padding: 8px 12px;
@@ -645,14 +651,6 @@
color: var(--dsw-alias-label-secondary);
}
-.wizardIcon[data-tone='done'] {
- color: var(--dsw-alias-state-success-primary);
-}
-
-.wizardIcon[data-tone='failed'] {
- color: var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-business-primary));
-}
-
.wizardTitle {
margin: 0;
font-size: 18px;
@@ -731,39 +729,7 @@
min-width: 0;
}
-.spinnerLarge {
- width: 28px;
- height: 28px;
- border: 3px solid color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);
- border-top-color: var(--dsw-alias-label-primary);
- border-radius: 50%;
- corner-shape: round;
- animation: spin 900ms linear infinite;
-}
-
-.spinner {
- width: 14px;
- height: 14px;
- flex: none;
- border: 2px solid color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);
- border-top-color: var(--dsw-alias-label-primary);
- border-radius: 50%;
- corner-shape: round;
- animation: spin 900ms linear infinite;
-}
-
-@keyframes spin {
- to {
- transform: rotate(360deg);
- }
-}
-
@media (prefers-reduced-motion: reduce) {
- .spinner,
- .spinnerLarge {
- animation: none;
- }
-
.detailsChevron {
transition: none;
}
diff --git a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx
index a7cec19607..65eece8d3f 100644
--- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx
+++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx
@@ -12,9 +12,10 @@
import { useEffect, useId, useState, type ReactNode } from 'react'
import type { PluginInstallFailureKind } from '@deepseek-ai/dsh-api-remotes/client'
import {
- Button, IconCheckOutline16, IconChevronDownOutline14, IconChevronLeftOutline14, IconChevronRightOutline14, IconCloseOutline16,
- IconCordisPluginOutline14, IconPluginPinwheelOutline16, IconPlusOutline16, IconRefreshOutline16, IconTrashOutline16,
- IconWarningOutline16, Input, Modal, StateDot, Switch, Tag, TerminalBlock, Toast,
+ Button, IconChevronDownOutlineRegular, IconChevronLeftOutlineRegular,
+ IconChevronRightOutlineRegular, IconCloseOutlineRegular, IconCordisPluginOutlineRegular,
+ IconPluginPinwheelOutlineRegular, IconPlusOutlineRegular, IconRefreshOutlineRegular, IconTrashOutlineRegular,
+ IconWarningOutlineRegular, Input, Modal, StateDot, Switch, Tag, TerminalBlock, Toast,
type StateDotState, type TerminalBlockLabels,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
@@ -74,13 +75,13 @@ const PHASE_KEYS = {
unloading: 'rowPhaseUnloading',
} satisfies Record
-/** Status dot naming a live root-fiber phase: pending and unloading fibers do nothing; only loading is in progress. */
+/** Status dot naming a root-fiber phase; loading and unloading are live transitions. */
const PHASE_STATES = {
pending: 'idle',
loading: 'ongoing',
active: 'done',
failed: 'error',
- unloading: 'idle',
+ unloading: 'ongoing',
} satisfies Record
/** The count line over a pack's components: the total, then only the states that occur. */
@@ -189,20 +190,20 @@ function RowsSection({ rows, t, toggle, configure }: {
{...row.phase === 'failed' ? { 'data-state': 'failed' } : row.enabled ? {} : { 'data-state': 'off' }}
>
)
@@ -940,7 +949,7 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode {
}
+ icon={}
holdMs={toastHoldMs(noticeLine)}
onDone={props.dismissNotice}
/>
diff --git a/packages/client/ui-plugin-manager/src/client/PluginsPanelIcon.tsx b/packages/client/ui-plugin-manager/src/client/PluginsPanelIcon.tsx
index c4fec0df7f..6e6da72d43 100644
--- a/packages/client/ui-plugin-manager/src/client/PluginsPanelIcon.tsx
+++ b/packages/client/ui-plugin-manager/src/client/PluginsPanelIcon.tsx
@@ -1,7 +1,7 @@
/** The sidebar's Plugins entry icon; the sidebar owns the button, label, and selected state around it. */
import type { ReactNode } from 'react'
-import { IconPluginPinwheelOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
+import { IconPluginPinwheelOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
@@ -11,5 +11,5 @@ import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
* @returns the icon element.
*/
export function PluginsPanelIcon({ size }: PropsRuntime<'sidebar.panellist'>): ReactNode {
- return
+ return
}
diff --git a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx
index bca0c579da..1177c746f3 100644
--- a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx
+++ b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx
@@ -101,12 +101,12 @@ describe('PluginManagerPage', () => {
it('asks the store once mounted and renders the loading, unavailable, error, and empty states', () => {
const { actions, set } = renderTab({ status: 'loading' })
expect(actions.ensure).toHaveBeenCalledTimes(1)
- expect(screen.getByText(en.loading)).toBeTruthy()
+ expect(screen.getByText(en.loading).querySelector('[data-state="ongoing"]')).not.toBeNull()
expect(screen.getByRole('button', { name: en.addPlugin })).toHaveProperty('disabled', true)
set({ status: 'unavailable' })
- expect(screen.getByRole('status').textContent).toBe(en.unavailable)
+ expect(screen.getByRole('status').querySelector('[data-state="idle"]')).not.toBeNull()
set({ status: 'error' })
- expect(screen.getByRole('alert').textContent).toBe(en.error)
+ expect(screen.getByRole('alert').querySelector('[data-state="error"]')).not.toBeNull()
fireEvent.click(screen.getByRole('button', { name: en.retry }))
expect(actions.refresh).toHaveBeenCalledTimes(1)
fireEvent.click(screen.getByRole('button', { name: en.refresh }))
@@ -380,6 +380,7 @@ describe('PluginManagerPage', () => {
...index === 1 ? { readOnlyReason: 'unaddressable' as const } : {},
...index === 3 ? { phase: 'failed' as const } : {},
...index === 4 ? { phase: 'loading' as const } : {},
+ ...index === 5 ? { phase: 'unloading' as const } : {},
})
if (index !== 2) return live
// The third row has no live entry: nothing to switch.
@@ -389,7 +390,7 @@ describe('PluginManagerPage', () => {
const { actions, set } = renderTab({ packages: [pkg({ rows })], busy: [rowKey('include:row-5')] })
fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'better-sidebar') }))
const detail = document.querySelector('[data-plugin-detail]') as HTMLElement
- expect(within(detail).getByText(`${en.partsCountTotal.replace('{count}', '12')} · ${en.partsCountRunning.replace('{count}', '9')} · ${en.partsCountOff.replace('{count}', '1')} · ${en.partsCountFailed.replace('{count}', '1')}`)).toBeTruthy()
+ expect(within(detail).getByText(`${en.partsCountTotal.replace('{count}', '12')} · ${en.partsCountRunning.replace('{count}', '8')} · ${en.partsCountOff.replace('{count}', '1')} · ${en.partsCountFailed.replace('{count}', '1')}`)).toBeTruthy()
fireEvent.click(within(detail).getByRole('switch', { name: en.partToggle.replace('{name}', 'row-0') }))
expect(actions.setRowEnabled).toHaveBeenCalledWith('include:row-0', false)
// A protected row, a row without a live entry, and a row with a write in flight cannot be switched.
@@ -402,7 +403,10 @@ describe('PluginManagerPage', () => {
expect(within(detail).getByRole('switch', { name: en.partToggle.replace('{name}', 'row-5') })).toHaveProperty('disabled', true)
expect(within(detail).getByText(en.rowPhaseFailed)).toBeTruthy()
expect(within(detail).getByText(en.rowPhaseLoading)).toBeTruthy()
+ expect(within(detail).getByText(en.rowPhaseUnloading)).toBeTruthy()
expect(document.querySelector('[data-plugin-row="include:row-3"]')?.getAttribute('data-state')).toBe('failed')
+ expect(document.querySelector('[data-plugin-row="include:row-4"] [data-state="ongoing"]')).not.toBeNull()
+ expect(document.querySelector('[data-plugin-row="include:row-5"] [data-state="ongoing"]')).not.toBeNull()
// A long list gets a filter; nothing matching says so.
const filter = within(detail).getByRole('searchbox', { name: en.partsFilter })
fireEvent.change(filter, { target: { value: 'ROW-1' } })
@@ -439,6 +443,7 @@ describe('PluginManagerPage', () => {
expect(screen.getByRole('textbox', { name: en.installSpecLabel })).toHaveProperty('disabled', true)
const checking = screen.getByRole('button', { name: en.installChecking })
expect(checking).toHaveProperty('disabled', true)
+ expect(checking.querySelector('[data-state="ongoing"]')).not.toBeNull()
fireEvent.keyDown(screen.getByRole('textbox', { name: en.installSpecLabel }), { key: 'Enter' })
expect(actions.runInstall).toHaveBeenCalledTimes(2)
@@ -466,6 +471,7 @@ describe('PluginManagerPage', () => {
const run = { jobId: 'j1', command: 'pnpm add dsh-x', cwd: '/home/u/.dsh/profiles/web', output: 'Progress: resolved \x1b[96m1\x1b[39m\n' }
const { actions, set } = renderTab({ install: { ...IDLE_INSTALL, open: true, spec: 'dsh-x', phase: 'running', subject, runs: [run] } })
expect(screen.getByRole('status').textContent).toBe(en.installingTitle)
+ expect(screen.getByRole('status').parentElement?.querySelector('[data-state="ongoing"]')).not.toBeNull()
expect(screen.getByText('dsh-x')).toBeTruthy()
expect(screen.getByText('A sidebar.')).toBeTruthy()
expect(screen.getByText(en.installVersion.replace('{version}', '1.4.2'))).toBeTruthy()
@@ -549,6 +555,7 @@ describe('PluginManagerPage', () => {
},
})
expect(screen.getByText(en.installedTitle)).toBeTruthy()
+ expect(screen.getByText(en.installedTitle).parentElement?.querySelector('[data-state="done"]')).not.toBeNull()
// A path without a description reads by its kind.
expect(screen.getByText('dsh-x')).toBeTruthy()
expect(screen.getByText(en.installSubjectPath)).toBeTruthy()
diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml
index e7dd304b2f..6a866733a0 100644
--- a/packages/client/ui-primitives/README.i18n.yaml
+++ b/packages/client/ui-primitives/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-primitives/README.md
-README.md: cefa67e49146d5fc2976d3d43b8ae39c3d4b99f6
-README.zh.md: 3c069d05b2948e8494cef934f87270e86a14d978
+README.md: 165df8a18eac4a64befbbb43b0270fc57e3952d6
+README.zh.md: da3186dde43e21d01938c597c13aa422c75ce865
diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md
index cefa67e491..165df8a18e 100644
--- a/packages/client/ui-primitives/README.md
+++ b/packages/client/ui-primitives/README.md
@@ -43,7 +43,7 @@ Check this table before writing a control in a feature package. A plugin cannot
| `Menu` | Dropdown of items, separators, and group labels, with nested submenus. While open, ↑/↓ (with Home and End) walk the list, Tab settles the focused row, and Escape or Shift+Tab close back to the anchor; selecting a row also returns the keyboard to the anchor unless the owner moved it itself. Only a keyboard on the anchor or inside the list is intercepted, and `autoFocus` decides solely whether opening focuses the first row. |
| `Pill` | Selectable capsule button for view switchers and filters; takes `active` and `onClick`. |
| `Tag` | Read-only capsule badge; `tone` selects one of eight palettes. |
-| `StateDot` | Status mark: `done`, `warning`, `ongoing`, `error`, or `idle`. `aria-hidden`, so the render site owns the name. |
+| `StateDot` | Solid green `done`, amber `warning`, red `error`, and neutral-grey `idle` marks in a 10px slot, plus a tertiary-grey 14px rotating `ongoing` loader. `aria-hidden`, so the render site owns the name. |
| `ConnectionIndicator` | Inline connection-recovery control across outage, retry, and recovered states. |
| `DisclosureRow` | 24px compact disclosure that lays title and content side by side. |
| `Modal` | Centered dialog over a page mask. |
@@ -55,8 +55,9 @@ Check this table before writing a control in a feature package. A plugin cannot
| `JsonTree`, `JsonBlock` | Read-only JSON inspection. |
| `MarkdownText`, `MarkdownDelegateProvider`, `CodeBlock` | Untrusted GFM with TeX math, owner-delegated HTTP(S) navigation, and highlighted code. `CodeBlock` accepts opt-in `lineNumbers`; copied source excludes the gutter, and `contentRef` exposes its stable source wrapper to an owner that uses it as a scrollport. Set `showHeader={false}` when the owner supplies its own language and copy toolbar. |
| `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, `WebBlock` | The agent-output card matching each tool-result intent. |
-| `icons/*`, `FishLogo`, `BrandWordmark`, `ReferenceIcon`, `LinkIcon` | Glyphs and brand marks. Use `LinkIcon` for 14px clickable-link categories and known-site marks. |
-| `FileTypeIcon`, `classifyFileType`, `fileExtension` | A category-colored 28px file or folder glyph and the shared case-insensitive filename mapping behind it. Code and configuration files use detailed full-color technology glyphs; use `LinkIcon` for link-leading glyphs and image previews for image content. |
+| `icons/*`, `FishLogo`, `BrandWordmark`, `ReferenceIconRegular`/`ReferenceIconMedium`, `LinkIconRegular`/`LinkIconMedium` | Glyphs and brand marks. Use `LinkIconMedium` for 14px clickable-link categories and known-site marks. |
+| `PermissionIconReadOnlyRegular`/`Medium`, `PermissionIconWorkspaceWriteRegular`/`Medium`, `PermissionIconFullAccessRegular`/`Medium` | Permission-mode glyphs for read-only, workspace-write, and full-access choices. |
+| `FileTypeIcon`, `classifyFileType`, `fileExtension` | A category-colored 28px file or folder glyph and the shared case-insensitive filename mapping behind it. Code and configuration files use detailed full-color technology glyphs; use `LinkIconMedium` for link-leading glyphs and image previews for image content. |
Three pairs are easy to confuse:
@@ -68,7 +69,7 @@ Writing your own component in your own package is fine when the need is genuinel
### Controls and icons
-The catalog above lists what each export is for; this section covers the behavior that props alone do not show. The `ic_ds_*` icon set and `FishLogo`/`BrandWordmark` marks fill brand and inline-icon slots. `FileTypeIcon` renders the traditional 28px Excel, folder, HTML, image, Markdown, generic, PDF, PPT, video, and Word glyphs, and uses the imported square technology artwork for the established 48 code and configuration categories. The import replaces artwork only: archive-only categories do not extend `CodeFileType`. `classifyFileType` applies exact filename, prefix, suffix, optional project-context, and extension rules in that order; React names win over TypeScript/JavaScript, Angular suffixes win over their base extension, and a Dart file becomes Flutter only when the supplied project files contain a `pubspec.yaml` with `flutter:`. Markdown and SVG remain traditional Markdown and image files. Office mappings include XLSM/Numbers as Excel, KEY as slides, and RTF/ODT/Pages as documents. `fileExtension` exposes the same basename and final-dot parsing for adjacent metadata labels. Traditional glyphs use a solid category-colored sheet with a white mark and translucent white corner; the generic file uses a grey sheet and darker grey corner. Callers may override the sheet color through `--dsh-file-type-icon-color`. The full-color technology artwork is the deliberate exception and retains its embedded palette. All glyphs are decorative and carry no label. `LinkIcon` remains the smaller leading glyph for clickable artifact links — globe, folder, code, image, document, or plain paper, and for a `url` link the mark of a well-known site named by its `href` — the developer sites a transcript usually cites (GitHub, GitLab, npm, PyPI, Stack Overflow, MDN, Wikipedia, Hacker News, YouTube, X, Bilibili, Zhihu, Juejin, CSDN) and mainstream search, video, social, shopping, and reference sites (Google, Baidu, DuckDuckGo, TikTok, Netflix, Spotify, Facebook, Instagram, Reddit, Telegram, WhatsApp, WeChat, QQ, Weibo, Taobao, AliExpress, eBay, Quora, V2EX, Apple) — while `classifyLinkPath` folds the shared file types into that existing six-category vocabulary. `ConnectionIndicator` renders a warning-colored disconnected action whose permanent retry glyph marks the retry action beside the owner-supplied outage label, a connecting spinner beside a label whose one-to-three dots advance every 500ms independently of retry timing, or a success-colored recovered status. Clicking either warning state requests an immediate reconnect; no hover interaction changes the copy. The pill fades in on appearance, fades out for 150ms before unmounting, and sizes to its current label. Its owner supplies visibility, the recovery hold, localized labels, and the immediate-reconnect callback; the primitive uses no native title tooltip. `useAnchoredPosition` and `useAnchoredMaxHeight` keep floating panels and bottom-anchored overlays clamped to the viewport and following their anchor. `HoverCard` keeps its portaled preview reachable across the anchor gap and can expose a copy button through the `copyText` prop. `Toast` holds for the window its owner names through `holdMs`, because how long a banner has to stay depends on how much there is to read; the same value drives its unmount timer and the stylesheet's fade delay, so the two cannot disagree. `rankByName` is the `/` menu's shared candidate ranker for the command and skill sources: the query must be a case-insensitive ordered subsequence of the name; prefix hits rank first, then alignment score, then source order. `Menu.autoFocus` focuses its first enabled item, supports Arrow Up/Down and Home/End navigation, and focuses the first button in the anchor on Escape; it is opt-in for action menus.
+The catalog above lists what each export is for; this section covers the behavior that props alone do not show. Product icon names omit artboard sizes and end in `Regular` for the supplied one-pixel artwork or `Medium` for the same geometry at a 1.3px stroke; the `size` prop controls rendered dimensions ([decision](../../../.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.md)). Each product, reference, link, and permission glyph intentionally keeps both weight exports even when the current product uses only one, so callers can choose emphasis without adding another API later; fill-only pairs render identically. `FishLogo` and `BrandWordmark` fill brand slots. `FileTypeIcon` renders the traditional 28px spreadsheet, folder, HTML, image, Markdown, generic, PDF, PPT, video, and Word glyphs, and uses the imported square technology artwork for the established 48 code and configuration categories. The import replaces artwork only: archive-only categories do not extend `CodeFileType`. `classifyFileType` applies exact filename, prefix, suffix, optional project-context, and extension rules in that order; React names win over TypeScript/JavaScript, Angular suffixes win over their base extension, and a Dart file becomes Flutter only when the supplied project files contain a `pubspec.yaml` with `flutter:`. Markdown and SVG remain traditional Markdown and image files. Spreadsheet mappings include CSV, TSV, Excel workbooks and templates, OpenDocument spreadsheets, and Numbers; KEY maps to slides, while RTF/ODT/Pages map to documents. `fileExtension` exposes the same basename and final-dot parsing for adjacent metadata labels. Traditional glyphs use a solid category-colored sheet with a white mark and translucent white corner; the generic file uses a grey sheet and darker grey corner. Callers may override the sheet color through `--dsh-file-type-icon-color`. The full-color technology artwork is the deliberate exception and retains its embedded palette. All glyphs are decorative and carry no label. `LinkIconMedium` is the leading glyph for clickable artifact links — globe, folder, code, image, document, or plain paper, and for a `url` link the mark of a well-known site named by its `href` — the developer sites a transcript usually cites (GitHub, GitLab, npm, PyPI, Stack Overflow, MDN, Wikipedia, Hacker News, YouTube, X, Bilibili, Zhihu, Juejin, CSDN) and mainstream search, video, social, shopping, and reference sites (Google, Baidu, DuckDuckGo, TikTok, Netflix, Spotify, Facebook, Instagram, Reddit, Telegram, WhatsApp, WeChat, QQ, Weibo, Taobao, AliExpress, eBay, Quora, V2EX, Apple) — while `classifyLinkPath` folds the shared file types into that existing six-category vocabulary. `ConnectionIndicator` renders a warning-colored disconnected action whose permanent retry glyph marks the retry action beside the owner-supplied outage label, the shared ongoing loader beside a label whose one-to-three dots advance every 500ms independently of retry timing, or a success-colored recovered status. Clicking either warning state requests an immediate reconnect; no hover interaction changes the copy. The pill fades in on appearance, fades out for 150ms before unmounting, and sizes to its current label. Its owner supplies visibility, the recovery hold, localized labels, and the immediate-reconnect callback; the primitive uses no native title tooltip. `useAnchoredPosition` and `useAnchoredMaxHeight` keep floating panels and bottom-anchored overlays clamped to the viewport and following their anchor. `HoverCard` keeps its portaled preview reachable across the anchor gap and can expose a copy button through the `copyText` prop. `Toast` holds for the window its owner names through `holdMs`, because how long a banner has to stay depends on how much there is to read; the same value drives its unmount timer and the stylesheet's fade delay, so the two cannot disagree. `rankByName` is the `/` menu's shared candidate ranker for the command and skill sources: the query must be a case-insensitive ordered subsequence of the name; prefix hits rank first, then alignment score, then source order. `Menu.autoFocus` focuses its first enabled item, supports Arrow Up/Down and Home/End navigation, and focuses the first button in the anchor on Escape; it is opt-in for action menus.
### Rendering agent output
@@ -105,7 +106,7 @@ The package enforces one separation: presentational React atoms with zero Cordis
| [`src/TerminalBlock.tsx`](src/TerminalBlock.tsx) | ANSI escape parsing (`anser`) and terminal card rendering |
| [`src/ReadBlock.tsx`](src/ReadBlock.tsx) / [`src/DiffBlock.tsx`](src/DiffBlock.tsx) | Read and diff cards |
| [`src/SearchBlock.tsx`](src/SearchBlock.tsx) / [`src/WebBlock.tsx`](src/WebBlock.tsx) | Search and web-retrieval cards |
-| [`src/icons/`](src/icons/) | `ic_ds_*` glyph components and brand marks |
+| [`src/icons/`](src/icons/) | Size-neutral `Regular` and `Medium` product glyph components |
| [`src/useAnchoredPosition.ts`](src/useAnchoredPosition.ts) / [`src/useAnchoredMaxHeight.ts`](src/useAnchoredMaxHeight.ts) | Floating-panel and overlay geometry hooks |
### Streaming markdown
@@ -153,7 +154,7 @@ These limits define how the atoms behave at the edges; they are current package
- **Known-site marks are a fixed list** — only the named hosts resolve to their own mark, and every other external host keeps the globe; recognizing an arbitrary site would require fetching its icon over the network.
- **Streaming defers cross-boundary reference resolution** — a reference-style link or footnote whose definition sits on the other side of the incremental freeze boundary renders as literal text while the reply streams; the settled full parse at finalize resolves it.
- **A long highlighted fence retains its complete token DOM** — streaming avoids re-parsing, re-tokenizing, and reconciling the completed prefix, but it does not discard old colors or virtualize token spans. Final DOM cardinality therefore still follows the fence's token count; nested/container fences and a pathological single long line remain on the general tail path.
-- **Glyph-level icons are redrawn approximations** — the fish logo and the sparkle mark come from font glyphs whose vector geometry is not exportable from the local design data; hand-authored recreations stand in until an exact export path exists.
+- **The fish logo is a redrawn approximation** — its font glyph has no exportable vector geometry in the local design data, so a hand-authored recreation stands in until an exact export path exists.
- **`Pill` and `Input` have no design source** — both atoms are self-defined; the sidebar search field and view-tab strip that resemble them are consumer-owned compositions, not these atoms.
- **No `Active` `StateDot` variant** — the supported states are done, warning, ongoing, error, and idle.
- **User-facing copy is required at the render site** — the atoms are zero-Cordis and cannot reach `ctx.locale`; each feature must supply complete localized labels through the primitive's typed props ([decision](../../../.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.md)).
diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md
index 3c069d05b2..da3186dde4 100644
--- a/packages/client/ui-primitives/README.zh.md
+++ b/packages/client/ui-primitives/README.zh.md
@@ -43,7 +43,7 @@ kind: "package-library"
| `Menu` | 由条目、分隔线与分组标题构成的下拉菜单,支持嵌套子菜单。打开期间 `↑`/`↓`(以及 Home、End)在列表中走位,Tab 选定聚焦行,Escape 或 Shift+Tab 关闭并把焦点还给锚点;选定一行同样把键盘还给锚点——除非拥有者自己移动了焦点。只拦截位于锚点或列表内的键盘,`autoFocus` 仅决定打开时是否聚焦首行。 |
| `Pill` | 可选中的胶囊按钮,用于视图切换与筛选器;接受 `active` 与 `onClick`。 |
| `Tag` | 只读胶囊徽章;`tone` 选择八种配色之一。 |
-| `StateDot` | 状态标记:`done`、`warning`、`ongoing`、`error` 或 `idle`。它是 `aria-hidden` 的,名称由渲染点提供。 |
+| `StateDot` | 10px 槽内的绿色 `done`、琥珀色 `warning`、红色 `error`、中性灰色 `idle` 圆点,以及 tertiary 灰色 14px 旋转 `ongoing` loading。它是 `aria-hidden` 的,名称由渲染点提供。 |
| `ConnectionIndicator` | 行内连接恢复控件,覆盖断线、重试与已恢复三种状态。 |
| `DisclosureRow` | 24px 紧凑折叠行,标题与内容左右排列。 |
| `Modal` | 页面遮罩之上的居中对话框。 |
@@ -55,8 +55,9 @@ kind: "package-library"
| `JsonTree`、`JsonBlock` | 只读 JSON 查看。 |
| `MarkdownText`、`MarkdownDelegateProvider`、`CodeBlock` | 不可信 GFM 与 TeX 数学、owner 委托的 HTTP(S) 导航,以及高亮代码。`CodeBlock` 可通过 `lineNumbers` 开启行号;复制的源码不含行号栏,`contentRef` 则向需要把稳定源码包装节点用作滚动区的 owner 提供该节点。调用方提供自己的语言与复制工具栏时,设置 `showHeader={false}`。 |
| `TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock`、`WebBlock` | 与各类工具结果意图对应的 agent 输出卡片。 |
-| `icons/*`、`FishLogo`、`BrandWordmark`、`ReferenceIcon`、`LinkIcon` | 字形与品牌标识。`LinkIcon` 用于 14px 的可点击链接分类及已知站点标记。 |
-| `FileTypeIcon`、`classifyFileType`、`fileExtension` | 按类别着色的 28px 文件或文件夹图形,以及它背后共享的不区分大小写文件名映射。代码与配置文件使用细分的全彩技术图形;链接前置图形使用 `LinkIcon`,图片内容使用图片预览。 |
+| `icons/*`、`FishLogo`、`BrandWordmark`、`ReferenceIconRegular`/`ReferenceIconMedium`、`LinkIconRegular`/`LinkIconMedium` | 字形与品牌标识。`LinkIconMedium` 用于 14px 的可点击链接分类及已知站点标记。 |
+| `PermissionIconReadOnlyRegular`/`Medium`、`PermissionIconWorkspaceWriteRegular`/`Medium`、`PermissionIconFullAccessRegular`/`Medium` | 只读、工作区写入与完全访问选项使用的权限模式图形。 |
+| `FileTypeIcon`、`classifyFileType`、`fileExtension` | 按类别着色的 28px 文件或文件夹图形,以及它背后共享的不区分大小写文件名映射。代码与配置文件使用细分的全彩技术图形;链接前置图形使用 `LinkIconMedium`,图片内容使用图片预览。 |
有三组容易混淆:
@@ -68,7 +69,7 @@ kind: "package-library"
### 控件与图标
-上面的目录说明每个导出的用途;本节讲 props 本身看不出来的行为。`ic_ds_*` 图标集与 `FishLogo`/`BrandWordmark` 标记填充品牌与行内图标 slot。`FileTypeIcon` 渲染传统的 28px Excel、folder、HTML、image、Markdown、generic、PDF、PPT、video 与 Word 图形,并为现有 48 个代码和配置类别使用导入的方形技术图形。该导入只替换图形:资源包中额外的类别不会扩展 `CodeFileType`。`classifyFileType` 按完整文件名、前缀、后缀、可选项目上下文、扩展名的顺序匹配;React 文件名优先于 TypeScript/JavaScript,Angular 后缀优先于基础扩展名,只有传入的项目文件包含带 `flutter:` 的 `pubspec.yaml` 时 Dart 文件才使用 Flutter。Markdown 与 SVG 仍分别使用传统 Markdown 与图片图形。办公文件映射包含 XLSM/Numbers 的表格图标、KEY 的幻灯片图标,以及 RTF/ODT/Pages 的文档图标。`fileExtension` 为相邻元数据 label 暴露同一套 basename 与最终点号解析。传统图形使用实色分类底板、白色标记和半透明白色折角;通用文件使用灰色底板与较深灰色折角。调用方可通过 `--dsh-file-type-icon-color` 覆盖底板颜色。全彩技术图形是明确例外,会保留其内嵌调色板。所有图形都是装饰性的,不自带 label。`LinkIcon` 仍是可点击产物链接较小的前置图形——地球、文件夹、代码、图片、文档或纸张,`url` 链接的 `href` 指向已知站点时则改用该站点自己的标记——转写内容常引用的开发者站点(GitHub、GitLab、npm、PyPI、Stack Overflow、MDN、Wikipedia、Hacker News、YouTube、X、Bilibili、知乎、掘金、CSDN),以及主流搜索、视频、社交、购物与参考资料站点(Google、百度、DuckDuckGo、TikTok、Netflix、Spotify、Facebook、Instagram、Reddit、Telegram、WhatsApp、微信、QQ、微博、淘宝、速卖通、eBay、Quora、V2EX、Apple)——`classifyLinkPath` 把共享文件类型折叠进原有六类词汇。`ConnectionIndicator` 可渲染警告色的断联操作(常驻重试图形指明重试动作,断联文案由持有方提供)、spinner 加一至三个点以独立于 retry 时序的 500ms 节奏推进的连接中状态,或成功色的恢复状态。点击任一警告状态都会请求立即重连;没有任何悬停交互会改变文案。药丸出现时淡入、卸载前淡出 150ms,宽度随当前 label 自适应。它的持有方提供可见性、恢复驻留时间、本地化 label 与立即重连回调;该原语不使用原生 title tooltip。`useAnchoredPosition` 与 `useAnchoredMaxHeight` 让浮动面板与底部锚定浮层始终钳制在视口内并跟随锚点。`HoverCard` 通过指针离开宽限期让采用 portal 的预览在跨过锚点间隙时仍可触及,并可通过 `copyText` prop 提供复制按钮。`Toast` 的停留时长由使用方通过 `holdMs` 指定,因为横幅该留多久取决于有多少内容要读;同一个值同时驱动它的卸载定时器与样式表的淡出延迟,两者不可能再错位。`rankByName` 是 `/` 菜单命令源与 skill(技能)源共享的候选排序器:查询必须是名字的不区分大小写的有序子序列;前缀命中排最前,其次按对齐分数,再按来源顺序。 `Menu.autoFocus` 聚焦首个启用项,支持上下方向键与 Home/End 导航,并在 Escape 时聚焦 anchor 内的第一个按钮;操作菜单可显式启用。
+上面的目录说明每个导出的用途;本节讲 props 本身看不出来的行为。产品图标名称不含画板尺寸,以 `Regular` 表示原始 1px 图形,以 `Medium` 表示同一几何的 1.3px 描边;`size` prop 控制渲染尺寸([决定](../../../.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.zh.md))。每个产品、引用、链接与权限图形都会有意保留两种线重导出,即使当前产品只使用其中一种,也让调用方无需再次扩展 API 就能选择强调程度;仅填充的成对图形外观相同。`FishLogo` 与 `BrandWordmark` 填充品牌 slot。`FileTypeIcon` 渲染传统的 28px spreadsheet、folder、HTML、image、Markdown、generic、PDF、PPT、video 与 Word 图形,并为现有 48 个代码和配置类别使用导入的方形技术图形。该导入只替换图形:资源包中额外的类别不会扩展 `CodeFileType`。`classifyFileType` 按完整文件名、前缀、后缀、可选项目上下文、扩展名的顺序匹配;React 文件名优先于 TypeScript/JavaScript,Angular 后缀优先于基础扩展名,只有传入的项目文件包含带 `flutter:` 的 `pubspec.yaml` 时 Dart 文件才使用 Flutter。Markdown 与 SVG 仍分别使用传统 Markdown 与图片图形。表格映射包括 CSV、TSV、Excel 工作簿与模板、OpenDocument 表格和 Numbers;KEY 映射为幻灯片,RTF/ODT/Pages 映射为文档。`fileExtension` 为相邻元数据 label 暴露同一套 basename 与最终点号解析。传统图形使用实色分类底板、白色标记和半透明白色折角;通用文件使用灰色底板与较深灰色折角。调用方可通过 `--dsh-file-type-icon-color` 覆盖底板颜色。全彩技术图形是明确例外,会保留其内嵌调色板。所有图形都是装饰性的,不自带 label。`LinkIconMedium` 是可点击产物链接的前置图形——地球、文件夹、代码、图片、文档或纸张,`url` 链接的 `href` 指向已知站点时则改用该站点自己的标记——转写内容常引用的开发者站点(GitHub、GitLab、npm、PyPI、Stack Overflow、MDN、Wikipedia、Hacker News、YouTube、X、Bilibili、知乎、掘金、CSDN),以及主流搜索、视频、社交、购物与参考资料站点(Google、百度、DuckDuckGo、TikTok、Netflix、Spotify、Facebook、Instagram、Reddit、Telegram、WhatsApp、微信、QQ、微博、淘宝、速卖通、eBay、Quora、V2EX、Apple)——`classifyLinkPath` 把共享文件类型折叠进原有六类词汇。`ConnectionIndicator` 可渲染警告色的断联操作(常驻重试图形指明重试动作,断联文案由持有方提供)、共享 ongoing loading 加一至三个点以独立于 retry 时序的 500ms 节奏推进的连接中状态,或成功色的恢复状态。点击任一警告状态都会请求立即重连;没有任何悬停交互会改变文案。药丸出现时淡入、卸载前淡出 150ms,宽度随当前 label 自适应。它的持有方提供可见性、恢复驻留时间、本地化 label 与立即重连回调;该原语不使用原生 title tooltip。`useAnchoredPosition` 与 `useAnchoredMaxHeight` 让浮动面板与底部锚定浮层始终钳制在视口内并跟随锚点。`HoverCard` 通过指针离开宽限期让采用 portal 的预览在跨过锚点间隙时仍可触及,并可通过 `copyText` prop 提供复制按钮。`Toast` 的停留时长由使用方通过 `holdMs` 指定,因为横幅该留多久取决于有多少内容要读;同一个值同时驱动它的卸载定时器与样式表的淡出延迟,两者不可能再错位。`rankByName` 是 `/` 菜单命令源与 skill(技能)源共享的候选排序器:查询必须是名字的不区分大小写的有序子序列;前缀命中排最前,其次按对齐分数,再按来源顺序。 `Menu.autoFocus` 聚焦首个启用项,支持上下方向键与 Home/End 导航,并在 Escape 时聚焦 anchor 内的第一个按钮;操作菜单可显式启用。
### 渲染 agent 输出
@@ -105,7 +106,7 @@ kind: "package-library"
| [`src/TerminalBlock.tsx`](src/TerminalBlock.tsx) | ANSI 转义解析(`anser`)与终端卡片渲染 |
| [`src/ReadBlock.tsx`](src/ReadBlock.tsx) / [`src/DiffBlock.tsx`](src/DiffBlock.tsx) | 读取与差异卡片 |
| [`src/SearchBlock.tsx`](src/SearchBlock.tsx) / [`src/WebBlock.tsx`](src/WebBlock.tsx) | 搜索与网页检索卡片 |
-| [`src/icons/`](src/icons/) | `ic_ds_*` 字形组件与品牌标记 |
+| [`src/icons/`](src/icons/) | 与尺寸无关的 `Regular` 和 `Medium` 产品图标组件 |
| [`src/useAnchoredPosition.ts`](src/useAnchoredPosition.ts) / [`src/useAnchoredMaxHeight.ts`](src/useAnchoredMaxHeight.ts) | 浮动面板与浮层几何钩子 |
### 流式 Markdown
@@ -153,7 +154,7 @@ kind: "package-library"
- **已知站点标记是固定列表**:只有列名的主机解析为自己的标记,其余外部主机仍使用地球;要识别任意站点需要通过网络抓取它的图标。
- **流式期间跨边界引用解析被推迟**:定义落在增量冻结边界另一侧的引用式链接或脚注,在回复流式输出期间渲染为字面文本;定稿时的全量解析会将其解析。
- **长高亮 fence 会保留完整 token DOM**:流式路径避免重新解析、重新 tokenize 和 reconcile 已完成前缀,但不会丢弃旧颜色或虚拟化 token span。因此最终 DOM 数量仍随 fence 的 token 数增长;嵌套/容器内 fence 与病态的单个超长行仍走通用尾部路径。
-- **字形级图标是重新绘制的近似版本**:鱼形标志与闪光标记来自字体字形,而本地设计数据无法导出其矢量几何;在获得精确导出路径前,使用手工重建版本代替。
+- **鱼形标志是重新绘制的近似版本**:它来自字体字形,而本地设计数据无法导出其矢量几何;在获得精确导出路径前,使用手工重建版本代替。
- **`Pill` 与 `Input` 没有设计来源**:两个原子组件均自行定义;与其相似的侧边栏搜索字段和视图标签条由消费方组合,不是这些原子组件。
- **`StateDot` 没有 `Active` 变体**:支持的状态为 done、warning、ongoing、error 和 idle。
- **面向用户的文案必须由渲染点提供**:这些原子组件是 zero-Cordis 的,拿不到 `ctx.locale`;各功能必须通过原子组件的带类型 prop 提供完整本地化 label(见[决策](../../../.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.zh.md))。
diff --git a/packages/client/ui-primitives/package.json b/packages/client/ui-primitives/package.json
index c10bf53c7d..1e6b7de54e 100644
--- a/packages/client/ui-primitives/package.json
+++ b/packages/client/ui-primitives/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-primitives",
"description": "Pure React atoms for the dsh web UI: controls, icons, markdown, and JSON inspectors (zero cordis)",
- "version": "0.1.6-alpha.1",
+ "version": "0.1.6-alpha.2",
"publishConfig": {
"access": "public"
},
diff --git a/packages/client/ui-primitives/src/ConnectionIndicator.module.css b/packages/client/ui-primitives/src/ConnectionIndicator.module.css
index 285edb6052..e559fbca0e 100644
--- a/packages/client/ui-primitives/src/ConnectionIndicator.module.css
+++ b/packages/client/ui-primitives/src/ConnectionIndicator.module.css
@@ -77,14 +77,6 @@
text-align: left;
}
-.spinner {
- animation: spinner-rotate 0.9s linear infinite;
-}
-
-@keyframes spinner-rotate {
- to { transform: rotate(360deg); }
-}
-
.dots {
display: inline-block;
width: 1em;
@@ -112,8 +104,7 @@
@media (prefers-reduced-motion: reduce) {
.indicator,
.secondDot,
- .thirdDot,
- .spinner {
+ .thirdDot {
animation: none;
}
diff --git a/packages/client/ui-primitives/src/ConnectionIndicator.tsx b/packages/client/ui-primitives/src/ConnectionIndicator.tsx
index 552e778e3c..0a12404560 100644
--- a/packages/client/ui-primitives/src/ConnectionIndicator.tsx
+++ b/packages/client/ui-primitives/src/ConnectionIndicator.tsx
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'
-import { IconCheckOutline16, IconLoadingOutline16, IconRefreshOutline14 } from './icons/index.tsx'
+import { IconCheckOutlineRegular, IconRefreshOutlineRegular } from './icons/index.tsx'
+import { StateDot } from './StateDot.tsx'
import css from './ConnectionIndicator.module.css'
/** Visual state rendered by {@link ConnectionIndicator}. */
@@ -63,7 +64,7 @@ export function ConnectionIndicator({
role="status"
aria-label={recoveredLabel}
>
-
+ {recoveredLabel}
)
@@ -80,8 +81,8 @@ export function ConnectionIndicator({
>
{connecting
- ?
- : }
+ ?
+ : }
{connecting
diff --git a/packages/client/ui-primitives/src/DisclosureRow.tsx b/packages/client/ui-primitives/src/DisclosureRow.tsx
index f04ad8986a..f79b7e4720 100644
--- a/packages/client/ui-primitives/src/DisclosureRow.tsx
+++ b/packages/client/ui-primitives/src/DisclosureRow.tsx
@@ -1,6 +1,6 @@
import { type KeyboardEvent, type MouseEvent, type ReactNode } from 'react'
import clsx from 'clsx'
-import { IconChevronDownOutline14 } from './icons/index.tsx'
+import { IconChevronDownOutlineRegular } from './icons/index.tsx'
import css from './DisclosureRow.module.css'
/** Shared 24px disclosure chrome for compact flow rows. */
@@ -61,12 +61,12 @@ export function DisclosureRow({
? (
<>
{icon}
-
+
>
)
: icon
const leading = open
- ?
+ ?
: collapsedLeading
return (
diff --git a/packages/client/ui-primitives/src/FileTypeIcon.tsx b/packages/client/ui-primitives/src/FileTypeIcon.tsx
index 0a88317401..9beeb360f9 100644
--- a/packages/client/ui-primitives/src/FileTypeIcon.tsx
+++ b/packages/client/ui-primitives/src/FileTypeIcon.tsx
@@ -53,8 +53,8 @@ const EXTENSION_TYPES: Readonly> = {
astro: 'code',
bat: 'code',
cmd: 'code',
- csv: 'code',
- tsv: 'code',
+ csv: 'excel',
+ tsv: 'excel',
html: 'html',
htm: 'html',
png: 'image',
@@ -93,6 +93,13 @@ const EXTENSION_TYPES: Readonly> = {
xls: 'excel',
xlsx: 'excel',
xlsm: 'excel',
+ xlsb: 'excel',
+ xlt: 'excel',
+ xltx: 'excel',
+ xltm: 'excel',
+ ods: 'excel',
+ ots: 'excel',
+ fods: 'excel',
numbers: 'excel',
}
@@ -145,7 +152,7 @@ const LARGE_FILE_MARK_TRANSFORM = 'translate(14 16) scale(1.22) translate(-14 -1
const FOLDER_MARK_TRANSFORM = 'translate(14 13.0693) scale(1.12) translate(-14 -13.0693)'
function FileGlyph({
- size, className, children, markTransform = FILE_MARK_TRANSFORM, muted = false,
+ size, className, children, markTransform, muted = false,
}: IconProps & { children?: ReactNode; markTransform?: string; muted?: boolean }): ReactNode {
return (
{renderCopy?.(true)}
diff --git a/packages/client/ui-primitives/src/LinkIcon.tsx b/packages/client/ui-primitives/src/LinkIcon.tsx
index 22b41fdd4f..d8f8347282 100644
--- a/packages/client/ui-primitives/src/LinkIcon.tsx
+++ b/packages/client/ui-primitives/src/LinkIcon.tsx
@@ -1,42 +1,29 @@
/**
- * Leading category glyph for clickable artifact links (anchors, file
- * mentions, produced-file chips). One component keyed by link category;
- * every glyph renders fill="currentColor" and so rides the link's own
- * color. Glyphs stay private to this module: the public surface is the
- * category vocabulary, not the individual glyph components, so consumers
- * cannot compose a glyph outside a link. Design sources:
- * ic_globe_language_outline_20, ic_code_outline_20, ic_folder_outline_20,
- * ic_photo_outline_20, ic_paper_doc_outline_20, ic_paper_outline_20.
- *
- * A `url` glyph whose destination names a well-known site is that site's own
- * mark instead of the globe (`SiteGlyph.tsx`); every mark keeps the same
- * currentColor-only rule.
+ * Leading current-color category glyphs for clickable artifact links. The
+ * public component keeps individual artwork private to the link vocabulary.
*/
import type { ReactNode } from 'react'
import { classifyFileType, fileExtension } from './FileTypeIcon.tsx'
import { isCodeFileType, isLinkCodeExtension } from './code-file-types.ts'
import { siteGlyph } from './SiteGlyph.tsx'
+import { ICON_MEDIUM_STROKE, ICON_REGULAR_STROKE } from './icons/index.tsx'
import type { IconProps } from './icons/props.ts'
+import { CodeBracketsArtwork, FolderCloseArtwork, GlobeOutlineArtwork } from './icons/shared-artwork.tsx'
-/**
- * Link categories with distinct leading glyphs. `url` and `folder` are
- * destination categories the consumer states directly; the rest are file
- * categories `classifyLinkPath` derives from a path. Code, web, and data
- * files share the `code` glyph by design.
- */
+/** Link categories with distinct leading glyphs. */
export type LinkIconKind = 'url' | 'folder' | 'code' | 'image' | 'document' | 'other'
-/** Props for {@link LinkIcon}: the category plus the shared icon sizing seat. */
+/** Props for link icons: the category plus the shared icon sizing seat. */
export interface LinkIconProps extends IconProps {
kind: LinkIconKind
- /**
- * Destination of a `url` link. A well-known site draws its own mark, so
- * callers that know the destination should pass it; anything else keeps the
- * globe. Other kinds ignore it — their destination is a path, not a site.
- */
+ /** Destination of a URL link; known HTTP(S) hosts render their site mark. */
href?: string | undefined
}
+interface WeightedLinkIconProps extends LinkIconProps {
+ strokeWidth: number
+}
+
/**
* Derive a file path's link-icon category from its extension. Unknown and
* missing extensions fall to `other` (the plain-paper glyph).
@@ -64,75 +51,27 @@ export function classifyLinkPath(path: string): LinkIconKind {
}
}
-const GlobeGlyph = ({ size, className }: IconProps) => (
-