Browse Source

feat(ui): extend the content font-size axis to flow chrome

Adopt --dsh-content-font-size / --dsh-content-font-delta across the flow
rows around the transcript body: DisclosureRow header (row height, title,
leading box, and registered glyphs, with StateDot exempt), ToolRow and
bash-row summaries and file links, think text, compaction/context/retry/
error rows, message clock and icon actions, the workflow-run panel, and
the workspace browser. Update the font-size Agent Note and add CSS-text
specs for the new adoptions.
yx.zhang 1 month ago
parent
commit
9ecd18e986
22 changed files with 387 additions and 101 deletions
  1. 2 2
      .agents/notes/implemented/feature/2026-08-18-settings-font-size-control.i18n.yaml
  2. 1 1
      .agents/notes/implemented/feature/2026-08-18-settings-font-size-control.md
  3. 1 1
      .agents/notes/implemented/feature/2026-08-18-settings-font-size-control.zh.md
  4. 3 1
      packages/client/ui-chat/src/client/chat/AssistantMarkdown.module.css
  5. 14 5
      packages/client/ui-chat/src/client/chat/ChatView.module.css
  6. 7 6
      packages/client/ui-chat/src/client/chat/ContextInjectionRow.module.css
  7. 2 2
      packages/client/ui-chat/src/client/chat/GenericCommandCard.module.css
  8. 14 7
      packages/client/ui-chat/src/client/chat/MessageIconActions.module.css
  9. 30 18
      packages/client/ui-chat/src/client/chat/MessageItem.module.css
  10. 8 5
      packages/client/ui-chat/src/client/chat/ReasoningRow.module.css
  11. 2 2
      packages/client/ui-chat/src/client/chat/StatsLine.module.css
  12. 3 2
      packages/client/ui-chat/src/client/chat/TurnUsageDisclosure.module.css
  13. 95 0
      packages/client/ui-chat/tests/chat-font-axis-styles.client.spec.ts
  14. 14 4
      packages/client/ui-message-feedback/src/client/MessageFeedbackActions.module.css
  15. 10 0
      packages/client/ui-message-feedback/tests/styles.client.spec.ts
  16. 19 6
      packages/client/ui-primitives/src/DisclosureRow.module.css
  17. 47 0
      packages/client/ui-primitives/tests/disclosure-row-styles.client.spec.ts
  18. 11 8
      packages/client/ui-tool/src/client/tool/components/ToolRow.module.css
  19. 19 7
      packages/client/ui-tool/src/client/tool/toolviews/bash-sample.module.css
  20. 11 0
      packages/client/ui-tool/tests/tool-row-styles.client.spec.ts
  21. 26 24
      packages/client/ui-workflow-run/src/client/WorkflowRunPanel.module.css
  22. 48 0
      packages/client/ui-workflow-run/tests/panel-styles.client.spec.ts

+ 2 - 2
.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-18-settings-font-size-control.md
-2026-08-18-settings-font-size-control.md: 56c4d2d1ba72af5096c7d44266d27143362daf51
-2026-08-18-settings-font-size-control.zh.md: a195c855de1f007e23c460e9518f6d48dc2e41c0
+2026-08-18-settings-font-size-control.md: b8141047a8a024069094a6c3ea07e592914f2ec1
+2026-08-18-settings-font-size-control.zh.md: b7ad80540d394de490748dbc03938d8c1721d4d6

+ 1 - 1
.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.md

@@ -14,7 +14,7 @@ The conversation's body text size was fixed (14px after the 0.875 markdown-ladde
 
 **Presentation rides the existing snapshot pipeline.** The service never touches the DOM: ui-layout's `ThemePresenter` writes `--dsh-content-font-size` on `body` from each snapshot (and retracts it on dispose), and the Host boot script embeds the durable value in the index response so first paint uses the chosen size — the same pre-plugin path the dark-mode attribute takes, avoiding a font-size flash.
 
-**One CSS delta variable moves the ladder.** `gradient-shadow-text.css` derives `--dsh-content-font-delta: calc(var(--dsh-content-font-size, 14px) - 14px)` and shifts the markdown h1–h4 and base variants (size and line height) by that same px increment, preserving the heading hierarchy and each variant's leading. Table, small, and code variants stay fixed: they are dense secondary text whose 12–13px defaults would collide with the base size when stepped down. Consumers outside the token ladder — the assistant narration root, the user bubble, and the composer card (whose textarea/mirror/backdrop stack inherits font metrics from the card by design) — read `var(--dsh-content-font-size, 14px)` and `calc(<default line-height> + var(--dsh-content-font-delta, 0px))` directly. The 14px fallbacks keep every surface pixel-identical when the variable is absent (tests, storybook-like mounts, remote compositions before adoption).
+**One CSS delta variable moves the ladder.** `gradient-shadow-text.css` derives `--dsh-content-font-delta: calc(var(--dsh-content-font-size, 14px) - 14px)` and shifts the markdown h1–h4 and base variants (size and line height) by that same px increment, preserving the heading hierarchy and each variant's leading. Table, small, and code variants stay fixed — as does the interrupted-turn `.stopped` tag (11px): they are dense secondary text whose defaults would fall below legibility when stepped down. Consumers outside the token ladder read `var(--dsh-content-font-size, 14px)` (or `calc(<own default> + var(--dsh-content-font-delta, 0px))` for smaller steps) and `calc(<default line-height> + var(--dsh-content-font-delta, 0px))` directly: the assistant narration root, the user bubble (reference summaries and their inline glyphs included), the composer card (whose textarea/mirror/backdrop stack inherits font metrics from the card by design), and the flow chrome around them — the shared DisclosureRow header (tool calls, think, commands; row height, title, and leading box all move) with its expanded bodies' `22px + delta` indent keeping content aligned under the shifted title start, ToolRow/bash-row summaries and file links, think text (12px keeping its 2px step under the body), compaction/context/retry/error rows, StatsLine, the chat hint and open-error strips, the workflow-run panel (run/phase headers and expanded member rows), the message clock and icon actions (slot-injected message-feedback actions match through the same variables), and the turn status line. Flow icons scale through each leading box's CSS edge (`svg` width/height overriding the glyph attributes); StateDot is exempt via its `data-state` attribute — a status mark, not text furniture. The 14px fallbacks keep every surface pixel-identical when the variable is absent (tests, storybook-like mounts, remote compositions before adoption).
 
 **The stepper is a pill, not a menu.** The row reuses the selector-pill geometry (h36 r18 module fill) with the value centered in the pill, the up/down arrow column revealed on hover/focus-within and absolutely anchored to the pill's right edge (so revealing never moves the value), and a `px` unit label after the pill. Arrows disable at the bounds; the display follows the store mirror, never the click echo — the same store/face pattern as the Appearance row.
 

+ 1 - 1
.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.zh.md

@@ -14,7 +14,7 @@
 
 **呈现走既有快照管线。**服务绝不接触 DOM:ui-layout 的 `ThemePresenter` 依据每份快照在 `body` 上写 `--dsh-content-font-size`(dispose 时收回),Host 引导脚本把持久化值嵌入 index 响应,让首帧就使用所选字号——与暗色属性同一条插件前路径,避免字号闪变。
 
-**一个 CSS 增量变量平移阶梯。**`gradient-shadow-text.css` 派生 `--dsh-content-font-delta: calc(var(--dsh-content-font-size, 14px) - 14px)`,把 markdown h1–h4 与 base 各变体(字号与行高)按同一像素增量平移,保持标题层级与各变体的行距。表格、small 与 code 变体保持固定:它们是密集次级文本,其 12–13px 默认值在字号下调时会与正文相撞。token 阶梯之外的消费方——助手正文根节点、用户气泡、composer 卡片(其 textarea/mirror/backdrop 三层按设计从卡片继承字体度量)——直接读取 `var(--dsh-content-font-size, 14px)` 与 `calc(<默认行高> + var(--dsh-content-font-delta, 0px))`。14px 回退让变量缺席时(测试、独立挂载、采纳前的远程组合)所有表面逐像素不变。
+**一个 CSS 增量变量平移阶梯。**`gradient-shadow-text.css` 派生 `--dsh-content-font-delta: calc(var(--dsh-content-font-size, 14px) - 14px)`,把 markdown h1–h4 与 base 各变体(字号与行高)按同一像素增量平移,保持标题层级与各变体的行距。表格、small 与 code 变体保持固定——中断回合的 `.stopped` 标签(11px)同样固定:它们是密集次级文本,其默认值在字号下调时会低于可读下限。token 阶梯之外的消费方直接读取 `var(--dsh-content-font-size, 14px)`(较小档位则用 `calc(<自身默认> + var(--dsh-content-font-delta, 0px))`)与 `calc(<默认行高> + var(--dsh-content-font-delta, 0px))`:助手正文根节点、用户气泡(含引用摘要及其行内字形)、composer 卡片(其 textarea/mirror/backdrop 三层按设计从卡片继承字体度量),以及围绕它们的流内 chrome——共享的 DisclosureRow 头部(工具调用、think、命令;行高、标题与 leading 盒同步移动),其展开内容以 `22px + delta` 缩进跟随平移后的标题起点保持对齐、ToolRow/bash 行的 summary 与文件链接、think 正文(12px,保持比正文小 2px 的层级)、compaction/context/retry/错误行、StatsLine、chat 提示与打开失败条、workflow-run 面板(run/phase 头部与展开的成员行)、消息时钟与图标操作(slot 注入的消息反馈操作经同一对变量同步缩放),以及回合状态行。流内图标经由各 leading 盒的 CSS 边长缩放(`svg` width/height 覆盖字形自身属性);StateDot 通过其 `data-state` 属性豁免——它是状态标记,不是文字组件。14px 回退让变量缺席时(测试、独立挂载、采纳前的远程组合)所有表面逐像素不变。
 
 **步进器是药丸控件,不是菜单。**该行复用选择器药丸几何(h36 r18 模块填充),数值在药丸内居中,上下箭头列在 hover/focus-within 时显示并绝对定位在药丸右缘(显示时数值不移动),药丸后带 `px` 单位标签。到达边界时对应箭头禁用;显示跟随 store 镜像,绝不跟随点击回声——与外观行相同的 store/face 模式。
 

+ 3 - 1
packages/client/ui-chat/src/client/chat/AssistantMarkdown.module.css

@@ -42,7 +42,9 @@
   padding-left: var(--dsh-table-lead);
 }
 
-/* Interrupted-turn terminal marker: quiet inline tag, no animation. */
+/* Interrupted-turn terminal marker: quiet inline tag, no animation. Fixed
+   size like the small/code token variants — 11px is dense secondary text
+   that would fall to an illegible 9px at the 12px floor. */
 .stopped {
   align-self: flex-start;
   padding: 0 6px;

+ 14 - 5
packages/client/ui-chat/src/client/chat/ChatView.module.css

@@ -71,8 +71,15 @@
   flex: none;
   display: inline-flex;
   align-items: center;
-  height: 26px;
+  height: calc(26px + var(--dsh-content-font-delta, 0px));
   font: var(--dsw-font-s-strong-14);
+  /* The status line reads at the body size: the fixed 14 token supplies the
+     family/weight, the axis supplies the size. At default that is 14px text
+     on a 22px line inside a 26px row; both box heights shift by the same
+     delta, preserving the 8px (22 − 14) leading and the 4px (26 − 22) row
+     padding at every size. */
+  font-size: var(--dsh-content-font-size, 14px);
+  line-height: calc(22px + var(--dsh-content-font-delta, 0px));
   white-space: nowrap;
   background: linear-gradient(
     90deg,
@@ -94,6 +101,8 @@
 .turnStatusClock {
   margin-left: 8px;
   font: var(--dsw-font-xs-13);
+  font-size: calc(13px + var(--dsh-content-font-delta, 0px));
+  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
   font-weight: 400;
   font-variant-numeric: tabular-nums;
   color: var(--dsw-alias-label-caption);
@@ -116,14 +125,14 @@
 
 .hint {
   color: var(--dsw-alias-label-tertiary);
-  font-size: 12px;
-  line-height: 18px;
+  font-size: calc(12px + var(--dsh-content-font-delta, 0px));
+  line-height: calc(18px + var(--dsh-content-font-delta, 0px));
 }
 
 .openError {
   color: var(--dsw-alias-state-error-primary);
-  font-size: 12px;
-  line-height: 18px;
+  font-size: calc(12px + var(--dsh-content-font-delta, 0px));
+  line-height: calc(18px + var(--dsh-content-font-delta, 0px));
 }
 
 .older {

+ 7 - 6
packages/client/ui-chat/src/client/chat/ContextInjectionRow.module.css

@@ -29,8 +29,8 @@
   min-width: 0;
   overflow: hidden;
   color: var(--dsw-alias-label-tertiary);
-  font-size: 14px;
-  line-height: 24px;
+  font-size: var(--dsh-content-font-size, 14px);
+  line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;
 }
@@ -41,17 +41,18 @@
   min-width: 0;
   overflow: hidden;
   color: var(--dsw-alias-label-tertiary);
-  font-size: 14px;
-  line-height: 24px;
+  font-size: var(--dsh-content-font-size, 14px);
+  line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;
 }
 
 .body {
   box-sizing: border-box;
-  width: calc(100% - 22px);
+  /* Indent under the title, which starts at leading (16 + delta) + gap 6. */
+  width: calc(100% - 22px - var(--dsh-content-font-delta, 0px));
   max-height: 141px;
-  margin: 4px 0 0 22px;
+  margin: 4px 0 0 calc(22px + var(--dsh-content-font-delta, 0px));
   overflow: auto;
   padding: 10px 16px 12px 12px;
   border: none;

+ 2 - 2
packages/client/ui-chat/src/client/chat/GenericCommandCard.module.css

@@ -55,8 +55,8 @@
   overflow: hidden;
   flex: 1 1 auto;
   color: var(--dsw-alias-label-tertiary);
-  font-size: 14px;
-  line-height: 24px;
+  font-size: var(--dsh-content-font-size, 14px);
+  line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;
 }

+ 14 - 7
packages/client/ui-chat/src/client/chat/MessageIconActions.module.css

@@ -6,22 +6,22 @@
   display: flex;
   align-items: center;
   gap: 10px;
-  height: 28px;
+  height: calc(28px + var(--dsh-content-font-delta, 0px));
 }
 
 /* Clock before icons (user figma 388:20051) / after (assistant 43:32997). */
 .timeStart {
   padding-right: 12px;
-  font-size: 14px;
-  line-height: 24px;
+  font-size: var(--dsh-content-font-size, 14px);
+  line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-tertiary);
   white-space: nowrap;
 }
 
 .timeEnd {
   padding-left: 12px;
-  font-size: 14px;
-  line-height: 24px;
+  font-size: var(--dsh-content-font-size, 14px);
+  line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-tertiary);
   white-space: nowrap;
 }
@@ -50,8 +50,8 @@
   display: inline-flex;
   align-items: center;
   justify-content: center;
-  width: 28px;
-  height: 28px;
+  width: calc(28px + var(--dsh-content-font-delta, 0px));
+  height: calc(28px + var(--dsh-content-font-delta, 0px));
   padding: 6px;
   border: none;
   border-radius: 28px;
@@ -60,6 +60,13 @@
   cursor: pointer;
 }
 
+/* The 16px action glyphs follow the same px delta as the text they serve;
+   the CSS edge overrides each svg's own width/height attributes. */
+.action svg {
+  width: calc(16px + var(--dsh-content-font-delta, 0px));
+  height: calc(16px + var(--dsh-content-font-delta, 0px));
+}
+
 .action:hover {
   background: var(--dsw-alias-interactive-bg-hover);
   color: var(--dsw-alias-label-secondary);

+ 30 - 18
packages/client/ui-chat/src/client/chat/MessageItem.module.css

@@ -38,8 +38,8 @@
 
 .referenceSummary {
   color: var(--dsw-alias-label-tertiary);
-  font-size: 12px;
-  line-height: 18px;
+  font-size: calc(12px + var(--dsh-content-font-delta, 0px));
+  line-height: calc(18px + var(--dsh-content-font-delta, 0px));
 }
 .contextRow {
   padding: 2px 0;
@@ -56,7 +56,7 @@
   display: flex;
   align-items: center;
   width: 100%;
-  height: 24px;
+  height: calc(24px + var(--dsh-content-font-delta, 0px));
   min-width: 0;
   padding: 0;
   border: none;
@@ -79,12 +79,19 @@
   flex: none;
   display: inline-grid;
   place-items: center;
-  width: 16px;
-  height: 16px;
+  width: calc(16px + var(--dsh-content-font-delta, 0px));
+  height: calc(16px + var(--dsh-content-font-delta, 0px));
   margin-right: 6px;
   color: var(--dsw-alias-label-secondary);
 }
 
+/* Same glyph-scaling rule as DisclosureRow's leading: the CSS edge overrides
+   the svg attributes so the 14px glyphs follow the font-size delta. */
+.compactionLeading svg {
+  width: calc(14px + var(--dsh-content-font-delta, 0px));
+  height: calc(14px + var(--dsh-content-font-delta, 0px));
+}
+
 .compactionContextIcon,
 .compactionDisclosureIcon {
   display: inline-flex;
@@ -109,8 +116,8 @@
 
 .compactionTitle {
   flex: none;
-  font-size: 14px;
-  line-height: 24px;
+  font-size: var(--dsh-content-font-size, 14px);
+  line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-primary-dimmed);
 }
 
@@ -128,23 +135,24 @@
   min-width: 0;
   overflow: hidden;
   color: var(--dsw-alias-label-tertiary);
-  font-size: 14px;
-  line-height: 24px;
+  font-size: var(--dsh-content-font-size, 14px);
+  line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;
 }
 
 .compactionBody {
-  padding: 4px 0 4px 22px;
+  /* Indent under the title, which starts at leading (16 + delta) + gap 6. */
+  padding: 4px 0 4px calc(22px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-tertiary);
-  font-size: 14px;
-  line-height: 24px;
+  font-size: var(--dsh-content-font-size, 14px);
+  line-height: calc(24px + var(--dsh-content-font-delta, 0px));
 }
 
 .retryRow {
   color: var(--dsw-alias-label-tertiary);
-  font-size: 13px;
-  line-height: 20px;
+  font-size: calc(13px + var(--dsh-content-font-delta, 0px));
+  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
 }
 
 .retrySummary {
@@ -215,8 +223,8 @@
   margin-top: 3px;
   padding-left: 14px;
   overflow-wrap: anywhere;
-  font-size: 12px;
-  line-height: 18px;
+  font-size: calc(12px + var(--dsh-content-font-delta, 0px));
+  line-height: calc(18px + var(--dsh-content-font-delta, 0px));
 }
 
 .retryDetailLabel {
@@ -229,8 +237,8 @@
   gap: 8px;
   align-items: start;
   padding: 2px 0;
-  font-size: 13px;
-  line-height: 20px;
+  font-size: calc(13px + var(--dsh-content-font-delta, 0px));
+  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
 }
 
 .turnErrorDot {
@@ -294,6 +302,10 @@
   vertical-align: baseline;
 }
 
+/* Inline reference glyphs (always ReferenceIcon svgs) ride the bubble text
+   size: the 16px svg edge moves by the same px delta as the surrounding font. */
 .refIcon {
   flex: none;
+  width: calc(16px + var(--dsh-content-font-delta, 0px));
+  height: calc(16px + var(--dsh-content-font-delta, 0px));
 }

+ 8 - 5
packages/client/ui-chat/src/client/chat/ReasoningRow.module.css

@@ -50,13 +50,15 @@
   background: var(--dsw-alias-label-caption);
 }
 
+/* Think text sits one 2px step under the body size and follows the Settings
+   font-size preference through the same px delta, keeping the step. */
 .summary {
   min-width: 0;
   overflow: hidden;
   flex: 1 1 auto;
   color: var(--dsw-alias-label-tertiary);
-  font-size: 12px;
-  line-height: 20px;
+  font-size: calc(12px + var(--dsh-content-font-delta, 0px));
+  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;
 }
@@ -66,10 +68,11 @@
 }
 
 .thinkBody {
-  padding: 4px 0 4px 22px;
+  /* Indent under the title, which starts at leading (16 + delta) + gap 6. */
+  padding: 4px 0 4px calc(22px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-tertiary);
-  font-size: 12px;
-  line-height: 20px;
+  font-size: calc(12px + var(--dsh-content-font-delta, 0px));
+  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
   white-space: pre-wrap;
   word-break: break-word;
 }

+ 2 - 2
packages/client/ui-chat/src/client/chat/StatsLine.module.css

@@ -11,8 +11,8 @@
   margin: 0 auto;
   box-sizing: border-box;
   padding: 4px calc(var(--dsh-composer-side-clearance) + 16px) 0px;
-  font-size: 12px;
-  line-height: 20px;
+  font-size: calc(12px + var(--dsh-content-font-delta, 0px));
+  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-tertiary);
   white-space: nowrap;
   overflow: hidden;

+ 3 - 2
packages/client/ui-chat/src/client/chat/TurnUsageDisclosure.module.css

@@ -41,8 +41,9 @@
   grid-template-columns: minmax(76px, auto) minmax(0, 1fr);
   gap: 6px 16px;
   box-sizing: border-box;
-  width: calc(100% - 22px);
-  margin: 4px 0 0 22px;
+  /* Indent under the title, which starts at leading (16 + delta) + gap 6. */
+  width: calc(100% - 22px - var(--dsh-content-font-delta, 0px));
+  margin: 4px 0 0 calc(22px + var(--dsh-content-font-delta, 0px));
   padding: 10px 16px 12px 12px;
   border-radius: 8px;
   background: var(--dsw-alias-markdown-code-block);

+ 95 - 0
packages/client/ui-chat/tests/chat-font-axis-styles.client.spec.ts

@@ -0,0 +1,95 @@
+/**
+ * The chat flow's font-size-axis adoption as CSS text. jsdom has no layout,
+ * so these read the declarations that make think text, compaction rows, the
+ * message clock, and the icon-action buttons follow the Settings font-size
+ * preference through --dsh-content-font-size / --dsh-content-font-delta.
+ */
+import { readFileSync } from 'node:fs'
+import { fileURLToPath } from 'node:url'
+import { describe, expect, it } from 'vitest'
+
+const read = (name: string): string =>
+  readFileSync(fileURLToPath(new URL(`../src/client/chat/${name}`, import.meta.url)), 'utf8')
+
+function declarationsFrom(source: string, selector: string): string[] {
+  const declarationText = source.replace(/\/\*[\s\S]*?\*\//g, ' ')
+  const rule = new RegExp(`(?:^|\\})\\s*${selector.replace(/[.[\]():*+^$\\]/g, '\\$&')}\\s*\\{([^{}]*)\\}`).exec(declarationText)
+  if (rule === null) throw new Error(`no \`${selector}\` rule`)
+  return (rule[1] ?? '').split(';').map(part => part.trim()).filter(Boolean)
+}
+
+describe('chat flow font-size axis', () => {
+  it('think text keeps its 2px step under the body size and moves with the delta', () => {
+    const css = read('ReasoningRow.module.css')
+    for (const selector of ['.summary', '.thinkBody']) {
+      expect(declarationsFrom(css, selector)).toEqual(expect.arrayContaining([
+        'font-size: calc(12px + var(--dsh-content-font-delta, 0px))',
+        'line-height: calc(20px + var(--dsh-content-font-delta, 0px))',
+      ]))
+    }
+  })
+
+  it('command and context summaries ride the axis at the body size', () => {
+    expect(declarationsFrom(read('GenericCommandCard.module.css'), '.summary')).toEqual(expect.arrayContaining([
+      'font-size: var(--dsh-content-font-size, 14px)',
+      'line-height: calc(24px + var(--dsh-content-font-delta, 0px))',
+    ]))
+    const context = read('ContextInjectionRow.module.css')
+    for (const selector of ['.source', '.summary']) {
+      expect(declarationsFrom(context, selector)).toEqual(expect.arrayContaining([
+        'font-size: var(--dsh-content-font-size, 14px)',
+        'line-height: calc(24px + var(--dsh-content-font-delta, 0px))',
+      ]))
+    }
+  })
+
+  it('the message clock and action glyphs scale with the text they serve', () => {
+    const actions = read('MessageIconActions.module.css')
+    for (const selector of ['.timeStart', '.timeEnd']) {
+      expect(declarationsFrom(actions, selector)).toEqual(expect.arrayContaining([
+        'font-size: var(--dsh-content-font-size, 14px)',
+      ]))
+    }
+    expect(declarationsFrom(actions, '.action svg')).toEqual(expect.arrayContaining([
+      'width: calc(16px + var(--dsh-content-font-delta, 0px))',
+      'height: calc(16px + var(--dsh-content-font-delta, 0px))',
+    ]))
+  })
+
+  it('compaction rows follow the axis like the disclosure rows they mirror', () => {
+    const css = read('MessageItem.module.css')
+    for (const selector of ['.compactionTitle', '.compactionSummary', '.compactionBody']) {
+      expect(declarationsFrom(css, selector)).toEqual(expect.arrayContaining([
+        'font-size: var(--dsh-content-font-size, 14px)',
+        'line-height: calc(24px + var(--dsh-content-font-delta, 0px))',
+      ]))
+    }
+    expect(declarationsFrom(css, '.compactionLeading svg')).toEqual(expect.arrayContaining([
+      'width: calc(14px + var(--dsh-content-font-delta, 0px))',
+      'height: calc(14px + var(--dsh-content-font-delta, 0px))',
+    ]))
+  })
+
+  it('expanded bodies indent by 22px + delta so content stays under the shifted title start', () => {
+    // The DisclosureRow title starts at leading (16 + delta) + gap 6; a fixed
+    // 22px indent would misalign at every non-default size.
+    const indent = 'calc(22px + var(--dsh-content-font-delta, 0px))'
+    expect(declarationsFrom(read('ReasoningRow.module.css'), '.thinkBody'))
+      .toEqual(expect.arrayContaining([`padding: 4px 0 4px ${indent}`]))
+    expect(declarationsFrom(read('MessageItem.module.css'), '.compactionBody'))
+      .toEqual(expect.arrayContaining([`padding: 4px 0 4px ${indent}`]))
+    expect(declarationsFrom(read('ContextInjectionRow.module.css'), '.body'))
+      .toEqual(expect.arrayContaining([`margin: 4px 0 0 ${indent}`]))
+    expect(declarationsFrom(read('TurnUsageDisclosure.module.css'), '.details'))
+      .toEqual(expect.arrayContaining([`margin: 4px 0 0 ${indent}`]))
+  })
+
+  it('the interrupted-turn tag stays fixed like the dense token variants', () => {
+    // 11px would fall to an illegible 9px at the 12px floor; the tag is
+    // exempt from the axis the same way small/code tokens are.
+    expect(declarationsFrom(read('AssistantMarkdown.module.css'), '.stopped')).toEqual(expect.arrayContaining([
+      'font-size: 11px',
+      'line-height: 18px',
+    ]))
+  })
+})

+ 14 - 4
packages/client/ui-message-feedback/src/client/MessageFeedbackActions.module.css

@@ -2,8 +2,11 @@
   display: inline-flex;
   align-items: center;
   justify-content: center;
-  width: 28px;
-  height: 28px;
+  /* Rides the Settings font-size axis like the host row's own actions
+     (ui-chat MessageIconActions), so slot-injected buttons match their
+     siblings at every size. */
+  width: calc(28px + var(--dsh-content-font-delta, 0px));
+  height: calc(28px + var(--dsh-content-font-delta, 0px));
   padding: 6px;
   border: none;
   border-radius: 28px;
@@ -12,6 +15,13 @@
   cursor: pointer;
 }
 
+/* The 16px glyphs follow the same px delta as the text they serve; the CSS
+   edge overrides each svg's own width/height attributes. */
+.action svg {
+  width: calc(16px + var(--dsh-content-font-delta, 0px));
+  height: calc(16px + var(--dsh-content-font-delta, 0px));
+}
+
 .action:hover {
   background: var(--dsw-alias-interactive-bg-hover);
   color: var(--dsw-alias-label-secondary);
@@ -36,8 +46,8 @@
   border-radius: 14px;
   background: transparent;
   color: var(--dsw-alias-label-tertiary);
-  font-size: 13px;
-  line-height: 28px;
+  font-size: calc(13px + var(--dsh-content-font-delta, 0px));
+  line-height: calc(28px + var(--dsh-content-font-delta, 0px));
   white-space: nowrap;
   text-overflow: ellipsis;
   cursor: pointer;

+ 10 - 0
packages/client/ui-message-feedback/tests/styles.client.spec.ts

@@ -90,4 +90,14 @@ describe('MessageFeedbackActions theme styles', () => {
     const bare = css.replace(/\/\*[\s\S]*?\*\//g, '')
     expect((bare.match(/\}/g) ?? []).length).toBe((bare.match(/\{/g) ?? []).length)
   })
+
+  it('slot-injected actions ride the content font-size axis like their host row', () => {
+    // These buttons render inside ui-chat's MessageIconActions row; a fixed
+    // 28px would leave them undersized (or overflowing) once the Settings
+    // font size moves the row.
+    expect(block('.action')).toMatch(/width:\s*calc\(28px \+ var\(--dsh-content-font-delta, 0px\)\)/)
+    expect(block('.action')).toMatch(/height:\s*calc\(28px \+ var\(--dsh-content-font-delta, 0px\)\)/)
+    expect(block('.action svg')).toMatch(/width:\s*calc\(16px \+ var\(--dsh-content-font-delta, 0px\)\)/)
+    expect(block('.noteOpen')).toMatch(/line-height:\s*calc\(28px \+ var\(--dsh-content-font-delta, 0px\)\)/)
+  })
 })

+ 19 - 6
packages/client/ui-primitives/src/DisclosureRow.module.css

@@ -1,4 +1,8 @@
-/* Shared disclosure header: [16px leading] gap 6 [title 14/24]. */
+/* Shared disclosure header: [16px leading] gap 6 [title 14/24] at the default
+   size. The Settings font-size preference moves the row through the
+   body-published axis: title size follows --dsh-content-font-size, and the row
+   height, leading box, and glyph edge shift by the same px delta so the icon
+   keeps its optical share of the line. */
 
 .root {
   display: flex;
@@ -12,7 +16,7 @@
   overflow: hidden;
   display: flex;
   align-items: center;
-  height: 24px;
+  height: calc(24px + var(--dsh-content-font-delta, 0px));
   min-width: 0;
 }
 
@@ -23,8 +27,8 @@
 .leading {
   position: relative;
   flex: none;
-  width: 16px;
-  height: 16px;
+  width: calc(16px + var(--dsh-content-font-delta, 0px));
+  height: calc(16px + var(--dsh-content-font-delta, 0px));
   display: inline-flex;
   align-items: center;
   justify-content: center;
@@ -35,6 +39,15 @@
   color: var(--dsw-alias-label-tertiary);
 }
 
+/* Flow-row glyphs render at 14px inside the 16px box; the CSS edge overrides
+   each svg's own width/height attributes so every registered icon scales
+   without a per-callsite size prop. StateDot (its svg carries data-state)
+   stays at its fixed figma size — it is a status mark, not text furniture. */
+.leading svg:not([data-state]) {
+  width: calc(14px + var(--dsh-content-font-delta, 0px));
+  height: calc(14px + var(--dsh-content-font-delta, 0px));
+}
+
 button.leading {
   cursor: pointer;
 }
@@ -63,7 +76,7 @@ button.leading {
 
 .title {
   flex: none;
-  font-size: 14px;
-  line-height: 24px;
+  font-size: var(--dsh-content-font-size, 14px);
+  line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-secondary);
 }

+ 47 - 0
packages/client/ui-primitives/tests/disclosure-row-styles.client.spec.ts

@@ -0,0 +1,47 @@
+/**
+ * DisclosureRow's font-size-axis adoption as CSS text. jsdom has no layout,
+ * so these read the declarations that make every flow row (tool calls, think,
+ * commands) follow the Settings font-size preference: title size on the axis
+ * variable, row/leading geometry on the shared px delta, and the leading
+ * glyph override that scales registered icons while exempting StateDot.
+ */
+import { readFileSync } from 'node:fs'
+import { fileURLToPath } from 'node:url'
+import { describe, expect, it } from 'vitest'
+
+const css = readFileSync(fileURLToPath(new URL('../src/DisclosureRow.module.css', import.meta.url)), 'utf8')
+const declarationText = css.replace(/\/\*[\s\S]*?\*\//g, ' ')
+
+function declarations(selector: string): string[] {
+  const rule = new RegExp(`(?:^|\\})\\s*${selector.replace(/[.[\]():*+^$\\]/g, '\\$&')}\\s*\\{([^{}]*)\\}`).exec(declarationText)
+  if (rule === null) throw new Error(`DisclosureRow.module.css has no \`${selector}\` rule`)
+  return (rule[1] ?? '').split(';').map(part => part.trim()).filter(Boolean)
+}
+
+describe('DisclosureRow.module.css font-size axis', () => {
+  it('sizes the title from the content font axis with the shared px delta', () => {
+    expect(declarations('.title')).toEqual(expect.arrayContaining([
+      'font-size: var(--dsh-content-font-size, 14px)',
+      'line-height: calc(24px + var(--dsh-content-font-delta, 0px))',
+    ]))
+  })
+
+  it('moves the row height and leading box by the same delta', () => {
+    expect(declarations('.row')).toEqual(expect.arrayContaining([
+      'height: calc(24px + var(--dsh-content-font-delta, 0px))',
+    ]))
+    expect(declarations('.leading')).toEqual(expect.arrayContaining([
+      'width: calc(16px + var(--dsh-content-font-delta, 0px))',
+      'height: calc(16px + var(--dsh-content-font-delta, 0px))',
+    ]))
+  })
+
+  it('scales leading glyphs via the svg edge but exempts StateDot', () => {
+    // StateDot marks itself with data-state; the :not filter keeps the status
+    // mark at its fixed figma size while text-furniture icons follow the text.
+    expect(declarations('.leading svg:not([data-state])')).toEqual(expect.arrayContaining([
+      'width: calc(14px + var(--dsh-content-font-delta, 0px))',
+      'height: calc(14px + var(--dsh-content-font-delta, 0px))',
+    ]))
+  })
+})

+ 11 - 8
packages/client/ui-tool/src/client/tool/components/ToolRow.module.css

@@ -1,5 +1,8 @@
-/* Tool summary row (figma 122:9479): 24px single line —
-   [16 leading] gap6 [title 14/24] gap8 [2x2 dot] gap8 [summary FILL truncate]. */
+/* Tool summary row (figma 122:9479): 24px single line at the default size —
+   [16 leading] gap6 [title 14/24] gap8 [2x2 dot] gap8 [summary FILL truncate].
+   The Settings font-size preference rides the body-published axis: the shared
+   DisclosureRow chrome scales the row height, title, and leading glyph; the
+   summary texts here follow the same size and px delta. */
 
 .root {
   display: flex;
@@ -79,8 +82,8 @@
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
-  font-size: 14px;
-  line-height: 24px;
+  font-size: var(--dsh-content-font-size, 14px);
+  line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-tertiary);
 }
 
@@ -93,8 +96,8 @@
   flex: none;
   margin-left: 4px;
   white-space: nowrap;
-  font-size: 14px;
-  line-height: 24px;
+  font-size: var(--dsh-content-font-size, 14px);
+  line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-tertiary);
 }
 
@@ -111,8 +114,8 @@
   background: none;
   font: inherit;
   text-align: left;
-  font-size: 14px;
-  line-height: 24px;
+  font-size: var(--dsh-content-font-size, 14px);
+  line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-secondary);
   text-decoration: underline;
   text-decoration-color: var(--dsw-alias-label-quaternary);

+ 19 - 7
packages/client/ui-tool/src/client/tool/toolviews/bash-sample.module.css

@@ -89,7 +89,7 @@
   overflow: hidden;
   display: flex;
   align-items: center;
-  height: 24px;
+  height: calc(24px + var(--dsh-content-font-delta, 0px));
   min-width: 0;
 }
 
@@ -119,8 +119,8 @@
 .leading {
   position: relative; /* .chevronHover overlay anchor */
   flex: none;
-  width: 16px;
-  height: 16px;
+  width: calc(16px + var(--dsh-content-font-delta, 0px));
+  height: calc(16px + var(--dsh-content-font-delta, 0px));
   display: inline-flex;
   align-items: center;
   justify-content: center;
@@ -128,6 +128,18 @@
   color: var(--dsw-alias-label-tertiary);
 }
 
+/* Same glyph-scaling rule as DisclosureRow's leading: the CSS edge overrides
+   the svg attributes so the 14px glyph follows the font-size delta; StateDot
+   (data-state) keeps its fixed figma size.
+   TODO: this bash header duplicates the DisclosureRow structure (and this is
+   the fourth copy of the glyph rule, with DisclosureRow, MessageItem
+   .compactionLeading, and MessageIconActions); migrating the bash row onto
+   the shared DisclosureRow would collapse them to one. */
+.leading svg:not([data-state]) {
+  width: calc(14px + var(--dsh-content-font-delta, 0px));
+  height: calc(14px + var(--dsh-content-font-delta, 0px));
+}
+
 .chevron {
   color: var(--dsw-alias-label-secondary);
 }
@@ -158,8 +170,8 @@
 
 .title {
   flex: none;
-  font-size: 14px;
-  line-height: 24px;
+  font-size: var(--dsh-content-font-size, 14px);
+  line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-secondary);
 }
 
@@ -178,8 +190,8 @@
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
-  font-size: 14px;
-  line-height: 24px;
+  font-size: var(--dsh-content-font-size, 14px);
+  line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-tertiary);
 }
 

+ 11 - 0
packages/client/ui-tool/tests/tool-row-styles.client.spec.ts

@@ -42,4 +42,15 @@ describe('ToolRow.module.css summary line', () => {
     ]))
     expect(declarations('.summarySuffix')).not.toEqual(expect.arrayContaining(['text-overflow: ellipsis']))
   })
+
+  it('sizes the summary texts from the content font axis', () => {
+    // The Settings font-size preference must reach tool-call rows, not only
+    // the narration body: summary, suffix, and file link ride the axis.
+    for (const selector of ['.summary', '.summarySuffix', '.fileLink']) {
+      expect(declarations(selector)).toEqual(expect.arrayContaining([
+        'font-size: var(--dsh-content-font-size, 14px)',
+        'line-height: calc(24px + var(--dsh-content-font-delta, 0px))',
+      ]))
+    }
+  })
 })

+ 26 - 24
packages/client/ui-workflow-run/src/client/WorkflowRunPanel.module.css

@@ -24,8 +24,8 @@
 .runLeading {
   display: inline-flex;
   flex: none;
-  width: 16px;
-  height: 16px;
+  width: calc(16px + var(--dsh-content-font-delta, 0px));
+  height: calc(16px + var(--dsh-content-font-delta, 0px));
   align-items: center;
   justify-content: center;
   margin-right: 0;
@@ -37,9 +37,9 @@
   flex: none;
   max-width: 42%;
   color: var(--dsw-alias-label-secondary);
-  font-size: 14px;
+  font-size: var(--dsh-content-font-size, 14px);
   font-weight: 510;
-  line-height: 24px;
+  line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;
 }
@@ -49,8 +49,8 @@
   flex: 1;
   min-width: 0;
   color: var(--dsw-alias-label-tertiary);
-  font-size: 12px;
-  line-height: 18px;
+  font-size: calc(12px + var(--dsh-content-font-delta, 0px));
+  line-height: calc(18px + var(--dsh-content-font-delta, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;
 }
@@ -88,8 +88,8 @@
 .phaseLeading {
   display: inline-flex;
   flex: none;
-  width: 16px;
-  height: 16px;
+  width: calc(16px + var(--dsh-content-font-delta, 0px));
+  height: calc(16px + var(--dsh-content-font-delta, 0px));
   align-items: center;
   justify-content: center;
   margin-right: 0;
@@ -102,8 +102,8 @@
   min-width: 0;
   max-width: 42%;
   color: var(--dsw-alias-label-secondary);
-  font-size: 14px;
-  line-height: 24px;
+  font-size: var(--dsh-content-font-size, 14px);
+  line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;
 }
@@ -113,8 +113,8 @@
   flex: 1;
   min-width: 0;
   color: var(--dsw-alias-label-tertiary);
-  font-size: 13px;
-  line-height: 20px;
+  font-size: calc(13px + var(--dsh-content-font-delta, 0px));
+  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;
 }
@@ -122,10 +122,12 @@
 .phaseStatus {
   overflow: hidden;
   flex: none;
-  width: 132px;
+  /* Column width scales with the text (~132/13 ≈ 10px per font-size px) so
+     larger sizes keep the same status text visible instead of truncating. */
+  width: calc(132px + var(--dsh-content-font-delta, 0px) * 10);
   color: var(--dsw-alias-label-secondary);
-  font-size: 13px;
-  line-height: 20px;
+  font-size: calc(13px + var(--dsh-content-font-delta, 0px));
+  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
   text-align: right;
   text-overflow: ellipsis;
   white-space: nowrap;
@@ -166,7 +168,7 @@
   gap: 12px;
   width: 100%;
   min-width: 0;
-  min-height: 24px;
+  min-height: calc(24px + var(--dsh-content-font-delta, 0px));
   padding: 0;
   border: 0;
   border-radius: 4px;
@@ -190,7 +192,7 @@
   display: inline-flex;
   flex: none;
   width: 16px;
-  height: 24px;
+  height: calc(24px + var(--dsh-content-font-delta, 0px));
   align-items: center;
   justify-content: center;
   overflow: hidden;
@@ -210,7 +212,7 @@
   overflow: hidden;
   flex: 1;
   min-width: 0;
-  height: 24px;
+  height: calc(24px + var(--dsh-content-font-delta, 0px));
   align-items: center;
   padding: 0 2px;
   border-radius: 4px;
@@ -221,8 +223,8 @@
   flex: 1;
   min-width: 0;
   color: var(--dsw-alias-label-secondary);
-  font-size: 14px;
-  line-height: 24px;
+  font-size: var(--dsh-content-font-size, 14px);
+  line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;
 }
@@ -232,8 +234,8 @@
   overflow: hidden;
   width: 64px;
   color: var(--dsw-alias-label-secondary);
-  font-size: 13px;
-  line-height: 20px;
+  font-size: calc(13px + var(--dsh-content-font-delta, 0px));
+  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
   text-align: right;
   text-overflow: ellipsis;
   white-space: nowrap;
@@ -241,8 +243,8 @@
 
 .empty {
   color: var(--dsw-alias-label-tertiary);
-  font-size: 13px;
-  line-height: 20px;
+  font-size: calc(13px + var(--dsh-content-font-delta, 0px));
+  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
   padding: 0;
 }
 

+ 48 - 0
packages/client/ui-workflow-run/tests/panel-styles.client.spec.ts

@@ -0,0 +1,48 @@
+/**
+ * WorkflowRunPanel's font-size-axis adoption as CSS text. jsdom has no
+ * layout, so these read the declarations that make the run/phase headers and
+ * the expanded member rows follow the Settings font-size preference through
+ * --dsh-content-font-size / --dsh-content-font-delta.
+ */
+import { readFileSync } from 'node:fs'
+import { fileURLToPath } from 'node:url'
+import { describe, expect, it } from 'vitest'
+
+const css = readFileSync(
+  fileURLToPath(new URL('../src/client/WorkflowRunPanel.module.css', import.meta.url)),
+  'utf8',
+)
+const declarationText = css.replace(/\/\*[\s\S]*?\*\//g, ' ')
+
+function declarations(selector: string): string[] {
+  const rule = new RegExp(`(?:^|\\})\\s*${selector.replace(/[.[\]():*+^$\\,\s]/g, '\\$&')}\\s*\\{([^{}]*)\\}`).exec(declarationText)
+  if (rule === null) throw new Error(`WorkflowRunPanel.module.css has no \`${selector}\` rule`)
+  return (rule[1] ?? '').split(';').map(part => part.trim()).filter(Boolean)
+}
+
+describe('WorkflowRunPanel.module.css font-size axis', () => {
+  it('member labels ride the axis at the body size with matching row geometry', () => {
+    expect(declarations('.memberLabel')).toEqual(expect.arrayContaining([
+      'font-size: var(--dsh-content-font-size, 14px)',
+      'line-height: calc(24px + var(--dsh-content-font-delta, 0px))',
+    ]))
+    expect(declarations('.memberLabelWrap')).toEqual(expect.arrayContaining([
+      'height: calc(24px + var(--dsh-content-font-delta, 0px))',
+    ]))
+  })
+
+  it('member status and the empty placeholder keep their 1px step under the body', () => {
+    for (const selector of ['.memberStatus', '.empty']) {
+      expect(declarations(selector)).toEqual(expect.arrayContaining([
+        'font-size: calc(13px + var(--dsh-content-font-delta, 0px))',
+        'line-height: calc(20px + var(--dsh-content-font-delta, 0px))',
+      ]))
+    }
+  })
+
+  it('the phase status column widens with the text so larger sizes do not truncate', () => {
+    expect(declarations('.phaseStatus')).toEqual(expect.arrayContaining([
+      'width: calc(132px + var(--dsh-content-font-delta, 0px) * 10)',
+    ]))
+  })
+})