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', () => {