feat(tasks-ui): readability pass, height-budget fix and leftover sweep

The native port is finished off with the readability work the earlier rounds
left open, plus the cleanup of everything the shadcn era or the older canvas
left behind.

- Geometry: node height is now composed from named parts (chrome, two title
  lines, meta row) and per-node extras, so a teammate card that carries a task
  line no longer gets clipped by the old fixed 68px budget; the layout spec
  follows the new model and GRAPH_ROW_STRIDE stays exported for it.
- Readability: row rhythm (28-32px), three ink levels for title/meta/owner,
  truncation with title hints, current-row and hover expressed as an accent bar
  plus a hover fill (no shadows), phase frames and edges back on the l1/l2
  hairlines, control cluster uniformly 28px, drawer durations on tabular
  figures, kill action revealed on hover/focus without shifting the row, no
  horizontal overflow at 360px.
- Task window: the markdown body is pulled onto the page scale (12px body,
  12/13px headings, tightened list indent and paragraph spacing) while the
  shared MarkdownText/markdownTextProps contract is untouched.
- Cleanup: dead stylesheet rules, unused TSX exports and helpers, stale
  comments that still described the retired Variant-D canvas, zero-reference
  i18n keys across the dictionaries, and the Tailwind/shadcn/Variant-D
  narrative in the design doc, AGENTS.md and the plugin guide (kept as one
  short "why it was rolled back" section).

Verified here: typecheck and lint clean; 133 files / 1413 tests; build with no
radix-ui or --tw- strings in lib/client.js (988.9 KiB); the 70-assertion
real-browser contract at 70/70 with the 20 reference screenshots regenerated;
mount e2e 7/7.
This commit is contained in:
Menghuan1918
2026-09-27 22:18:19 +08:00
parent ae4280c70e
commit b554665bde
35 changed files with 856 additions and 1388 deletions
+64 -4
View File
@@ -2,28 +2,44 @@
* Unit tests for the layered graph layout: parent centering, depth rows, the
* sibling-band WRAP that keeps a narrow sidebar readable (and the row
* reservation that stops a wrapped band from landing on a sibling's
* children), the live-line height, and the width-aware solver.
* children), the PER-SHAPE card height (the live rows and the shared-task row
* a card renders are reserved, so a card never renders outside its box), and
* the width-aware solver.
*/
import { describe, expect, it } from 'vitest'
import {
GRAPH_GAP_Y,
GRAPH_LIVE_H,
GRAPH_LIVE_TAIL_H,
GRAPH_NODE_H,
GRAPH_NODE_W,
GRAPH_PAD,
GRAPH_ROW_STRIDE,
GRAPH_TASK_H,
bandColsFor,
layoutTasksGraph,
layoutTasksGraphForWidth,
} from '../src/client/tasks-graph-layout.ts'
import type { TasksAgentNode, TasksNode } from '../src/client/tasks-model.ts'
import type { TasksAgentNode, TasksNode, TasksNodeTask } from '../src/client/tasks-model.ts'
/** An agent node. */
function agent(id: string, parentId?: string, live = false): TasksAgentNode {
/** One shared task as the card's task line shows it. */
function task(id: string): TasksNodeTask {
return { id, subject: id, status: 'in_progress', ready: true }
}
/** An agent node. `live` gives it a text-only tail; `extra` shapes the card
* further (a settled state, a tool call, owned shared tasks). */
function agent(
id: string,
parentId?: string,
live = false,
extra: Partial<TasksAgentNode> = {},
): TasksAgentNode {
return {
kind: 'agent', id, ...(parentId === undefined ? {} : { parentId }),
label: id, state: 'running', activity: 'running', current: false,
...(live ? { live: { text: 'x' } } : {}),
...extra,
}
}
@@ -66,6 +82,50 @@ describe('layoutTasksGraph', () => {
expect(layout.height).toBeGreaterThan(2 * (GRAPH_NODE_H + GRAPH_GAP_Y))
})
it('reserves the shared-task row a teammate card renders', () => {
// A non-running teammate with a task line renders two title lines + the
// meta line + the task row; the base card alone reserves 16px less, and
// the card (height auto) renders that much outside its own box — which is
// what the edges and the phase frames are placed from.
const nodes: TasksNode[] = [
agent('root'),
agent('mate', 'root', false, { state: 'idle', tasks: [task('t1')] }),
]
const layout = layoutTasksGraph(nodes)
expect(layout.boxes.get('mate')?.h).toBe(GRAPH_NODE_H + GRAPH_TASK_H)
// Without tasks the card keeps the base shape.
const plain = layoutTasksGraph([agent('root'), agent('mate', 'root', false, { state: 'idle' })])
expect(plain.boxes.get('mate')?.h).toBe(GRAPH_NODE_H)
})
it('reserves the live tail row only when the tail carries a tool AND text', () => {
const nodes: TasksNode[] = [
agent('root'),
agent('tool-only', 'root', false, { live: { tool: { name: 'read', args: 'x' } } }),
agent('both', 'root', false, { live: { tool: { name: 'read', args: 'x' }, text: 'y' } }),
]
const layout = layoutTasksGraph(nodes)
// One live row either way (the tool row, or the meta-styled thinking
// fallback); the flattened text row is the second one.
expect(layout.boxes.get('tool-only')?.h).toBe(GRAPH_NODE_H + GRAPH_LIVE_H)
expect(layout.boxes.get('both')?.h).toBe(GRAPH_NODE_H + GRAPH_LIVE_H + GRAPH_LIVE_TAIL_H)
})
it('keeps every card shape inside one row stride', () => {
// The stride is the row pitch: a card taller than it would overlap the
// next row (and its phase frame).
const tallest = GRAPH_NODE_H + GRAPH_LIVE_H + GRAPH_LIVE_TAIL_H + GRAPH_TASK_H
expect(GRAPH_ROW_STRIDE).toBeGreaterThanOrEqual(tallest)
const nodes: TasksNode[] = [
agent('root'),
agent('busiest', 'root', false, {
live: { tool: { name: 'read', args: 'x' }, text: 'y' }, tasks: [task('t1')],
}),
]
const layout = layoutTasksGraph(nodes)
expect(layout.boxes.get('busiest')?.h).toBe(tallest)
})
it('gives every node a box even with a defensive orphan', () => {
const nodes: TasksNode[] = [agent('root'), agent('ghost', 'missing')]
const layout = layoutTasksGraph(nodes)