Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
bfb892c
fix(desktop): restore zoom on timeline text via rem tokens
Jun 15, 2026
485d10e
feat(desktop): make chat text the 16px base type scale
Jun 15, 2026
c439255
fix(desktop): convert px text literals to rem so zoom scales app-wide
Jun 15, 2026
8ca67e0
fix(desktop): convert DiffViewer.css font-size px to rem for zoom
Jun 15, 2026
e42e70e
fix(composer): scale mention highlight with zoom, align to timeline chip
Jun 15, 2026
e5b10ce
refactor(desktop): consolidate sub-xs text onto rem tokens, enforce a…
Jun 15, 2026
25a8187
fix(desktop): scale composer chips with editor text
tellaho Jun 15, 2026
bfabdfe
refactor(desktop): remove markdown display variants
tellaho Jun 15, 2026
8d11f7f
fix(desktop): unify mention chip and code styling
tellaho Jun 15, 2026
7366ebd
fix(desktop): align message header typography
tellaho Jun 15, 2026
30c3b14
fix(desktop): align message text stack with avatar height
tellaho Jun 15, 2026
49b6255
Merge origin/main into tho/chat-base-type-scale
Jun 15, 2026
0714b65
fix(desktop): restore markdown and system row cleanup after merge
tellaho Jun 16, 2026
cf71af1
fix(desktop): inherit mention chip text rhythm
tellaho Jun 16, 2026
a185823
fix(desktop): downshift chat and forum message bodies to text-sm
tellaho Jun 16, 2026
2a1e896
fix(desktop): use full foreground on system events and body text
tellaho Jun 16, 2026
97d80e1
fix(desktop): match composer text scale to timeline text-sm
tellaho Jun 16, 2026
2c2fc22
fix(desktop): unify timeline and composer inline chip styling
tellaho Jun 17, 2026
e07b1ef
fix(desktop): tokenize inline chips and restore code block scale
tellaho Jun 17, 2026
1b8632c
merge(desktop): combine origin/main with chat type-scale + avatar work
Jun 17, 2026
bba9ce3
fix(desktop): tighten markdown block spacing
tellaho Jun 17, 2026
b4fc1bd
Restore agent-mention styling in SystemMessageRow after merge
Jun 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,40 @@ just desktop-dev # web-only dev server (faster iteration)
just dev # full Tauri app with native shell
```

### Text sizing & zoom (use rem, never px)

The desktop app implements Cmd +/- zoom by scaling the root `<html>`
font-size (`desktop/src/app/useWebviewZoomShortcuts.ts`) and pinning the native
webview zoom. **Only rem-based text scales with zoom — hardcoded px text sizes
are frozen.**

So for any readable text, reach for rem-based Tailwind tokens, never arbitrary
px:

- ✅ Stock rem tokens (`text-base`, `text-sm`, `text-xs`, …). **Chat body/author
text === `text-base` (16px) — chat is the app's base type size**, and the
surrounding timeline elements (timestamps, system rows, code, reactions) are
deliberate steps on that same stock ramp.
- ✅ The `text-2xs` (0.6875rem / 11px) and `text-3xs` (0.5rem / 8px) meta-text
tokens (in `desktop/tailwind.config.js` under `theme.extend.fontSize`) for the
sub-`text-xs` ramp — timestamps, count badges, tracking labels, tiny glyphs.
These replaced the dozens of arbitrary `text-[…rem]` literals that had drifted
apart pixel-by-pixel; keep meta text on these two tokens, not new arbitrary
values.
- ❌ `text-[15px]`, `text-[13px]`, CSS `font-size: 15px` — px froze against zoom
and caused the message-timeline regression (PR #891).
- ❌ Arbitrary rem literals too: `text-[0.6875rem]`, `text-[0.9rem]`, etc. They
zoom fine but re-fragment the scale we consolidated. Use a named token.

Prefer stock tokens — they're rem and zoom-safe. Only if a design genuinely
needs a size the stock/`2xs`/`3xs` scale can't express should you **add a
rem-based token** (in `desktop/tailwind.config.js` under `theme.extend.fontSize`)
rather than an arbitrary literal. A CI guard (`pnpm check:px-text`, in
`desktop/scripts/check-px-text.mjs`) scans all of `desktop/src` and fails on any
new arbitrary text-size literal — px **or** rem/em. Genuinely decorative glyphs
(e.g. the `text-[6rem]` avatar emoji) are allowlisted by `path:line` in that
script.

### Workspace Switching

The desktop app supports multiple workspaces (each backed by a different relay).
Expand Down
84 changes: 84 additions & 0 deletions RESEARCH/CHAT_BASE_TYPE_SCALE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
title: "Chat-as-base type scale: re-anchoring the timeline type ramp"
tags: [desktop, typography, design-tokens, tailwind, zoom]
status: active
created: 2026-06-15
---

# Chat-as-base type scale

## Why this exists
tho's reframe (2026-06-15): **chat text *is* the base size** — the fundamental
building block of the app. It should not be a special between-the-cracks token
(`text-chat` = 15px, wedged between Tailwind's `text-sm` 14px and `text-base`
16px). Direction: **bump chat to 16px and make it the base**, then re-anchor the
*other* timeline/thread elements that currently sit relative to the old ~15px
chat assumption. This is a **deliberate, wider pass** — a design decision, not a
surgical bugfix.

Builds on the zoom-regression work (PR #1051, branch `tho/rem-font-zoom-fix`)
which introduced the `text-chat`/`text-code` rem tokens. That fix preserved
Kenny's 15px intent; this pass *changes* that intent to 16px-as-base.

## Non-negotiables (carry forward from the zoom fix)
- **Everything stays rem.** Cmd +/- zoom scales the root `<html>` font-size
(rem-only by design, from #573). px would freeze against zoom — that was the
original bug. The `check:px-text` guard already enforces this; keep it happy.
- No `html { font-size }` override exists, so root is browser-default 16px and
rem math is standard (1rem = 16px). The `--font-size: 14px` var in globals.css
is scoped to the emoji-picker component, NOT the app root.

## Current type landscape (timeline/thread render path)
`text-chat` (15px) is used in:
- `MessageRow.tsx` — markdown body wrapper, author name (`<span>` and `<h3>`)
- `markdown.tsx` — chat body wrapper
- `mentionChip.ts` — mention chip text
- (`text-code` 13px — inline/block code in `markdown.tsx`)
- `globals.css` `.mention-highlight` — 0.9375rem

Satellite elements currently on the **stock** scale (these are the "other
elements that sit relative to chat" tho means — review each for whether it still
reads right once chat = 16px base):
- `MessageTimestamp.tsx` — text-xs
- `SystemMessageRow.tsx` — text-xs / text-sm
- `MessageThreadSummaryRow.tsx` — text-xs
- `MessageThreadPanel.tsx` — text-sm / text-xs
- `MessageReactions.tsx` — text-xs / text-sm
- `MessageActionBar.tsx` — text-xs
- `TypingIndicatorRow.tsx` — text-xs / text-sm
- `MessageTimeline.tsx` — text-base / text-sm / text-xl (empty states / headers)
- `MessageRow.tsx` (secondary bits) — text-sm / text-xs

## The design question for the build
If chat === base (16px), the type ramp around it should be *intentional*, not
incidental. The relationships that matter:
- chat body / author = base (16px)
- timestamps, metadata, system rows, reactions = one or two steps down,
consistently (don't leave them as ad-hoc text-xs/text-sm if the ratio now
looks off against a 16px anchor)
- code = its own deliberate step down from chat

Open for Bart to decide the cleanest expression: lean on stock Tailwind tokens
where they land right now that base is the anchor, retire/rename `text-chat` if
it's redundant with `text-base`, and only define custom rem tokens where the
stock scale genuinely can't express the intended step.

## Out of scope
- App-wide px sweep (the ~130 px classes elsewhere). Stay in the
timeline/thread render path + the type tokens that serve it.
- Don't widen the `check:px-text` guard roots in this pass (Marge's noted
follow-up) unless a new render-path file demands it.

## Decision (built)
Chat === `text-base` (16px, stock). Both custom tokens **retired** — `text-chat`
and `text-code` were artifacts of the 15px-wedge era; with chat at the stock
base they're redundant. Code (inline + block) re-anchored to stock `text-sm`
(14px) — a deliberate, documented one-notch step down from the 16px base.

Satellites left on their stock tokens **deliberately**: against a 16px base the
ratios actually tighten into the standard ramp (e.g. timestamps `text-xs` 12px =
clean base→sm→xs drop, was the awkward 12/15 before). Bumping them would inflate
the UI for no design gain — the right call is to keep them and document why.

Net: zero custom font tokens. `.mention-highlight` (composer) follows chat to
`1rem` (16px). Everything rem, guard stays green.
45 changes: 45 additions & 0 deletions RESEARCH/TIMELINE_ZOOM_REM_REGRESSION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Timeline Zoom Regression — Findings (for Bart)

## The bug
Cmd +/- zoom no longer scales message-timeline & thread text.

## Why (mechanism)
`desktop/src/app/useWebviewZoomShortcuts.ts` scales the ROOT `<html>` font-size
(rem-based scaling) and pins native `webview.setZoom(DEFAULT)`. So only **rem**
sizes scale; hardcoded **px** sizes are frozen. This is the intended approach
("only text should scale" — keeps webview coordinate system stable). Do NOT
revert to native webview zoom.

## Two-layer history
1. **#573** (9e76a08a, May 14) — switched zoom native→root-font-size/rem-only.
2. **#891** (45f3dfe5, Jun 8, "Tune chat text sizing", klopez4212) — the recent
timeline regression. Converted timeline rem→px.

## Kenny's intent in #891 (PRESERVE THIS)
He bumped chat text up from `text-sm` (0.875rem=14px) to **15px** because sm felt
too small. Conversions made:
- MessageRow author name `<span>` & `<h3>`: `text-sm` → `text-[15px]`
- markdown body / mentionChip: `text-sm` → `text-[15px]`
- `globals.css` `.mention-highlight`: added `font-size: 15px`, radius 0.375rem→4px
- also touched MessageTimeline, SystemMessageRow, MessageThreadSummaryRow

## The crux
Tailwind v4 here uses STOCK text tokens (no `@theme` override). Stock scale:
text-sm=14px, text-base=16px. **15px sits between them — no stock token exists.**
That's WHY Kenny reached for arbitrary px.

## tho's directive
- Use rem + Tailwind tokens wherever possible.
- Preserve Kenny's visual intent (the 15px chat sizing).
- Outcome MAY be to define/update a text-size token to yield 15px in rem — but
only if stock tokens genuinely can't deliver the look. Take a critical pass:
prefer a stock token if it looks right; introduce a custom token only if needed.

## Scope: timeline + thread render path only
MessageRow, markdown.tsx, mentionChip.ts, SystemMessageRow,
MessageThreadSummaryRow, MessageTimeline, and the relevant globals.css rules
(incl. `.mention-highlight` px font-size). Verify zoom works after.

## Codify (fix #2)
No custom lint exists. Add a guard (Biome rule or CI grep) flagging new
`text-[NNpx]` / px `fontSize` in the desktop app + a note in AGENTS.md/CLAUDE.md.
3 changes: 2 additions & 1 deletion desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
"build": "tsc && vite build",
"typecheck": "tsc --noEmit",
"check:file-sizes": "node ./scripts/check-file-sizes.mjs",
"check:px-text": "node ./scripts/check-px-text.mjs",
"lint": "biome lint .",
"check": "biome check . && pnpm check:file-sizes",
"check": "biome check . && pnpm check:file-sizes && pnpm check:px-text",
"format": "biome format --write .",
"test": "node --import ./test-loader.mjs --experimental-strip-types --test 'src/**/*.test.mjs'",
"preview": "vite preview",
Expand Down
36 changes: 36 additions & 0 deletions desktop/scripts/check-px-text.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { runPxTextCheck } from "../../scripts/check-px-text-core.mjs";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, "..");

// Enforces the rem-token text scale app-wide. The rem→px zoom regression
// (PR #891) landed in the message-timeline render path, but arbitrary text
// literals (`text-[…px]`, `text-[…rem]`) had drifted across the whole desktop
// app — so the guard now scans all of `src`. Readable text MUST use a rem-based
// token (the stock `text-base`/`text-sm`/`text-xs` scale, or the `text-2xs` /
// `text-3xs` meta-text tokens) so Cmd +/- zoom scales it and the size stays on
// one consolidated scale. Genuine decorative glyphs are allowlisted below.
const rules = [
{
root: "src",
extensions: new Set([".ts", ".tsx", ".css"]),
},
];

// Decorative / chrome exceptions: `relativePath:lineNumber`. The avatar emoji
// glyph is a fixed display size sized to its avatar box (not readable message
// text), so it stays as the lone documented `text-[6rem]` literal.
const overrides = new Set([
"src/features/settings/ui/ProfileSettingsCard.tsx:573",
"src/features/onboarding/ui/AvatarStep.tsx:89",
]);

await runPxTextCheck({
projectRoot,
rules,
overrides,
label: "Desktop",
scriptPath: "desktop/scripts/check-px-text.mjs",
});
4 changes: 2 additions & 2 deletions desktop/src/features/agent-memory/ui/MemorySection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,8 @@ function MemoryGraphView({
className="text-xs italic text-muted-foreground"
data-testid="agent-memory-no-core"
>
No <code className="font-mono text-[10px]">core</code> memory yet —
agent identity is unrooted.
No <code className="font-mono text-2xs">core</code> memory yet — agent
identity is unrooted.
</p>
) : null}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export function AddTeamToChannelDialog({
>
<ProfileAvatar
avatarUrl={persona.avatarUrl}
className="h-5 w-5 text-[9px]"
className="h-5 w-5 text-2xs"
label={persona.displayName}
/>
<span className="text-xs font-medium">
Expand Down
6 changes: 3 additions & 3 deletions desktop/src/features/agents/ui/AgentSessionToolItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ function ToolTimestamp({
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="shrink-0 cursor-default text-[11px] text-muted-foreground/60">
<span className="shrink-0 cursor-default text-2xs text-muted-foreground/60">
{time}
{duration ? ` · ${duration}` : null}
</span>
Expand Down Expand Up @@ -260,7 +260,7 @@ function BuzzToolInlineAction({
if (action.onClick) {
return (
<button
className="inline-flex max-w-[14rem] shrink min-w-0 items-center gap-1 rounded-full border border-primary/20 bg-primary/[0.05] px-1.5 py-0.5 text-[11px] font-normal leading-none text-primary/90 transition-colors hover:border-primary/35 hover:bg-primary/10 hover:text-primary"
className="inline-flex max-w-[14rem] shrink min-w-0 items-center gap-1 rounded-full border border-primary/20 bg-primary/[0.05] px-1.5 py-0.5 text-2xs font-normal leading-none text-primary/90 transition-colors hover:border-primary/35 hover:bg-primary/10 hover:text-primary"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
Expand All @@ -279,7 +279,7 @@ function BuzzToolInlineAction({

return (
<span
className="inline-flex max-w-[14rem] shrink min-w-0 items-center gap-1 rounded-full border border-border/60 bg-muted/40 px-1.5 py-0.5 text-[11px] font-normal leading-none text-muted-foreground"
className="inline-flex max-w-[14rem] shrink min-w-0 items-center gap-1 rounded-full border border-border/60 bg-muted/40 px-1.5 py-0.5 text-2xs font-normal leading-none text-muted-foreground"
title={action.title}
>
{action.avatar}
Expand Down
10 changes: 5 additions & 5 deletions desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ function MessageItem({
{!isAssistant ? (
<UserAvatar
avatarUrl={authorProfile?.avatarUrl ?? null}
className="mr-2 mt-1 h-5 w-5 shrink-0 text-[8px]"
className="mr-2 mt-1 h-5 w-5 shrink-0 text-3xs"
displayName={authorLabel}
size="xs"
/>
Expand All @@ -139,7 +139,7 @@ function MessageItem({
)}
>
{isAssistant ? (
<Markdown compact content={text || " "} />
<Markdown content={text || " "} />
) : (
<>
<p className="whitespace-pre-wrap break-words">{text}</p>
Expand All @@ -166,7 +166,7 @@ function ThoughtItem({
<ChevronDown className="h-4 w-4 shrink-0 transition-transform group-open:rotate-180" />
</summary>
<div className="py-2 pl-5 text-sm leading-6 text-muted-foreground">
<Markdown compact content={item.text.trim() || " "} />
<Markdown content={item.text.trim() || " "} />
</div>
</details>
);
Expand Down Expand Up @@ -198,7 +198,7 @@ function MetadataItem({
<span className="truncate">{section.title}</span>
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform group-open/section:rotate-180" />
</summary>
<pre className="mt-2 max-h-56 overflow-auto whitespace-pre-wrap break-words rounded-md bg-muted/50 px-3 py-2 font-mono text-[11px] leading-5 text-muted-foreground">
<pre className="mt-2 max-h-56 overflow-auto whitespace-pre-wrap break-words rounded-md bg-muted/50 px-3 py-2 font-mono text-2xs leading-5 text-muted-foreground">
{section.body.trim() || "No metadata."}
</pre>
</details>
Expand Down Expand Up @@ -248,7 +248,7 @@ function TranscriptTimestamp({ timestamp }: { timestamp: string }) {
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="shrink-0 cursor-default text-[11px] text-muted-foreground/60">
<span className="shrink-0 cursor-default text-2xs text-muted-foreground/60">
{formatted}
</span>
</TooltipTrigger>
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/features/agents/ui/ManagedAgentLogPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ export function ManagedAgentLogPanel({
<Skeleton className="mt-2 h-4 w-3/4" />
</div>
) : (
<div className="mt-4 overflow-hidden rounded-xl border border-border/70 bg-[#17171d] text-[12px] text-zinc-100">
<div className="flex items-center justify-between border-b border-white/10 px-4 py-2 text-[11px] uppercase tracking-[0.18em] text-zinc-400">
<div className="mt-4 overflow-hidden rounded-xl border border-border/70 bg-[#17171d] text-xs text-zinc-100">
<div className="flex items-center justify-between border-b border-white/10 px-4 py-2 text-2xs uppercase tracking-[0.18em] text-zinc-400">
<span>{selectedAgent.name}</span>
<span>{selectedAgent.status}</span>
</div>
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/features/agents/ui/ManagedAgentRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ function StatusBlock({
}) {
return (
<div className="space-y-1 lg:pt-0.5">
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground lg:hidden">
<p className="text-2xs font-semibold uppercase tracking-[0.16em] text-muted-foreground lg:hidden">
Status
</p>
<AgentStatusBadge
Expand Down Expand Up @@ -385,7 +385,7 @@ function RuntimeBlock({
}) {
return (
<div className="space-y-1 lg:pt-0.5">
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground lg:hidden">
<p className="text-2xs font-semibold uppercase tracking-[0.16em] text-muted-foreground lg:hidden">
Runtime
</p>
<p className="truncate font-mono text-xs text-foreground">
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/features/agents/ui/ModelPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export function ModelPicker({
</DropdownMenuContent>
</DropdownMenu>
{needsRestart ? (
<span className="text-[10px] text-warning">restart to apply</span>
<span className="text-2xs text-warning">restart to apply</span>
) : null}
</span>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function PersonaCatalogSelectionBadge({
return (
<span
className={cn(
"inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.14em]",
"inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-1 text-2xs font-semibold uppercase tracking-[0.14em]",
isActive
? "bg-primary text-primary-foreground shadow-xs"
: "border border-border/70 bg-background/85 text-muted-foreground",
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/features/agents/ui/RawEventRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function RawEventRail({ events }: { events: ObserverEvent[] }) {
<aside className="rounded-lg border border-border/70 bg-[#17171d] text-zinc-100">
<div className="flex items-center justify-between border-b border-white/10 px-3 py-2">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-zinc-400">
<p className="text-2xs font-semibold uppercase tracking-[0.16em] text-zinc-400">
Raw ACP
</p>
<p className="text-xs text-zinc-500">JSON-RPC payloads</p>
Expand Down Expand Up @@ -40,7 +40,7 @@ export function RawEventRail({ events }: { events: ObserverEvent[] }) {
<span className="font-mono text-zinc-500">#{event.seq}</span>{" "}
{describeRawEvent(event)}
</summary>
<pre className="mt-2 max-h-72 overflow-auto whitespace-pre-wrap break-words text-[11px] leading-5 text-zinc-300">
<pre className="mt-2 max-h-72 overflow-auto whitespace-pre-wrap break-words text-2xs leading-5 text-zinc-300">
{JSON.stringify(event.payload, null, 2)}
</pre>
</details>
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/features/agents/ui/RelayDirectorySection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export function RelayDirectorySection({
className="w-full border-collapse text-left text-sm"
data-testid="relay-directory-table"
>
<thead className="bg-muted/35 text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
<thead className="bg-muted/35 text-2xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
<tr>
<th className="px-4 py-3">Agent</th>
<th className="px-4 py-3">Status</th>
Expand All @@ -121,7 +121,7 @@ export function RelayDirectorySection({
</td>
<td className="px-4 py-3 align-top">
<PresenceBadge
className="px-2.5 py-0.5 text-[11px]"
className="px-2.5 py-0.5 text-2xs"
status={agent.status}
/>
</td>
Expand Down
Loading