The board card says one thing: pills learn to stay in their box

Cards on the Work board overflowed their frame and shouted six facts at
once. Two root causes, both fixed at the source:

- Pills truncated by counting characters in JS (truncate(title, 18)) while
  the CSS said white-space: nowrap with no max-width — CJK glyphs are twice
  as wide, so 18 Chinese characters punched straight through the card edge.
  The Pill component now owns truncation: max-width + an inner .pill-text
  with a real ellipsis. Every pill in the app inherits the fix; the JS
  truncation helper is gone.
- The card repeated what every other card already said: identical goal,
  workspace, and (in the Done column) status pills on all 280 cards carried
  zero information. A card now states what is particular to it: a title
  clamped to three lines, the project, the run state while the task is
  live, the assignee, and the actions. Goal and workspace stay in the task
  detail dialog where they belong.
- Done cards are no longer struck through — a six-line Chinese title with a
  line through it reads as vandalism, not completion. Done cards just go
  quiet.

Verified visually against the rebuilt app's board screenshot; full suite
plus e2e green.
This commit is contained in:
ZacharyZhang-NY committed 2026-07-09 20:59:41 -04:00
1 parent 34ea33cb8d
commit fb81505cb1
3 files changed
+18 -28

No files matched your search

+13 -26
View File
@@ -6,14 +6,12 @@ import {
TASK_PRIORITY, TASK_PRIORITY,
TASK_STATUS, TASK_STATUS,
type AgentRecord, type AgentRecord,
type GoalRecord,
type ProviderRecord, type ProviderRecord,
type ProjectRecord, type ProjectRecord,
type TaskPriority, type TaskPriority,
type TaskRecord, type TaskRecord,
type TaskRunRecord, type TaskRunRecord,
type TaskStatus, type TaskStatus,
type WorkspaceRecord,
} from "../../../shared/domain"; } from "../../../shared/domain";
import { NewTaskDialog } from "./NewTaskDialog"; import { NewTaskDialog } from "./NewTaskDialog";
import { GoalDialog } from "./work/GoalDialog"; import { GoalDialog } from "./work/GoalDialog";
@@ -111,8 +109,6 @@ export function WorkPage({ snapshot, actions }: PageProps) {
tasks={tasks} tasks={tasks}
agents={agents} agents={agents}
projects={projects} projects={projects}
goals={goals}
workspaces={workspaces}
providers={providers} providers={providers}
taskRuns={taskRuns} taskRuns={taskRuns}
onAdvance={advance} onAdvance={advance}
@@ -216,8 +212,6 @@ function BoardView({
tasks, tasks,
agents, agents,
projects, projects,
goals,
workspaces,
providers, providers,
taskRuns, taskRuns,
onAdvance, onAdvance,
@@ -228,8 +222,6 @@ function BoardView({
tasks: TaskRecord[]; tasks: TaskRecord[];
agents: AgentRecord[]; agents: AgentRecord[];
projects: ProjectRecord[]; projects: ProjectRecord[];
goals: GoalRecord[];
workspaces: WorkspaceRecord[];
providers: ProviderRecord[]; providers: ProviderRecord[];
taskRuns: TaskRunRecord[]; taskRuns: TaskRunRecord[];
onAdvance: (task: TaskRecord) => void; onAdvance: (task: TaskRecord) => void;
@@ -255,8 +247,6 @@ function BoardView({
task={task} task={task}
assignee={agents.find((agent) => agent.id === task.assigneeAgentId) ?? null} assignee={agents.find((agent) => agent.id === task.assigneeAgentId) ?? null}
project={projects.find((project) => project.id === task.projectId) ?? null} project={projects.find((project) => project.id === task.projectId) ?? null}
goal={goals.find((goal) => goal.id === task.goalId) ?? null}
workspace={workspaces.find((workspace) => workspace.id === task.workspaceId) ?? null}
provider={providerFor(agents.find((agent) => agent.id === task.assigneeAgentId) ?? null, providers)} provider={providerFor(agents.find((agent) => agent.id === task.assigneeAgentId) ?? null, providers)}
latestRun={latestTaskRun(task.id, taskRuns)} latestRun={latestTaskRun(task.id, taskRuns)}
onAdvance={() => onAdvance(task)} onAdvance={() => onAdvance(task)}
@@ -275,8 +265,6 @@ function TaskCard({
task, task,
assignee, assignee,
project, project,
goal,
workspace,
provider, provider,
latestRun, latestRun,
onAdvance, onAdvance,
@@ -286,8 +274,6 @@ function TaskCard({
task: TaskRecord; task: TaskRecord;
assignee: AgentRecord | null; assignee: AgentRecord | null;
project: ProjectRecord | null; project: ProjectRecord | null;
goal: GoalRecord | null;
workspace: WorkspaceRecord | null;
provider: ProviderRecord | null; provider: ProviderRecord | null;
latestRun: TaskRunRecord | null; latestRun: TaskRunRecord | null;
onAdvance: () => void; onAdvance: () => void;
@@ -317,23 +303,29 @@ function TaskCard({
<div <div
style={{ style={{
flex: 1, flex: 1,
minWidth: 0,
fontSize: 14, fontSize: 14,
fontWeight: 480, fontWeight: 480,
lineHeight: 1.35, lineHeight: 1.35,
letterSpacing: "-0.005em", letterSpacing: "-0.005em",
textDecoration: task.status === "done" ? "line-through" : "none",
color: task.status === "done" ? "var(--text-3)" : "var(--text-1)", color: task.status === "done" ? "var(--text-3)" : "var(--text-1)",
display: "-webkit-box",
WebkitLineClamp: 3,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}} }}
> >
{task.title} {task.title}
</div> </div>
</div> </div>
<div className="row" style={{ flexWrap: "wrap", gap: 6 }}> <div className="row" style={{ flexWrap: "wrap", gap: 6, minWidth: 0 }}>
{project ? <Pill>{truncate(project.name, 22)}</Pill> : <Pill>{capitalize(task.risk)}</Pill>} {project ? <Pill>{project.name}</Pill> : <Pill>{capitalize(task.risk)}</Pill>}
{goal && <Pill tone="violet">{truncate(goal.title, 18)}</Pill>} {task.status !== "done" && latestRun && (
{workspace && <Pill>{truncate(workspace.name, 18)}</Pill>} <span style={{ flexShrink: 0, display: "inline-flex" }}>
{latestRun && <Pill tone={runTone(latestRun.state)}>{prettyRunState(latestRun.state)}</Pill>} <Pill tone={runTone(latestRun.state)}>{prettyRunState(latestRun.state)}</Pill>
<div style={{ marginLeft: "auto", display: "flex", gap: 8, alignItems: "center" }} onClick={(event) => event.stopPropagation()}> </span>
)}
<div style={{ marginLeft: "auto", display: "flex", gap: 8, alignItems: "center", flexShrink: 0 }} onClick={(event) => event.stopPropagation()}>
{assignee && <Avatar name={assignee.name} size="sm" bot />} {assignee && <Avatar name={assignee.name} size="sm" bot />}
{assignee && <IconButton icon="send" label={canRun(provider) ? t("Run {title}", { title: task.title }) : t("Connect a provider to run")} onClick={onRun} disabled={!canRun(provider)} />} {assignee && <IconButton icon="send" label={canRun(provider) ? t("Run {title}", { title: task.title }) : t("Connect a provider to run")} onClick={onRun} disabled={!canRun(provider)} />}
{nextStatus(task.status) && <IconButton icon="check" label={t("Advance")} onClick={onAdvance} />} {nextStatus(task.status) && <IconButton icon="check" label={t("Advance")} onClick={onAdvance} />}
@@ -346,8 +338,3 @@ function TaskCard({
function capitalize(value: string): string { function capitalize(value: string): string {
return value.charAt(0).toUpperCase() + value.slice(1); return value.charAt(0).toUpperCase() + value.slice(1);
} }
function truncate(value: string, max: number): string {
if (value.length <= max) return value;
return `${value.slice(0, max - 1)}…`;
}
+1 -1
View File
@@ -108,7 +108,7 @@ export function Pill({
return ( return (
<span className={`pill ${tone ?? ""}`.trim()}> <span className={`pill ${tone ?? ""}`.trim()}>
{dot && <span className="dot" />} {dot && <span className="dot" />}
{children} <span className="pill-text">{children}</span>
</span> </span>
); );
} }
+4 -1
View File
@@ -100,9 +100,12 @@
font-size: var(--fs-caption); font-size: var(--fs-caption);
font-weight: 500; font-weight: 500;
white-space: nowrap; white-space: nowrap;
max-width: 100%;
min-width: 0;
transition: background var(--motion-base) var(--ease-out), color var(--motion-base) var(--ease-out), border-color var(--motion-base) var(--ease-out); transition: background var(--motion-base) var(--ease-out), color var(--motion-base) var(--ease-out), border-color var(--motion-base) var(--ease-out);
} }
.pill .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-3); transition: background var(--motion-base) var(--ease-out); } .pill .pill-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pill .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-3); flex-shrink: 0; transition: background var(--motion-base) var(--ease-out); }
.pill.amber { background: var(--amber-soft); color: var(--amber); border-color: transparent; } .pill.amber { background: var(--amber-soft); color: var(--amber); border-color: transparent; }
.pill.amber .dot { background: var(--amber); } .pill.amber .dot { background: var(--amber); }
.pill.green { background: var(--green-soft); color: var(--green); border-color: transparent; } .pill.green { background: var(--green-soft); color: var(--green); border-color: transparent; }