fix(icons): say "background work", not "to-do list", and lighten the terminal glyph

Two glyphs read wrong in the live instance:

- Tasks shows subagent sessions AND the host's background jobs, so a checklist
  glyph described a page that does not exist. `VscLayers` (sheets stacked, i.e.
  work running behind the conversation) says what the page is; the amber token
  stays, so it still reads as live activity.
- The terminal was the widest glyph in the set and the only bare fill in a
  strip of the plugin's own 1.5px-stroked icons, which made it look like a
  heavy foreign block. It now renders a step down (0.85x the strip size, floor
  10px) and every colored tab glyph gets a hairline outline of its own ink
  (`stroke: currentColor` + `non-scaling-stroke`, so the line stays one device
  pixel across the sets' mixed 16- and 24-unit viewBoxes; `paint-order: stroke`
  centres it on the fill edge instead of fattening the shape).

tests/builtins.spec.ts pins both readings by glyph identity (tasks = layers,
terminal = the shrunk, outlined VscTerminal), so a future swap has to argue
with a failing assertion rather than with a comment.

Gates: typecheck / lint green, pnpm test 127 files / 1343 passed / 9 skipped.
This commit is contained in:
Menghuan1918
2026-09-11 15:40:32 +08:00
parent 6317dde7c0
commit fe2017af49
3 changed files with 80 additions and 9 deletions
+2
View File
@@ -29,6 +29,8 @@
}
.terminal {
/* The shell chip is ink, not accent — and it is the widest glyph in the set,
so it renders a step smaller than its siblings (see tab-icons.tsx). */
color: var(--dsw-alias-label-primary);
}
+46 -9
View File
@@ -14,14 +14,14 @@
* `files` is the one exception in kind rather than in color: it renders DSH's
* own folder artwork (`FileTypeIcon`), matching the file rows this tab shows.
*/
import type { ReactNode } from 'react'
import { cloneElement, type ReactElement, type ReactNode } from 'react'
import { FileTypeIcon } from '@deepseek-ai/dsh-client-ui-primitives'
import styles from './tab-icons.module.css'
import {
VscCommentDiscussion,
VscGitCommit,
VscGlobe,
VscTasklist,
VscLayers,
VscTerminal,
} from 'react-icons/vsc'
/** The styled wrapper classes; typed so a renamed rule fails the build. */
@@ -35,6 +35,35 @@ function themed(className: string, glyph: ReactNode): ReactNode {
return <span className={className}>{glyph}</span>
}
/**
* Give a filled VSCodicon a hairline of its own ink.
*
* The plugin's hand-drawn icons are 1.5px strokes on a 16px grid; the
* VSCodicon set is filled at that size, and next to the plugin's own chrome a
* bare fill reads heavy and slightly out of place — most visibly on the
* terminal chip. A `currentColor` stroke in the SVG's own user units keeps the
* host's shape and the token's color while lending it the same drawn weight,
* and `paint-order: stroke` centres that hairline on the fill's edge instead
* of fattening it.
* @param icon - the glyph element to outline.
* @returns the same glyph with its outline applied.
*/
function outlined(icon: ReactElement): ReactElement {
return cloneElement(icon as ReactElement<{ style?: React.CSSProperties }>, {
style: {
// `non-scaling-stroke` keeps the hairline one device pixel whatever the
// glyph's own grid is (the sets mix 16- and 24-unit viewBoxes and the
// strip renders at 14px), and `paint-order: stroke` centres it on the
// fill's edge instead of fattening the shape.
stroke: 'currentColor',
strokeWidth: 1,
strokeLinejoin: 'round',
paintOrder: 'stroke',
vectorEffect: 'non-scaling-stroke',
},
})
}
/** The Files tab: the host's folder artwork, like the rows it opens. */
export const filesTabIcon: TabIcon = (size) => (
<span className={css.files}>
@@ -44,20 +73,28 @@ export const filesTabIcon: TabIcon = (size) => (
/** Changes / diff: the commit glyph, green like the diff affordances. */
export const changesTabIcon: TabIcon = (size) =>
themed(css.changes, <VscGitCommit size={size} />)
themed(css.changes, outlined(<VscGitCommit size={size} />))
/** Tasks (subagent) — the live-activity amber. */
/**
* Tasks (subagents and background jobs) — the live-activity amber. The glyph
* is layered sheets, not a checklist: this page lists RUNNING work (subagent
* sessions plus the host's background jobs), not a to-do list.
*/
export const tasksTabIcon: TabIcon = (size) =>
themed(css.tasks, <VscTasklist size={size} />)
themed(css.tasks, outlined(<VscLayers size={size} />))
/** Side chat — the conversational/secondary accent. */
export const sidechatTabIcon: TabIcon = (size) =>
themed(css.sidechat, <VscCommentDiscussion size={size} />)
themed(css.sidechat, outlined(<VscCommentDiscussion size={size} />))
/** Terminal — primary ink, the shell is text. */
/**
* Terminal — primary ink, the shell is text. Drawn one step down from the
* strip's 14px and with the outline above: the VSCodicon terminal is a wide
* filled rectangle that dominated the chip at full size.
*/
export const terminalTabIcon: TabIcon = (size) =>
themed(css.terminal, <VscTerminal size={size} />)
themed(css.terminal, outlined(<VscTerminal size={Math.max(10, Math.round(size * 0.85))} />))
/** Browser — the same secondary accent as the side chat's sibling surfaces. */
export const browserTabIcon: TabIcon = (size) =>
themed(css.browser, <VscGlobe size={size} />)
themed(css.browser, outlined(<VscGlobe size={size} />))
+32
View File
@@ -8,6 +8,8 @@
* tab (git lens + session lens, PR #471's file-trace merged in).
*/
import { describe, expect, it } from 'vitest'
import type { ReactElement } from 'react'
import { VscCommentDiscussion, VscGitCommit, VscGlobe, VscLayers, VscTerminal } from 'react-icons/vsc'
// First import: browser globals before the xterm-carrying builtin graph loads.
import './browser-globals.ts'
@@ -235,6 +237,36 @@ describe('built-in tab registrations', () => {
expect(tab.icon, tab.id).toBeDefined()
}
})
it('the tab glyphs say what the page shows (colored, token-driven)', () => {
const { service } = setup()
const iconOf = (id: string): ReactElement => {
const icon = service.getTab(id)?.icon
expect(icon, id).toBeDefined()
return (typeof icon === 'function' ? icon(14) : icon) as ReactElement
}
// Every colored glyph is [wrapper][glyph]; unwrap the themed wrapper.
const glyphOf = (id: string): unknown => {
const wrapper = iconOf(id) as ReactElement<{ children?: ReactElement }>
return (wrapper.props.children as ReactElement | undefined)?.type ?? wrapper.type
}
// Tasks lists subagent sessions AND background jobs — layered sheets say
// "work running in the background"; a checklist glyph would say "to-do
// list", which this page is not.
expect(glyphOf('subagent')).toBe(VscLayers)
expect(glyphOf('git')).toBe(VscGitCommit)
expect(glyphOf('sidechat')).toBe(VscCommentDiscussion)
expect(glyphOf('browser')).toBe(VscGlobe)
// The terminal glyph is the widest in the set: it renders a step down from
// the strip's size and keeps a hairline outline so it matches the plugin's
// own drawn chrome instead of reading as a heavy filled block.
const wrapper = iconOf('terminal') as ReactElement<{ children?: ReactElement }>
const terminal = wrapper.props.children as ReactElement<{ size?: number; style?: Record<string, unknown> }>
expect(terminal.type).toBe(VscTerminal)
expect(terminal.props.size).toBeLessThan(14)
expect(terminal.props.style?.strokeWidth).toBe(1)
expect(terminal.props.style?.vectorEffect).toBe('non-scaling-stroke')
})
})
describe('built-in file viewer registrations', () => {