feat(client): unify process row shimmer

Co-authored-by: imccyu <[email protected]>
This commit is contained in:
yixiangihsiangandimccyu authored and imccyu committed 2026-09-28 16:48:53 +08:00
1 parent aad6053316
commit ae9a455bfd
32 files changed
+227 -168

No files matched your search

+2 -2
View File
@@ -33,8 +33,8 @@
en: 0ca3e8c4eab1ca78
zh: 1a1c71bab151eb7d
/deepseek-ai-dsh-client-ui-chat/grouped-rendering:
en: cc6718a9ff761114
zh: d734fc73c1b77074
en: f7b5260bea372361
zh: 2daa3ff650ec9246
/deepseek-ai-dsh-client-ui-chat/scroll-ownership:
en: 69b021c5a5d8b5a7
zh: 091ae7986c7a8a20
+2
View File
@@ -95,6 +95,8 @@ Live tool deltas share reasoning's frame-batched publication; durable calls and
The process group uses a stable `div` layout box, a scroll body, and an uncapped content box that reports growth inside the body. Business styles must adapt spacing within and across groups, including hidden or empty members and the answer-spacing exception. CSS variables do not belong in the Group Definition.
Running group titles, reasoning previews, tool and command rows, the active retry line, and the bottom running status use the shared [TextShimmer](../ui-primitives/README.md#component-catalog). Each row’s text shares one left-to-right highlight, including collapsed group titles; icons and hover chevrons retain their base appearance. Tool and process rows sit 6px apart. An expanded group title has 8px before its content, Assistant responses have 12px from adjacent process rows, and completed-Turn duration/status controls have 16px around them. Hidden and empty rows, including retained empty slot anchors, add no gap. The running “Deep diving” text retains its blue base while its theme-specific highlight sweeps across it.
Scroll-edge fades initialize when `ResizeObserver` reports the open group's layout; opening the group performs no immediate scroll-dimension read in a layout effect.
Each group owns local `useDisclosure` state that survives mode changes while its component stays mounted.
+2
View File
@@ -95,6 +95,8 @@ Chat 通过 `uiConversation.groups` 注册过程 Group Definition。React 通过
过程组使用稳定的 `div` 布局盒子、滚动正文及不限高的内容盒子,后者报告正文内部的内容增长。业务样式必须适配组内及组边界间距,处理隐藏或空成员以及回答前的间距特例。CSS 变量不属于 Group Definition。
运行中的分组标题、思考预览、工具与命令行、当前重试行以及底部运行状态使用共享的 [TextShimmer](../ui-primitives/README.zh.md#component-catalog)。每行的文字共用一道从左到右的高光,包括折叠的分组标题;图标和悬停箭头保留底色。工具和过程行之间相隔 6px;展开的分组标题与内容之间相隔 8px;Assistant 回复与相邻过程行之间相隔 12px;每轮完成/耗时控件周围保留 16px。隐藏行与空行(包括保留的空 slot 锚点)不产生间距。“深度求索”运行文字保留蓝色底色,并使用主题专用高光扫过文字。
滚动边缘渐隐在 `ResizeObserver` 报告已展开组的布局后初始化;展开组时不会在 layout effect 中立即读取滚动尺寸。
每个组拥有本地 `useDisclosure` 状态,组件保持挂载时,模式切换保留该状态。
@@ -120,7 +120,9 @@ const ProcessGroupHeader = memo(function ProcessGroupHeader({ groupKey, useChatG
{open ? <IconChevronUpOutlineRegular /> : <IconChevronDownOutlineRegular />}
</span>
</span>
<TextShimmer active={!data.closed} className={css.label}>{title}</TextShimmer>
<TextShimmer active={!data.closed}>
<TextShimmer className={css.label}>{title}</TextShimmer>
</TextShimmer>
</button>
)
})
@@ -118,6 +118,7 @@
flex-direction: column;
align-items: flex-start;
--dsh-chat-flow-gap: 12px;
--dsw-alias-label-shimmer: var(--dsw-alias-label-deep-diving-shimmer);
color: var(--dsw-alias-label-deep-diving);
font-size: calc(var(--dsh-content-font-size, 14px) - 2px);
line-height: calc(22px + var(--dsh-content-font-delta, 0px));
@@ -42,9 +42,9 @@ export const GenericCommandCard = memo(function GenericCommandCard({ node, t, ru
const toggle = useCallback(() => { setExpanded(value => !value) }, [])
const collapsedContent = useMemo(() => (
<>
<span className={css.separator} aria-hidden />
<span className={css.separator} data-shimmer-decoration aria-hidden />
<span className={css.summary} data-error={state === 'error' || undefined}>
<TextShimmer active={running}>{summary}</TextShimmer>
<TextShimmer>{summary}</TextShimmer>
</span>
</>
), [running, state, summary])
@@ -201,6 +201,7 @@
display: inline-flex;
align-items: center;
width: fit-content;
max-width: 100%;
padding: 2px 0;
gap: 7px;
border-radius: var(--dsw-radius-sm);
@@ -215,8 +216,10 @@
}
.retrySummary::after {
flex: none;
width: 6px;
height: 6px;
margin-right: 2px;
border-right: 1.5px solid currentcolor;
border-bottom: 1.5px solid currentcolor;
content: '';
@@ -235,24 +238,8 @@
}
.retryText {
color: inherit;
}
.retryRow[data-active] .retryText {
background:
linear-gradient(
90deg,
var(--dsw-alias-label-tertiary) 0%,
var(--dsw-alias-label-tertiary) 40%,
var(--dsw-alias-label-secondary) 50%,
var(--dsw-alias-label-tertiary) 60%,
var(--dsw-alias-label-tertiary) 100%
);
background-position: 100% 50%;
background-size: 200% 100%;
background-clip: text;
color: transparent;
animation: retry-shimmer 1.6s ease-in-out infinite;
min-width: 0;
overflow-wrap: anywhere;
}
.retryRow[open] .retrySummary::after {
@@ -313,24 +300,6 @@
font-weight: 600;
}
@keyframes retry-shimmer {
from {
background-position: 100% 50%;
}
to {
background-position: 0 50%;
}
}
@media (prefers-reduced-motion: reduce) {
.retryRow[data-active] .retryText {
background: none;
color: inherit;
animation: none;
}
}
.attachmentRow {
display: flex;
flex-wrap: wrap;
@@ -2,7 +2,7 @@ import { Fragment, memo, useEffect, useMemo, useState } from 'react'
import type { ReactNode } from 'react'
import type { PendingSubmission } from '@deepseek-ai/dsh-api-session-controller/client'
import type { MessageImageSource } from '@deepseek-ai/dsh-client-ui-conversation/client'
import { fileExtension, FileTypeIcon, fileSizeText, JsonBlock, projectUserText, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
import { fileExtension, FileTypeIcon, fileSizeText, JsonBlock, projectUserText, StateDot, TextShimmer } from '@deepseek-ai/dsh-client-ui-primitives'
import type { ChatNodeOwnerProps, ChatNodeViewProps, ChatViewSlotProps } from '../contract/slots.ts'
import type { ModelRetryNode, TurnErrorNode, UserMessageNode } from '../contract/snapshot.ts'
import { CompactionItem } from './CompactionItem.tsx'
@@ -107,7 +107,9 @@ function ModelRetryItem({ node, active, t }: {
<details className={css.retryRow} data-active={active || undefined}>
<summary className={css.retrySummary}>
<span className={css.retryText} role="status">
{t('message.retry.status', { label, retry: node.retry, maximum, seconds })}
<TextShimmer active={active}>
{t('message.retry.status', { label, retry: node.retry, maximum, seconds })}
</TextShimmer>
</span>
</summary>
<div className={css.retryDetails}>
@@ -22,27 +22,6 @@
background: var(--dsw-alias-bg-base);
}
.root[data-state='running'] .row::after {
content: '';
position: absolute;
inset-block: 0;
left: 0;
width: 300px;
background: linear-gradient(
90deg,
transparent 0%,
color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%,
transparent 100%
);
animation: dsh-reasoning-row-sweep 2.6s ease-out infinite;
pointer-events: none;
}
@keyframes dsh-reasoning-row-sweep {
0% { left: -300px; }
90%, 100% { left: 100%; }
}
.leading {
flex-shrink: 0;
}
@@ -102,9 +81,3 @@
padding: 4px 0 4px calc(22px + var(--dsh-content-font-delta, 0px));
min-width: 0;
}
@media (prefers-reduced-motion: reduce) {
.root[data-state='running'] .row::after {
animation: none;
}
}
@@ -1,6 +1,6 @@
/** Assistant reasoning disclosure, independent of Tool-call presentation. */
import { memo, useMemo } from 'react'
import { DisclosureRow, IconThinkOutlineRegular, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives'
import { DisclosureRow, IconThinkOutlineRegular, MarkdownText, TextShimmer } from '@deepseek-ai/dsh-client-ui-primitives'
import type { ChatViewSlotProps, UseDisclosure, UsePresentation } from '../contract/slots.ts'
import { markdownLabels } from '../markdown-labels.ts'
import a11yCss from './accessibility.module.css'
@@ -59,9 +59,11 @@ export const ReasoningRow = memo(function ReasoningRow({ text, running, usePrese
&& (running || policy.settledReasoningPreview))
const collapsedContent = useMemo(() => (
<>
<span className={css.separator} aria-hidden />
<span className={css.separator} data-shimmer-decoration aria-hidden />
<span className={css.summary} data-streaming={running || undefined}>
<span className={css.summaryText}>{summary}</span>
<span className={css.summaryText}>
<TextShimmer>{summary}</TextShimmer>
</span>
</span>
</>
), [running, summary])
@@ -87,6 +89,7 @@ export const ReasoningRow = memo(function ReasoningRow({ text, running, usePrese
chevronClassName={css.chevron}
icon={THINK_ICON}
title={t('message.think')}
running={running}
open={expanded}
expandable
expandOnRowClick
@@ -18,8 +18,8 @@
en: ddec1f6f9b018747
zh: 9555f132daf8901c
/deepseek-ai-dsh-client-ui-primitives/use-this-package/component-catalog:
en: d6d33ad4bfedf06f
zh: e131294a2d40af28
en: 91d4b79159aab5df
zh: 3a2d8461e836bb66
/deepseek-ai-dsh-client-ui-primitives/use-this-package/controls-and-icons:
en: f78bd63b167a7574
zh: 472eb7791f080ac2
+1
View File
@@ -53,6 +53,7 @@ Check this table before writing a control in a feature package. A plugin cannot
| `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 whose animations pin to document time zero so every visible loader rotates in phase. `aria-hidden`, so the render site owns the name. `appearance="step"` shows a filled check for completion and a hollow pending circle. |
| `ConnectionIndicator` | Inline connection-recovery control across outage, retry, and recovered states. |
| `DisclosureRow` | 24px compact disclosure that lays title and content side by side. Memoized with shallow prop comparison; keep callbacks and React-node props stable when their content is unchanged. |
| `TextShimmer` | One left-to-right highlight across a row’s title, separators, summary, and suffix, with a 300ms initial delay, a one-second sweep and a 500ms rest. The mask spans the content width, capped by the visible row and tilts 15° from vertical, with a flat peak and soft edges. The moving decoration stays clipped to the row without enlarging its scrollable area. Base text inherits the caller’s color, including hover changes; the theme supplies the translucent highlight. Place icons and chevrons outside `TextShimmer` so only text and separators receive the highlight. A string child uses the same retained text interface; nested `TextShimmer` instances share the outer animation. Composite children render twice while active and must have no effects or element ids. Mark separators with `data-shimmer-decoration` so their fill receives the highlight. `DisclosureRow` forwards `contentClassName` for the text area and `contentLayoutClassName` for its inner layout. The inert decorative copy is excluded from interaction, selection, and accessibility; text updates retain the animation, and reduced-motion mode keeps the static base. |
| `Modal` | Centered dialog over a page mask. A nested dialog can intercept keys with `onKeyDownCapture` before document Escape handlers. The tint and dialog fade in while backdrop blur stays fully applied, honoring reduced motion. Set `backdropBlur={false}` when the caller already blurs the source page. |
| `RiskConfirmation` | Sensitive action gated behind an explicit checkbox. |
| `Tooltip` | Hover and keyboard-focus text anchored to a cloned child; optional `delayMs` controls hover delay and `focusDelayMs` controls keyboard-focus delay, both defaulting to 0 ms. Optional `portal` rendering escapes clipping containers and ancestor stacking contexts that cap the bubble's z-index. |
@@ -53,6 +53,7 @@ kind: "package-library"
| `StateDot` | 10px 槽内的绿色 `done`、琥珀色 `warning`、红色 `error`、中性灰色 `idle` 圆点,以及 tertiary 灰色 14px 旋转 `ongoing` loading,其动画固定到文档时间零点,所以所有可见 loading 同相旋转。它是 `aria-hidden` 的,名称由渲染点提供。 `appearance="step"` 以实心勾表示完成、空心圆表示等待。 |
| `ConnectionIndicator` | 行内连接恢复控件,覆盖断线、重试与已恢复三种状态。 |
| `DisclosureRow` | 24px 紧凑折叠行,标题与内容左右排列。使用浅层 prop 比较进行 memo;内容未变时,保持回调与 React 节点 prop 的引用稳定。 |
| `TextShimmer` | 同一道高光从左到右扫过一行的标题、分隔符、摘要与后缀:首次等待 300ms,扫动一秒,再静止 500ms。遮罩覆盖内容宽度,并限制在可见行宽内,相对垂直方向倾斜 15°,峰值区域平坦,两侧柔和渐隐。移动的装饰层始终裁剪在行内,不会扩大可滚动区域。底色继承调用方的颜色,包括悬停变化;主题提供半透明高光。图标和箭头放在 `TextShimmer` 外,只有文字和分隔符参与扫光。字符串子节点沿用稳定的文字接口;嵌套的 `TextShimmer` 共用外层动画。活动期间组合子节点渲染两次,因此不得带副作用或元素 id。分隔符标记 `data-shimmer-decoration`,让填色参与扫光。`DisclosureRow` 将 `contentClassName` 传给文字区域,将 `contentLayoutClassName` 传给内部布局。惰性装饰副本不参与交互、选区和无障碍访问;文字更新保留动画,减少动态效果模式保留静态底色。 |
| `Modal` | 页面遮罩之上的居中对话框。嵌套对话框可通过 `onKeyDownCapture` 在文档级 Escape 处理器之前拦截按键。 色层与弹窗淡入,背景模糊始终完整生效,并遵循减少动态效果偏好。调用方已模糊源页面时设置 `backdropBlur={false}`。 |
| `RiskConfirmation` | 以显式复选框把关的敏感操作确认。 |
| `Tooltip` | 锚定在克隆子元素上的悬停与键盘聚焦提示;可选的 `delayMs` 控制悬停延迟,`focusDelayMs` 控制键盘聚焦延迟,均默认为 0 毫秒。可通过 `portal` 渲染到外层,避免被容器裁剪,或受祖先层叠上下文限制其 z-index。 |
@@ -11,7 +11,7 @@ export interface DisclosureRowProps {
open: boolean
expandable: boolean
onToggle: () => void
/** Animate the title while its owning operation is running. */
/** Animate the complete header while its owning operation is running. */
running?: boolean | undefined
/** Makes the complete title row the disclosure target. */
expandOnRowClick?: boolean | undefined
@@ -23,6 +23,10 @@ export interface DisclosureRowProps {
children?: ReactNode
className?: string | undefined
rowClassName?: string | undefined
/** Sizing class for the header text area, beside the leading icon. */
contentClassName?: string | undefined
/** Layout class shared by the header text and its decorative copy. */
contentLayoutClassName?: string | undefined
leadingClassName?: string | undefined
chevronClassName?: string | undefined
titleClassName?: string | undefined
@@ -48,6 +52,8 @@ export const DisclosureRow = memo(function DisclosureRow({
children,
className,
rowClassName,
contentClassName,
contentLayoutClassName,
leadingClassName,
chevronClassName,
titleClassName,
@@ -100,8 +106,10 @@ export const DisclosureRow = memo(function DisclosureRow({
{leading}
</span>
)}
<TextShimmer className={clsx(css.title, titleClassName)} active={running}>{title}</TextShimmer>
{(keepContentWhenOpen || !open) && collapsedContent}
<TextShimmer active={running} className={contentClassName} contentClassName={contentLayoutClassName}>
<TextShimmer className={clsx(css.title, titleClassName)}>{title}</TextShimmer>
{(keepContentWhenOpen || !open) && collapsedContent}
</TextShimmer>
</div>
{open && children}
</div>
@@ -1,27 +1,88 @@
.root[data-text-shimmer] {
background-image: linear-gradient(
90deg,
currentColor calc(50% - var(--dsh-text-shimmer-spread)),
color-mix(in oklab, currentColor 50%, transparent),
currentColor calc(50% + var(--dsh-text-shimmer-spread))
);
background-position: 100% center;
background-repeat: no-repeat;
background-size: 250% 100%;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: dsh-text-shimmer 1.5s cubic-bezier(0.33, 0, 0.67, 1) infinite;
.root {
position: relative;
display: inline-grid;
width: max-content;
max-width: 100%;
min-width: 0;
vertical-align: top;
color: inherit;
}
@keyframes dsh-text-shimmer {
66.6667%, 100% { background-position: 0% center; }
.content {
display: flex;
align-items: center;
min-width: 0;
}
.text {
color: inherit;
}
.text[data-shimmer-text]::after {
content: attr(data-shimmer-text);
}
.decoration {
position: absolute;
inset: 0;
/* The moving mask must not enlarge any ancestor's scrollable area. */
overflow: clip;
pointer-events: none;
user-select: none;
}
.sweep {
position: absolute;
inset: 0;
overflow: hidden;
color: var(--dsw-alias-label-shimmer);
mask-image: linear-gradient(105deg, transparent 0%, black 40% 60%, transparent 100%);
transform: translateX(-100%);
animation-name: dsh-row-shimmer-sweep;
}
/* Semantic base colors stay on the real content; the overlay supplies one tint. */
.sweep * {
color: inherit !important;
}
.sweep [data-shimmer-decoration] {
background: currentColor;
}
/* Opposite transforms keep every glyph aligned while their shared mask moves. */
.highlight {
width: 100%;
height: 100%;
transform: translateX(100%);
animation-name: dsh-row-shimmer-highlight;
}
.sweep,
.highlight {
animation-duration: 1.5s;
animation-delay: 0.3s;
animation-timing-function: steps(48, end);
animation-iteration-count: infinite;
}
@keyframes dsh-row-shimmer-sweep {
0% { transform: translateX(-100%); }
66.6667%, 100% { transform: translateX(100%); }
}
@keyframes dsh-row-shimmer-highlight {
0% { transform: translateX(100%); }
66.6667%, 100% { transform: translateX(-100%); }
}
@media (prefers-reduced-motion: reduce) {
.root[data-text-shimmer] {
background-image: none;
-webkit-text-fill-color: currentColor;
.decoration {
display: none;
}
.sweep,
.highlight {
animation: none;
}
}
@@ -1,23 +1,58 @@
/** Text-only activity animation with a stable span across lifecycle changes. */
import { memo, useMemo, type CSSProperties } from 'react'
/** Text activity animation shared by a row and its nested text fragments. */
import { createContext, memo, useContext, type ReactNode } from 'react'
import clsx from 'clsx'
import css from './TextShimmer.module.css'
const DecorativeCopy = createContext<boolean | undefined>(undefined)
/** Text and activity supplied by the owning row. */
export interface TextShimmerProps {
children: string
active: boolean
/** Text or presentational children with text in nested TextShimmer instances. No effects or element ids. */
children: ReactNode
/** Whether to animate; nested instances share the containing row's activity. */
active?: boolean | undefined
className?: string | undefined
/** Layout class applied equally to the base and decorative content. */
contentClassName?: string | undefined
}
function TextContent({ children, className }: Pick<TextShimmerProps, 'children' | 'className'>) {
const decorative = useContext(DecorativeCopy)
const generated = decorative === true && typeof children === 'string'
return (
<span className={clsx(css.text, className)} data-shimmer-text={generated ? children : undefined}>
{generated ? null : children}
</span>
)
}
/**
* Render text with an optional moving highlight; inactive text keeps the same node.
* Render text with one shared highlight while retaining selectable, accessible content.
* Nested instances inherit the outer animation. Keep icons outside; mark decorative
* separators with data-shimmer-decoration so their background follows the highlight.
* Active children also render in an inert, clipped decoration; supply only presentation.
* @param props - localized text, running state, and owner styling.
* @returns the retained text span.
* @returns retained text and its optional decorative highlight.
*/
export const TextShimmer = memo(function TextShimmer({ children, active, className }: TextShimmerProps) {
const style = useMemo(() => ({
'--dsh-text-shimmer-spread': `${children.length * 8}px`,
} as CSSProperties), [children.length])
return <span className={clsx(css.root, className)} style={style} data-text-shimmer={active || undefined}>{children}</span>
export const TextShimmer = memo(function TextShimmer({ children, active = false, className, contentClassName }: TextShimmerProps) {
const decorative = useContext(DecorativeCopy)
if (decorative !== undefined) return <TextContent className={className}>{children}</TextContent>
const content = typeof children === 'string' ? <TextContent>{children}</TextContent> : children
return (
<span className={clsx(css.root, className)} data-shimmer={active || undefined}>
<DecorativeCopy.Provider value={false}>
<span className={clsx(css.content, contentClassName)}>{content}</span>
</DecorativeCopy.Provider>
{active && (
// React 18 forwards inert as an empty string; a boolean is dropped.
<span className={css.decoration} aria-hidden="true" {...{ inert: '' }}>
<span className={css.sweep}>
<DecorativeCopy.Provider value>
<span className={clsx(css.content, css.highlight, contentClassName)}>{content}</span>
</DecorativeCopy.Provider>
</span>
</span>
)}
</span>
)
})
+2 -2
View File
@@ -21,8 +21,8 @@
en: 2ba1a6158c85b46e
zh: 9ab58ceef8743849
/deepseek-ai-dsh-client-ui-skill/use-this-package/the-skill-tool-row:
en: 52c7aee8faf26795
zh: f68e2f17cc7e369a
en: 452686249f73702d
zh: 47b0a191dfacba30
/deepseek-ai-dsh-client-ui-skill/understand-the-implementation:
en: df2a0ee9d2638f41
zh: 3b4af7e505e990ca
+1 -1
View File
@@ -35,7 +35,7 @@ Ordinary-session candidates come from the `skills/list` Remote; the host serves
The `preparing` stage shows only the skill icon and title, with no requested skill name or expansion. The `start` and `result` stages use the recorded arguments and output described below; preparation does not consult the installed catalog.
A collapsed row renders the skill glyph, `Skill` title, and requested skill name on the same secondary font-size axis, line height, row height, and glyph scaling as ordinary Tool rows. Every lifecycle state retains the ordinary skill glyph; running calls carry the transcript shimmer, while failures replace the name with the first error line and interruptions retain their explicit status text. A settled row expands into a bounded `Instructions` card containing the exact durable tool output, with the standard trajectory `Inspect` affordance when available; this expanded card keeps its own typography. The row derives its name, lifecycle, and body only from the frozen call/result slice supplied by ui-tool, never from the current catalog, so replay stays stable when installed skills or their descriptions change.
A collapsed row renders the skill glyph, `Skill` title, and requested skill name on the same secondary font-size axis, line height, row height, and glyph scaling as ordinary Tool rows. Every lifecycle state retains the ordinary skill glyph; running calls carry one left-to-right highlight across the title, separator, and skill name, while failures replace the name with the first error line and interruptions retain their explicit status text. A settled row expands into a bounded `Instructions` card containing the exact durable tool output, with the standard trajectory `Inspect` affordance when available; this expanded card keeps its own typography. The row derives its name, lifecycle, and body only from the frozen call/result slice supplied by ui-tool, never from the current catalog, so replay stays stable when installed skills or their descriptions change.
Hovering over `/name` highlights the entire reference. Clicking a known skill opens its provider-supplied `SKILL.md` path in the right Sidebar while keeping the token editable. An uncached click shares the per-Session catalog fetch and opens when it completes, retaining the clicked Session address. Preset changes, connection resets, and plugin disposal cancel pending previews; a later click fetches the current catalog again. Skills without a file path remain invocable but have no file preview.
+1 -1
View File
@@ -35,7 +35,7 @@ kind: "package-reference"
`preparing` 阶段只显示技能图标与标题,不显示请求加载的技能名称,也不能展开。`start` 和 `result` 阶段使用下述记录参数与输出;准备阶段不查询已安装目录。
收起的行显示 skill 图标、`Skill` 标题与请求加载的 skill 名称,并与普通工具行使用相同的 secondary 字号轴、行高、行高度和图标缩放。所有生命周期状态都保留普通 skill 图标;运行中的调用带有 transcript(文本记录)的扫光效果,失败时以错误首行替换名称,中断则保留明确状态文本。已结算的行展开为一个尺寸受限的 `Instructions` 卡片,其中原样呈现持久化的工具输出;可用时还会提供标准轨迹的 `Inspect` 入口,展开卡片保留自身排版。该行的名称、生命周期与正文只派生自 ui-tool 提供的冻结调用/结果切片,绝不读取当前目录,因此即使已安装的 skill 或其描述发生变化,回放仍保持稳定。
收起的行显示 skill 图标、`Skill` 标题与请求加载的 skill 名称,并与普通工具行使用相同的 secondary 字号轴、行高、行高度和图标缩放。所有生命周期状态都保留普通 skill 图标;运行中的调用以同一道高光从左到右扫过标题、分隔符与 skill 名称,失败时以错误首行替换名称,中断则保留明确状态文本。已结算的行展开为一个尺寸受限的 `Instructions` 卡片,其中原样呈现持久化的工具输出;可用时还会提供标准轨迹的 `Inspect` 入口,展开卡片保留自身排版。该行的名称、生命周期与正文只派生自 ui-tool 提供的冻结调用/结果切片,绝不读取当前目录,因此即使已安装的 skill 或其描述发生变化,回放仍保持稳定。
鼠标移入 `/name` 时,背景覆盖整个引用。点击已知 skill 会在右侧栏打开提供方给出的 `SKILL.md` 路径,同时保留文本的可编辑性。缓存未就绪时,点击复用该 Session 的目录请求,在完成后按点击时的 Session 地址打开预览。切换预设、重置连接和插件释放会取消待处理的预览;后续点击重新获取当前目录。没有文件路径的 skill 仍可调用,但没有文件预览。
@@ -18,27 +18,6 @@
cursor: pointer;
}
.card[data-state='running'] .row::after,
.card[data-state='preparing'] .row::after {
content: '';
position: absolute;
inset: 0 auto 0 0;
width: 300px;
background: linear-gradient(
90deg,
transparent 0%,
color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%,
transparent 100%
);
animation: dsh-skill-row-sweep 2.6s ease-out infinite;
pointer-events: none;
}
@keyframes dsh-skill-row-sweep {
0% { left: -300px; }
90%, 100% { left: 100%; }
}
.leading {
position: relative;
flex: none;
@@ -210,11 +189,6 @@
}
@media (prefers-reduced-motion: reduce) {
.card[data-state='running'] .row::after {
animation: none;
display: none;
}
.iconIdle,
.chevronHover,
.inspectButton {
@@ -1,6 +1,6 @@
import { useState, type KeyboardEvent, type ReactNode } from 'react'
import {
IconChevronDownOutlineRegular, IconInspectOutlineRegular, IconSkillOutlineRegular,
IconChevronDownOutlineRegular, IconInspectOutlineRegular, IconSkillOutlineRegular, TextShimmer,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { StartedToolCallViewProps, ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client'
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
@@ -104,7 +104,7 @@ export function SkillRow(props: SkillRowProps) {
<div className={css.row}>
<span className={css.leading}><IconSkillOutlineRegular size={14} /></span>
<span className={css.visuallyHidden}>{props.t('row.preparing')}</span>
<span className={css.title}>{props.t('row.title')}</span>
<TextShimmer active className={css.title}>{props.t('row.title')}</TextShimmer>
</div>
</div>
return <StartedSkillRow {...props} />
@@ -116,6 +116,7 @@ function StartedSkillRow({ block, inspect, t }: Exclude<SkillRowProps, { phase:
const expandable = model.output !== null
const open = expanded && expandable
const status = stateStatus(model.state, t)
const running = model.state === 'running'
const summary = model.state === 'stopped' ? t('row.stopped') : model.errorSummary ?? model.name
const toggleExpand = (): void => {
setExpanded(value => !value)
@@ -142,14 +143,16 @@ function StartedSkillRow({ block, inspect, t }: Exclude<SkillRowProps, { phase:
>
<span className={css.leading}>{leading}</span>
{status !== null ? <span className={css.visuallyHidden}>{status}</span> : null}
<span className={css.title}>{t('row.title')}</span>
<span className={css.separator} aria-hidden />
<span className={`${css.summary}${
model.state === 'error' ? ` ${css.errorSummary}`
: model.state === 'stopped' ? ` ${css.stoppedSummary}` : ''
}`}>
{summary}
</span>
<TextShimmer active={running}>
<span className={css.title}><TextShimmer>{t('row.title')}</TextShimmer></span>
<span className={css.separator} data-shimmer-decoration aria-hidden />
<span className={`${css.summary}${
model.state === 'error' ? ` ${css.errorSummary}`
: model.state === 'stopped' ? ` ${css.stoppedSummary}` : ''
}`}>
<TextShimmer>{summary}</TextShimmer>
</span>
</TextShimmer>
</div>
{open ? (
<div className={css.bodyWrap}>
+2 -2
View File
@@ -30,8 +30,8 @@
en: 8fbfdb050e880f9b
zh: dd201f049036c5c4
/deepseek-ai-dsh-client-ui-theme/understand-the-implementation/stylesheets:
en: b3fe877c28a600d8
zh: c8a28e4d9189c0ea
en: 76374ee500418cfa
zh: 741256fbc54c8207
/deepseek-ai-dsh-client-ui-theme/understand-the-implementation/scrollbar-rebinding:
en: bf76e7c34a331595
zh: 5accd0260ef7c61e
+2
View File
@@ -67,6 +67,8 @@ Menu icons use `--dsw-alias-menu-icon`: neutral-bluish 800 in light mode and `la
System toasts use `--dsw-alias-toast-bg` and `--dsw-alias-toast-label` for a shared background and text color across callers. Document previews pair `--dsw-alias-bg-document-preview` with `--dsw-alias-label-document-preview` so the backdrop and status text follow the same theme. Tooltip keycaps use `--dsw-alias-tooltip-key-bg`, a lighter fill derived from the tooltip background in each palette. Switch thumbs read `--dsw-alias-switch-thumb`: white in light mode and neutral-bluish 400 in dark mode, so an off switch stays lighter than its track without the glare of pure white.
`--dsw-alias-label-shimmer` supplies an overlay for the shared text shimmer: black at 30% alpha in the light palette and white at 45% alpha in the dark palette. `--dsw-alias-label-deep-diving` and `--dsw-alias-label-deep-diving-shimmer` supply the blue activity label and sweep; the dark palette uses a lighter, less saturated label with a brighter blue sweep.
`brand-font.css` exports the local Montserrat Light, Regular and Medium faces (normal style, weights 300, 400 and 500), with `montserrat-light.woff2`, `montserrat-regular.woff2`, `montserrat-medium.woff2` and its SIL Open Font License in `lib/styles/`. Desktop bundles the same stylesheet, font and license for offline welcome brand text; ordinary UI keeps its system font stack.
`corner-shape.css` smooths every rounded corner: inside `@supports (corner-shape: superellipse(1.5))` it defines `--dsw-corner-shape` and applies it to all elements and their `::before`/`::after` through the universal selector, so engines without `corner-shape` keep circular corners. Full-round shapes — `border-radius: 50%` circles and pill radii — pair `corner-shape: round` with their radius in the owning component sheet because a superellipse deforms them; the corner-shape stylesheet spec enforces that pairing across every package stylesheet.
+2
View File
@@ -67,6 +67,8 @@ kind: "package-reference"
系统提示使用 `--dsw-alias-toast-bg` 和 `--dsw-alias-toast-label`,在各调用方之间统一背景与文字颜色。文档预览配对使用 `--dsw-alias-bg-document-preview` 与 `--dsw-alias-label-document-preview`,使底色与状态文字遵循相同主题。Tooltip 键帽使用 `--dsw-alias-tooltip-key-bg`,由各主题的 tooltip 背景派生稍浅的填充。开关滑块读取 `--dsw-alias-switch-thumb`:浅色模式为纯白,深色模式为 neutral-bluish 400,使关闭态滑块比轨道更亮,同时避免纯白在暗色下过亮。
`--dsw-alias-label-shimmer` 为共享文字扫光提供叠加色:浅色配色使用 30% alpha 的黑色,深色配色使用 45% alpha 的白色。`--dsw-alias-label-deep-diving` 和 `--dsw-alias-label-deep-diving-shimmer` 提供蓝色活动文本和扫光颜色;深色主题使用更亮、饱和度更低的文字,并以更亮的蓝色扫光。
`brand-font.css` 导出本地 Montserrat Light、Regular 和 Medium 字体(正体、字重 300、400 和 500),`lib/styles/` 同时提供 `montserrat-light.woff2`、`montserrat-regular.woff2`、`montserrat-medium.woff2` 及其 SIL Open Font License。Desktop 将同一份样式表、字体和许可证打包,用于欢迎页品牌文字的离线显示;普通界面保留系统字体栈。
`corner-shape.css` 平滑所有圆角:在 `@supports (corner-shape: superellipse(1.5))` 内定义 `--dsw-corner-shape`,并通过通配选择器应用到所有元素及其 `::before`/`::after`,因此不支持 `corner-shape` 的引擎保持普通圆弧。正圆形状——`border-radius: 50%` 的圆与胶囊半径——因超级椭圆会使其变形,须在所属组件样式表中把 `corner-shape: round` 与半径声明配对;corner-shape 样式表 spec 跨全部包样式表强制这一配对。
@@ -213,6 +213,7 @@ body {
--dsw-alias-interactive-bg-hover: rgba(38, 49, 72, 0.06);
--dsw-alias-label-caption: var(--dsw-static-neutral-bluish-400);
--dsw-alias-label-deep-diving: color-mix(in srgb, var(--dsw-static-deepseek-500) 70%, var(--dsw-static-blue-950));
--dsw-alias-label-deep-diving-shimmer: color-mix(in srgb, var(--dsw-static-deepseek-500) 30%, var(--dsw-static-blue-950));
--dsw-alias-label-dimmed: var(--dsw-static-neutral-bluish-200);
--dsw-alias-label-primary-bluish: var(--dsw-static-blue-900);
--dsw-alias-label-primary-dimmed: var(--dsw-static-neutral-bluish-950);
@@ -220,6 +221,7 @@ body {
--dsw-alias-label-primary-inverted: var(--dsw-static-neutral-bluish-00);
--dsw-alias-label-primary: var(--dsw-static-neutral-bluish-1000);
--dsw-alias-label-secondary: var(--dsw-static-neutral-bluish-700);
--dsw-alias-label-shimmer: color-mix(in srgb, var(--dsw-static-neutral-1000) 30%, transparent);
--dsw-alias-label-tertiary: var(--dsw-static-neutral-bluish-600);
--dsw-alias-menu-icon: var(--dsw-static-neutral-bluish-800);
--dsw-alias-link: var(--dsw-static-deepseek-500);
@@ -326,6 +328,7 @@ body[data-ds-dark-theme] {
--dsw-alias-interactive-bg-hover: rgba(255, 255, 255, 0.08);
--dsw-alias-label-caption: var(--dsw-static-neutral-bluish-600);
--dsw-alias-label-deep-diving: color-mix(in srgb, var(--dsw-static-deepseek-450) 55%, var(--dsw-static-neutral-bluish-400));
--dsw-alias-label-deep-diving-shimmer: color-mix(in srgb, var(--dsw-static-blue-300) 65%, var(--dsw-static-deepseek-400));
--dsw-alias-label-dimmed: var(--dsw-static-neutral-bluish-750);
--dsw-alias-label-primary-bluish: var(--dsw-static-neutral-bluish-50);
--dsw-alias-label-primary-dimmed: var(--dsw-static-neutral-bluish-100);
@@ -333,6 +336,7 @@ body[data-ds-dark-theme] {
--dsw-alias-label-primary-inverted: var(--dsw-static-neutral-bluish-800);
--dsw-alias-label-primary: var(--dsw-static-neutral-bluish-50);
--dsw-alias-label-secondary: var(--dsw-static-neutral-bluish-300);
--dsw-alias-label-shimmer: color-mix(in srgb, var(--dsw-static-neutral-00) 45%, transparent);
--dsw-alias-label-tertiary: var(--dsw-static-neutral-bluish-400);
--dsw-alias-menu-icon: var(--dsw-alias-label-primary-dimmed);
--dsw-alias-link: var(--dsw-static-deepseek-400);
+2 -2
View File
@@ -21,8 +21,8 @@
en: 5332d87e76037f07
zh: db5d06e7e46b8429
/deepseek-ai-dsh-client-ui-tool/use-this-package/built-in-views:
en: 4b69e36952574494
zh: 1f9842b40931adb6
en: 53bd3369a3b868b0
zh: 030f281a5bd66c6f
/deepseek-ai-dsh-client-ui-tool/understand-the-implementation:
en: 00bc7b0c84edc603
zh: 1cfcf5b435248a91
+1 -1
View File
@@ -49,7 +49,7 @@ The owner payload is `ToolCallOwnerProps`: `callId`, `toolName`, the `phase` dis
Every registered view receives the explicit `preparing`, `start`, and `result` props declared in [the Tool slot types](src/client/contract/slots.ts). Shared rows use the same `ToolRow` in all stages. Their row model selects the title and combines any generic tool-name prefix with the available argument summary independently of lifecycle state. Tool-owned titles omit the English name. The shared argument parser returns no call during preparation and does not parse partial JSON. Write/edit use separate preparing and dispatched components, so only the preparing component invokes `useToolCallArgumentsPartial`; start and result share the dispatched component. Custom renderers such as Bash, Skill, and Cordis handle preparation separately; their argument-dependent components accept `StartedToolCallViewProps`.
This package owns the generic fallback and the built-in shell/pwsh, read, read_image, write/edit, running `str_replace_editor` `create`/`str_replace`, grep/glob, web, todo, question, and PTC dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall` and `presentResult` values never enter the Client. Running and settled foreground standard `bash`/`pwsh` and `terminal_send` calls use terminal cards at the root and in PTC dispatch children, subject to the same argument, result, and error checks. Persistent `bash`/`pwsh` calls use terminal cards only while running. Shell output ending in a recognized spill-policy notice uses expandable generic output in shell rows and generic output in Details; a displaced or omitted exit marker cannot establish success. Settled persistent-shell results stay generic because reset and partial-output diagnostics do not always describe one process exit status; root persistent results are expandable, while background acknowledgements remain collapsed. A native or PTC dispatch failure carrying `AUTO_REVIEW_DENIED` shows the Auto review verdict in its collapsed row and one normalized not-executed reason when expanded; a missing or whitespace-only reason uses localized fallback text. A successful question row pairs call questions with result answers by their stable ids and shows readable question/answer lines when expanded. A cancelled or interrupted row shows its verdict and original questions without inventing answers. Unsupported, malformed, or ambiguous inputs fall back to flattened Tool input/result text. `ui-skill` demonstrates a business-owned registration for `skill`.
This package owns the generic fallback and the built-in shell/pwsh, read, read_image, write/edit, running `str_replace_editor` `create`/`str_replace`, grep/glob, web, todo, question, and PTC dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall` and `presentResult` values never enter the Client. Running atomic rows share one left-to-right highlight across their titles, separators, summaries, and suffixes, while settled rows stay static. Running and settled foreground standard `bash`/`pwsh` and `terminal_send` calls use terminal cards at the root and in PTC dispatch children, subject to the same argument, result, and error checks. Persistent `bash`/`pwsh` calls use terminal cards only while running. Shell output ending in a recognized spill-policy notice uses expandable generic output in shell rows and generic output in Details; a displaced or omitted exit marker cannot establish success. Settled persistent-shell results stay generic because reset and partial-output diagnostics do not always describe one process exit status; root persistent results are expandable, while background acknowledgements remain collapsed. A native or PTC dispatch failure carrying `AUTO_REVIEW_DENIED` shows the Auto review verdict in its collapsed row and one normalized not-executed reason when expanded; a missing or whitespace-only reason uses localized fallback text. A successful question row pairs call questions with result answers by their stable ids and shows readable question/answer lines when expanded. A cancelled or interrupted row shows its verdict and original questions without inventing answers. Unsupported, malformed, or ambiguous inputs fall back to flattened Tool input/result text. `ui-skill` demonstrates a business-owned registration for `skill`.
-----
+1 -1
View File
@@ -49,7 +49,7 @@ owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、`phase` 判别字
每个注册视图都接收[工具 slot 类型](src/client/contract/slots.ts)声明的显式 `preparing`、`start` 和 `result` props。通用行在三个阶段使用同一个 `ToolRow`。行模型统一选择标题,并组合通用工具名前缀与已有参数摘要,不按生命周期阶段改变前缀;专用标题不附带英文名。准备阶段的共享参数解析入口直接返回无调用,不解析部分 JSON。write/edit 将准备态和派发后阶段拆成两个组件,只有准备态组件调用 `useToolCallArgumentsPartial`,start 与 result 共用派发后组件。Bash、Skill、Cordis 等自定义 renderer 分别处理准备态,其依赖参数的组件接收 `StartedToolCallViewProps`。
本包拥有 generic fallback,以及 shell/pwsh、read、read_image、write/edit、运行中的 `str_replace_editor` `create`/`str_replace`、grep/glob、web、todo、question 与 PTC dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall` 与 `presentResult` 值不会进入 Client。运行中与已完成的前台标准 `bash`/`pwsh` 和 `terminal_send` 调用,无论位于根还是 PTC dispatch 子调用中,都在通过相同的参数、结果和错误检查后使用 terminal 卡片。持久 `bash`/`pwsh` 调用仅在运行中使用 terminal 卡片。以已识别的 spill 策略提示结尾的 shell 输出,在 shell 行中使用可展开的 generic 输出,在 Details 中使用 generic 输出;位置被改变或被省略的退出标记无法证明成功。已完成的持久 shell 结果保持 generic 展示,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;根调用的持久 shell 结果可展开,后台启动回执则保持折叠。带有 `AUTO_REVIEW_DENIED` 的原生或 PTC dispatch 失败会在折叠行显示 Auto review 裁决,展开时显示一行归一化后的“未执行”原因;原因缺失或只有空白时使用本地化 fallback 文案。成功的问题行按稳定 id 配对调用中的问题与结果中的回答,展开后显示可读的问答行。已取消或已中断的问题行显示其裁决与原始问题,不虚构回答。不受支持、格式错误或含糊的输入回退为压平的工具输入/结果文本。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。
本包拥有 generic fallback,以及 shell/pwsh、read、read_image、write/edit、运行中的 `str_replace_editor` `create`/`str_replace`、grep/glob、web、todo、question 与 PTC dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall` 与 `presentResult` 值不会进入 Client。运行中的原子工具行以同一道高光从左到右扫过标题、分隔符、摘要与后缀;已完成行保持静态。运行中与已完成的前台标准 `bash`/`pwsh` 和 `terminal_send` 调用,无论位于根还是 PTC dispatch 子调用中,都在通过相同的参数、结果和错误检查后使用 terminal 卡片。持久 `bash`/`pwsh` 调用仅在运行中使用 terminal 卡片。以已识别的 spill 策略提示结尾的 shell 输出,在 shell 行中使用可展开的 generic 输出,在 Details 中使用 generic 输出;位置被改变或被省略的退出标记无法证明成功。已完成的持久 shell 结果保持 generic 展示,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;根调用的持久 shell 结果可展开,后台启动回执则保持折叠。带有 `AUTO_REVIEW_DENIED` 的原生或 PTC dispatch 失败会在折叠行显示 Auto review 裁决,展开时显示一行归一化后的“未执行”原因;原因缺失或只有空白时使用本地化 fallback 文案。成功的问题行按稳定 id 配对调用中的问题与结果中的回答,展开后显示可读的问答行。已取消或已中断的问题行显示其裁决与原始问题,不虚构回答。不受支持、格式错误或含糊的输入回退为压平的工具输入/结果文本。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。
-----
@@ -213,7 +213,7 @@ export const ToolRow = memo(function ToolRow({
/* An empty summary drops the separator with it (a row that is only
its title shows no trailing dot). */
<>
<span className={css.sep} aria-hidden />
<span className={css.sep} data-shimmer-decoration aria-hidden />
{openFile !== undefined ? (
<button
type="button"
@@ -221,7 +221,7 @@ export const ToolRow = memo(function ToolRow({
onClick={openFile}
onKeyDown={summaryLinkKeyDown}
>
<TextShimmer active={running}>{summaryText}</TextShimmer>
<TextShimmer>{summaryText}</TextShimmer>
</button>
) : linkHref !== undefined ? (
<a
@@ -232,7 +232,7 @@ export const ToolRow = memo(function ToolRow({
onClick={stopLinkClick}
onKeyDown={summaryLinkKeyDown}
>
<TextShimmer active={running}>{summaryText}</TextShimmer>
<TextShimmer>{summaryText}</TextShimmer>
</a>
) : (
<span
@@ -242,11 +242,11 @@ export const ToolRow = memo(function ToolRow({
state === 'stopped' && css.stoppedSummary,
)}
>
<TextShimmer active={running}>{summaryText}</TextShimmer>
<TextShimmer>{summaryText}</TextShimmer>
</span>
)}
{suffix !== null && (
<TextShimmer className={clsx(css.summarySuffix, suffix === diffStat && css.diffStat)} active={running}>{suffix}</TextShimmer>
<TextShimmer className={clsx(css.summarySuffix, suffix === diffStat && css.diffStat)}>{suffix}</TextShimmer>
)}
</>
), [diffStat, summaryLinkKeyDown, linkHref, openFile, running, state, suffix, summaryText])
@@ -109,15 +109,17 @@ const StartedBashRow = memo(function StartedBashRow({ toolName, block, sessionId
>
<span className={css.leading}>{leading}</span>
{status !== null && <span className={css.visuallyHidden}>{status}</span>}
<TextShimmer className={css.title} active={running}>{t(model.titleKey)}</TextShimmer>
<span className={css.sep} aria-hidden />
<span className={clsx(
css.summary,
state === 'error' && css.errorSummary,
state === 'stopped' && css.stoppedSummary,
)}>
<TextShimmer active={running}>{settlementLine ?? normalSummary}</TextShimmer>
</span>
<TextShimmer active={running}>
<TextShimmer className={css.title}>{t(model.titleKey)}</TextShimmer>
<span className={css.sep} data-shimmer-decoration aria-hidden />
<span className={clsx(
css.summary,
state === 'error' && css.errorSummary,
state === 'stopped' && css.stoppedSummary,
)}>
<TextShimmer>{settlementLine ?? normalSummary}</TextShimmer>
</span>
</TextShimmer>
</div>
{open && (
<div className={css.bodyWrap}>
@@ -264,3 +264,11 @@
padding-left: 12px;
}
}
.headerContent {
flex: 1;
}
.headerContentLayout {
gap: 6px;
}
@@ -219,6 +219,8 @@ function RunHeader({ children, count, name, onToggle, open, status, t }: {
previewChevron={false}
keepContentWhenOpen
rowClassName={css.runHeader}
contentClassName={css.headerContent}
contentLayoutClassName={css.headerContentLayout}
leadingClassName={css.runLeading}
titleClassName={css.runTitle}
collapsedContent={(
@@ -312,6 +314,8 @@ function PhaseSection({
previewChevron={false}
keepContentWhenOpen
rowClassName={css.phaseHeader}
contentClassName={css.headerContent}
contentLayoutClassName={css.headerContentLayout}
leadingClassName={css.phaseLeading}
titleClassName={css.phaseTitle}
collapsedContent={(