Copy the local-build version from the sidebar brand

This commit is contained in:
Turtle committed 2026-10-19 17:33:35 +08:00
1 parent cac36ac075
commit e934beb347
6 files changed
+113 -33

No files matched your search

+1 -1
View File
@@ -31,7 +31,7 @@ The sidebar is the navigation shell: users see the brand, start new sessions, co
### Brand and New Session
The expanded brand row renders `sidebar.brand.mark` and `sidebar.brand.name` as independent single slots; the collapsed rail renders the same mark slot. On Web and Windows desktop, clicking the expanded brand starts a new session; hovering or focusing it shows no tooltip. Without occupants, the shell uses the fish mark and a localized local-build label. A complete build stacks a code badge below the label as `version[-commit][-dirty]`, using `DSH_CLIENT_VERSION`, the optional 7-character `DSH_CLIENT_COMMIT_HASH`, and `DSH_CLIENT_GIT_DIRTY=true`; missing version metadata omits the badge. New Session targets the explicit Workspace used by a scoped action, otherwise the current Session's Workspace, otherwise the most recently active Workspace; when none exists it clears into the blank New Session page. The expanded New Session button displays the effective binding as trailing grey text on hover or keyboard focus. While the shortcut is visible, the centered icon and label fade before it when space is narrow; CSS reserves the shortcut’s width without measuring the button. Icon-only controls retain tooltips with platform-formatted keycaps, including the resident macOS header control when the sidebar is hidden. All controls expose `aria-keyshortcuts`.
The expanded brand row renders `sidebar.brand.mark` and `sidebar.brand.name` as independent single slots; the collapsed rail renders the same mark slot. On Web and Windows desktop, clicking the expanded brand starts a new session; hovering or focusing it shows no tooltip. Without occupants, the shell uses the fish mark and a localized local-build label. A complete build stacks a code badge below the label as `version[-commit][-dirty]`, using `DSH_CLIENT_VERSION`, the optional 7-character `DSH_CLIENT_COMMIT_HASH`, and `DSH_CLIENT_GIT_DIRTY=true`; missing version metadata omits the badge. Clicking the fallback mark or label of such a build copies that badge text instead of starting a session, also on macOS desktop, and the badge reads Copied for one second after a successful write. New Session targets the explicit Workspace used by a scoped action, otherwise the current Session's Workspace, otherwise the most recently active Workspace; when none exists it clears into the blank New Session page. The expanded New Session button displays the effective binding as trailing grey text on hover or keyboard focus. While the shortcut is visible, the centered icon and label fade before it when space is narrow; CSS reserves the shortcut’s width without measuring the button. Icon-only controls retain tooltips with platform-formatted keycaps, including the resident macOS header control when the sidebar is hidden. All controls expose `aria-keyshortcuts`.
### Global panel entries
+1 -1
View File
@@ -31,7 +31,7 @@ dsh Web 客户端的侧边栏让用户识别当前构建、启动新会话、将
### 品牌与 New Session
展开的品牌行把 `sidebar.brand.mark` 与 `sidebar.brand.name` 渲染为两个独立的 single slot;收起轨道则渲染同一个 mark slot。在 Web 和 Windows 桌面端,点击展开态的品牌会新建会话,悬停或聚焦时不显示 tooltip。没有占位者时,外壳使用鱼形标记和本地化的本地构建标签。完整构建会在标签下方显示代码徽标;该徽标使用 `DSH_CLIENT_VERSION`、可选的 7 位 `DSH_CLIENT_COMMIT_HASH` 与 `DSH_CLIENT_GIT_DIRTY=true` 组装成 `version[-commit][-dirty]`;缺少版本元数据时不显示徽标。New Session 优先使用作用域操作明确指定的 Workspace,否则使用当前 Session 所属 Workspace,再否则使用最近活跃 Workspace;一个 Workspace 都没有时则清空选择,进入空白 New Session 页面。 展开态的新建会话按钮在悬停或键盘聚焦时于右侧以灰色文字显示当前有效绑定。快捷键可见且空间较窄时,居中的图标与文字在快捷键之前渐隐;CSS 为快捷键保留宽度,无需测量按钮。纯图标控件保留使用平台键位样式的 tooltip,侧栏隐藏时常驻的 macOS 头部控件也保持一致。所有控件均提供 `aria-keyshortcuts`。
展开的品牌行把 `sidebar.brand.mark` 与 `sidebar.brand.name` 渲染为两个独立的 single slot;收起轨道则渲染同一个 mark slot。在 Web 和 Windows 桌面端,点击展开态的品牌会新建会话,悬停或聚焦时不显示 tooltip。没有占位者时,外壳使用鱼形标记和本地化的本地构建标签。完整构建会在标签下方显示代码徽标;该徽标使用 `DSH_CLIENT_VERSION`、可选的 7 位 `DSH_CLIENT_COMMIT_HASH` 与 `DSH_CLIENT_GIT_DIRTY=true` 组装成 `version[-commit][-dirty]`;缺少版本元数据时不显示徽标。此类构建中点击回退的标记或标签会复制该徽标文本而非新建会话,macOS 桌面端同样适用;写入成功后徽标显示“复制成功”一秒。New Session 优先使用作用域操作明确指定的 Workspace,否则使用当前 Session 所属 Workspace,再否则使用最近活跃 Workspace;一个 Workspace 都没有时则清空选择,进入空白 New Session 页面。 展开态的新建会话按钮在悬停或键盘聚焦时于右侧以灰色文字显示当前有效绑定。快捷键可见且空间较窄时,居中的图标与文字在快捷键之前渐隐;CSS 为快捷键保留宽度,无需测量按钮。纯图标控件保留使用平台键位样式的 tooltip,侧栏隐藏时常驻的 macOS 头部控件也保持一致。所有控件均提供 `aria-keyshortcuts`。
### 全局面板入口
@@ -308,6 +308,14 @@
white-space: nowrap;
}
/* Local-build fallbacks copy the version, also on macOS, where the
surrounding brand is a window-drag surface. */
.copyVersion {
display: inline-flex;
cursor: pointer;
-webkit-app-region: no-drag;
}
.localBuildTitle {
font-size: 12px;
line-height: 13px;
@@ -16,10 +16,11 @@
* scrollbar indirection away while it is elsewhere, so a list the user is not
* pointing at carries no bar.
*/
import { useEffect, useRef, useState } from 'react'
import { useEffect, useRef, useState, type MouseEvent } from 'react'
import clsx from 'clsx'
import {
FishLogo, IconNewChatOutlineMedium, IconNewChatOutlineRegular, IconPanelLeftOutlineRegular, isDarwinDesktop, ShortcutKeys, Tooltip,
writeClipboard,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { InjectFace, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import type {
@@ -38,6 +39,9 @@ const COLLAPSE_SETTLE_MS = 150
*/
const SCROLLBAR_LINGER_MS = 2000
/** How long the local-build badge shows the copied label after a successful copy. */
const VERSION_COPIED_MS = 1000
/** Format complete-build metadata for the local brand badge. */
function localBuildVersion(): string | undefined {
const version = process.env.DSH_CLIENT_VERSION
@@ -170,6 +174,17 @@ export function SidebarRoot({
}, [pointerInside])
const buildVersion = localBuildVersion()
// The local-build fallbacks copy the badge text instead of starting a
// session; occupied brand slots keep the New Session click.
const [versionCopied, setVersionCopied] = useState(false)
const copyVersion = (version: string) => (event: MouseEvent): void => {
event.stopPropagation()
void writeClipboard(version).then((ok) => {
if (!ok) return
setVersionCopied(true)
window.setTimeout(() => { setVersionCopied(false) }, VERSION_COPIED_MS)
})
}
const darwinDesktop = isDarwinDesktop()
// Rail resting state is the whale mark; hovering swaps in the panel icon
@@ -222,16 +237,20 @@ export function SidebarRoot({
const identity = (
<span className={css.brandIdentity} aria-hidden="true">
<span className={css.brandMark}>
{renderSlot('sidebar.brand.mark', { size: 24 }, { fallback: <FishLogo size={24} /> })}
{renderSlot('sidebar.brand.mark', { size: 24 }, {
fallback: buildVersion === undefined
? <FishLogo size={24} />
: <span className={css.copyVersion} onClick={copyVersion(buildVersion)}><FishLogo size={24} /></span>,
})}
</span>
<span className={css.brandName}>
{renderSlot('sidebar.brand.name', {}, {
fallback: buildVersion === undefined
? <span className={css.fallbackBrandName}>{t('brand.localBuild')}</span>
: (
<span className={css.localBuildBrand}>
<span className={clsx(css.localBuildBrand, css.copyVersion)} onClick={copyVersion(buildVersion)}>
<span className={css.localBuildTitle}>{t('brand.localBuild')}</span>
<span className={css.buildVersion}>{buildVersion}</span>
<span className={css.buildVersion}>{versionCopied ? t('copied') : buildVersion}</span>
</span>
),
})}
@@ -120,14 +120,18 @@ exports[`sidebar shell snapshots > renders Windows caption controls in expanded
data-slot="sidebar.brand.mark"
style="display: contents;"
>
<svg
aria-hidden="true"
data-content="965fe321"
fill="none"
height="17.6580310880829"
viewBox="0 0 23.16 17.04"
width="24"
/>
<span
class="copyVersion"
>
<svg
aria-hidden="true"
data-content="965fe321"
fill="none"
height="17.6580310880829"
viewBox="0 0 23.16 17.04"
width="24"
/>
</span>
</div>
</span>
<span
@@ -138,7 +142,7 @@ exports[`sidebar shell snapshots > renders Windows caption controls in expanded
style="display: contents;"
>
<span
class="localBuildBrand"
class="localBuildBrand copyVersion"
>
<span
class="localBuildTitle"
@@ -372,14 +376,18 @@ exports[`sidebar shell snapshots > renders the expanded column (wordmark, capsul
data-slot="sidebar.brand.mark"
style="display: contents;"
>
<svg
aria-hidden="true"
data-content="965fe321"
fill="none"
height="17.6580310880829"
viewBox="0 0 23.16 17.04"
width="24"
/>
<span
class="copyVersion"
>
<svg
aria-hidden="true"
data-content="965fe321"
fill="none"
height="17.6580310880829"
viewBox="0 0 23.16 17.04"
width="24"
/>
</span>
</div>
</span>
<span
@@ -390,7 +398,7 @@ exports[`sidebar shell snapshots > renders the expanded column (wordmark, capsul
style="display: contents;"
>
<span
class="localBuildBrand"
class="localBuildBrand copyVersion"
>
<span
class="localBuildTitle"
@@ -515,14 +523,18 @@ exports[`sidebar shell snapshots > renders the expanded column in the default lo
data-slot="sidebar.brand.mark"
style="display: contents;"
>
<svg
aria-hidden="true"
data-content="965fe321"
fill="none"
height="17.6580310880829"
viewBox="0 0 23.16 17.04"
width="24"
/>
<span
class="copyVersion"
>
<svg
aria-hidden="true"
data-content="965fe321"
fill="none"
height="17.6580310880829"
viewBox="0 0 23.16 17.04"
width="24"
/>
</span>
</div>
</span>
<span
@@ -533,7 +545,7 @@ exports[`sidebar shell snapshots > renders the expanded column in the default lo
style="display: contents;"
>
<span
class="localBuildBrand"
class="localBuildBrand copyVersion"
>
<span
class="localBuildTitle"
@@ -176,6 +176,47 @@ describe('SidebarRoot shell', () => {
expect(screen.getByText(expected)).toBeTruthy()
})
it.each([false, true])('copies the local-build version from the fallback mark and name (darwin=%s)', async (darwin) => {
if (darwin) document.documentElement.dataset.platform = 'darwin'
vi.stubEnv('DSH_CLIENT_COMMIT_HASH', 'abcdef0')
vi.stubEnv('DSH_CLIENT_VERSION', '1.2.3')
const writeText = vi.fn<(text: string) => Promise<void>>()
.mockResolvedValueOnce(undefined)
.mockRejectedValueOnce(new Error('denied'))
.mockResolvedValueOnce(undefined)
Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } })
const startSession = vi.fn()
render(<SidebarRoot
collapsed={false} width={300}
useSessions={neverHook} useSessionStatus={useSessionStatus} useSessionRetainInfo={neverHook}
usePanelInfo={usePanelInfo} selectPanel={() => {}} usePanels={selector => selector([])} useShortcuts={selector => selector([])}
useResource={useResource} useWorkspaces={neverHook}
{...allowAllAccess}
startSession={startSession} toggleSidebar={vi.fn()} t={t}
renderSlot={((_key: string, _owner: unknown, options?: { fallback?: ReactNode }) =>
options?.fallback ?? null) as SidebarRootComponentProps['renderSlot']}
/>)
vi.useFakeTimers()
await act(async () => { fireEvent.click(screen.getByText('DSH Local Build')) })
expect(writeText).toHaveBeenLastCalledWith('1.2.3-abcdef0')
expect(screen.getByText('Copied')).toBeTruthy()
act(() => { vi.advanceTimersByTime(1000) })
expect(screen.getByText('1.2.3-abcdef0')).toBeTruthy()
// A refused write claims no success.
await act(async () => { fireEvent.click(screen.getByText('1.2.3-abcdef0')) })
expect(screen.queryByText('Copied')).toBeNull()
// The fallback mark copies too; the rail toggle's mark is not in the brand row.
const mark = screen.getByText('DSH Local Build').closest('[aria-hidden]')!.querySelector('svg')!
await act(async () => { fireEvent.click(mark) })
expect(writeText).toHaveBeenCalledTimes(3)
expect(screen.getByText('Copied')).toBeTruthy()
expect(startSession).not.toHaveBeenCalled()
Reflect.deleteProperty(navigator, 'clipboard')
})
it('retains the local-build fallback without complete build metadata', () => {
render(<SidebarRoot
collapsed={false} width={300}