From fe2017af49942cb4265f0ae0df6e4df40aa1e31f Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Fri, 11 Sep 2026 15:40:32 +0800 Subject: [PATCH] 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. --- src/client/builtins/tab-icons.module.css | 2 + src/client/builtins/tab-icons.tsx | 55 ++++++++++++++++++++---- tests/builtins.spec.ts | 32 ++++++++++++++ 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/src/client/builtins/tab-icons.module.css b/src/client/builtins/tab-icons.module.css index 1b74378..0655970 100644 --- a/src/client/builtins/tab-icons.module.css +++ b/src/client/builtins/tab-icons.module.css @@ -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); } diff --git a/src/client/builtins/tab-icons.tsx b/src/client/builtins/tab-icons.tsx index 2381dc8..e0d389a 100644 --- a/src/client/builtins/tab-icons.tsx +++ b/src/client/builtins/tab-icons.tsx @@ -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 {glyph} } +/** + * 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) => ( @@ -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, ) + themed(css.changes, outlined()) -/** 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, ) + themed(css.tasks, outlined()) /** Side chat — the conversational/secondary accent. */ export const sidechatTabIcon: TabIcon = (size) => - themed(css.sidechat, ) + themed(css.sidechat, outlined()) -/** 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, ) + themed(css.terminal, outlined()) /** Browser — the same secondary accent as the side chat's sibling surfaces. */ export const browserTabIcon: TabIcon = (size) => - themed(css.browser, ) + themed(css.browser, outlined()) diff --git a/tests/builtins.spec.ts b/tests/builtins.spec.ts index 76c3a94..fab73dc 100644 --- a/tests/builtins.spec.ts +++ b/tests/builtins.spec.ts @@ -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 }> + 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', () => {