diff --git a/docs/task-ui-simplification/change.md b/docs/task-ui-simplification/change.md new file mode 100644 index 0000000..f75d161 --- /dev/null +++ b/docs/task-ui-simplification/change.md @@ -0,0 +1,24 @@ +# Intent + +精简 Task 列表页与详情页的视觉层级和信息密度,让核心状态、当前执行、最新结果与主要操作更容易扫描,同时保留现有数据、操作、状态限制和 Renderer/Main 边界。 + +风险判定:标准变更。它调整多个 Renderer 组件的用户可见结构、响应式样式与测试,但不改变 Task 状态机、持久化、IPC、Main 服务或权限边界。 + +# Spec + +- 列表页概览固定为总数、进行中、待处理、已完成四项;后三项继续切换现有列表视图,总数只读。近七天指标、耗时、Token 与完整状态分布不再常驻展示,底层 Dashboard DTO 保持不变。 +- 视图切换、搜索和项目筛选组成一条紧凑、可换行的工具栏;通知、新建、更多菜单、分页、空态、加载与重试保持可用。 +- 每条任务只常驻展示标题、项目、可读状态和更新时间,不展示 workspace、耗时或 Token;长文本截断但保留 title,交互行保留明确的 focus-visible 状态。 +- 详情页顶部集中返回、标题、状态和当前可用主要操作,并在足够宽度下保持 sticky。 +- 当前执行和最新结果保留强调容器;项目、workspace、耗时、Token 合并为无独立卡片的紧凑元数据行。 +- 需求、验证详情和历史 Run 使用分隔式折叠区域。需求仅在 backlog 默认展开;需要立即处理的验证状态可以默认展开,其余验证详情与历史默认收起。 +- 最新报告、测试汇总、交付链接、错误、注意状态及全部现有操作和状态限制保持清晰可用。 +- 继续使用现有主题 token、容器查询、语义化状态文本、可访问名称和 focus-visible 样式;不新增依赖或文案能力。 + +# Plan + +1. 先更新 TaskPage、TaskDetail 与布局测试,覆盖四项概览、精简列表字段、紧凑元数据、折叠验证/需求/历史和窄容器结构,并确认旧实现失败。 +2. 精简 `TaskDashboard` 和任务行展示,移除仅为已隐藏指标服务的 Renderer 计时与格式化逻辑,同时保持筛选、错误和刷新行为。 +3. 重组 `TaskDetail` 的验证详情与资源元数据,保留所有操作回调和状态门控。 +4. 调整 Task 专用 CSS 为紧凑工具栏、列表行、扁平详情分区和宽/窄容器响应式布局。 +5. 运行 focused Renderer tests、lint、typecheck、Renderer/build 验证及 `pnpm check`;记录无法完成的原生视觉检查。 diff --git a/src/features/tasks/TaskDashboard.tsx b/src/features/tasks/TaskDashboard.tsx index 741d946..87eed5c 100644 --- a/src/features/tasks/TaskDashboard.tsx +++ b/src/features/tasks/TaskDashboard.tsx @@ -1,43 +1,25 @@ -import { formatNumber, type AppLanguage } from "../../i18n"; -import { formatTaskDuration, formatTaskTokens } from "./taskMetrics"; -import { taskStatusKeys } from "./taskPresentation"; -import type { TaskDashboardValue, TaskStatus } from "./tasksApi"; +import { formatNumber } from "../../i18n"; +import type { TaskDashboardValue } from "./tasksApi"; import type { TaskListView } from "./tasksStore"; -const STATUS_ORDER: TaskStatus[] = [ - "backlog", - "queued", - "running", - "needs_review", - "completed", - "failed", - "cancelled", -]; - interface TaskDashboardProps { value: TaskDashboardValue | null; loading: boolean; error: string | null; - language: AppLanguage; - now: number; t(key: string, values?: Record): string; onRetry(): void; onSelectView( view: Extract, ): void; - onSelectStatus(status: TaskStatus): void; } export function TaskDashboard({ value, loading, error, - language, - now, t, onRetry, onSelectView, - onSelectStatus, }: TaskDashboardProps) { if (!value && loading) return ( @@ -66,16 +48,11 @@ export function TaskDashboard({ ); if (!value) return null; - const liveDuration = - value.durationMs + - value.advancingRunCount * Math.max(0, now - value.measuredAt); - const maximumStatusCount = Math.max(1, ...Object.values(value.byStatus)); const metrics: Array<{ key: string; label: string; value: string; view?: "attention" | "active" | "completed"; - description?: string; }> = [ { key: "total", @@ -93,7 +70,6 @@ export function TaskDashboard({ label: t("tasks.dashboard.attention"), value: formatNumber(value.attention), view: "attention", - description: t("tasks.dashboard.overlapHint"), }, { key: "completed", @@ -101,26 +77,6 @@ export function TaskDashboard({ value: formatNumber(value.completed), view: "completed", }, - { - key: "createdRecent", - label: t("tasks.dashboard.createdRecent"), - value: formatNumber(value.createdLast7Days), - }, - { - key: "completedRecent", - label: t("tasks.dashboard.completedRecent"), - value: formatNumber(value.completedLast7Days), - }, - { - key: "duration", - label: t("tasks.dashboard.duration"), - value: formatTaskDuration(liveDuration, language), - }, - { - key: "tokens", - label: t("tasks.dashboard.tokens"), - value: formatTaskTokens(value.tokenCount), - }, ]; return ( @@ -130,10 +86,7 @@ export function TaskDashboard({ aria-busy={loading} >
-
-

{t("tasks.dashboard.heading")}

-

{t("tasks.dashboard.globalHint")}

-
+

{t("tasks.dashboard.heading")}

{loading ? ( {t("tasks.dashboard.refreshing")} ) : null} @@ -158,7 +111,6 @@ export function TaskDashboard({ {metric.label} {metric.value} - {metric.description ? {metric.description} : null} ); return metric.view ? ( @@ -181,41 +133,6 @@ export function TaskDashboard({ ); })}
-
-

{t("tasks.dashboard.distribution")}

- {value.total === 0 ?

{t("tasks.dashboard.empty")}

: null} -
    - {STATUS_ORDER.map((status) => { - const count = value.byStatus[status]; - const label = t(taskStatusKeys[status]); - return ( -
  • - -
  • - ); - })} -
-
); } diff --git a/src/features/tasks/TaskDetail.test.tsx b/src/features/tasks/TaskDetail.test.tsx index de1ad9c..db0eea1 100644 --- a/src/features/tasks/TaskDetail.test.tsx +++ b/src/features/tasks/TaskDetail.test.tsx @@ -164,6 +164,13 @@ describe("TaskDetail", () => { expect(screen.getByText("1h 1m 1s")).toBeInTheDocument(); expect(screen.getByText("Tokens")).toBeInTheDocument(); expect(screen.getByText("98.8K")).toBeInTheDocument(); + const metadata = screen.getByRole("region", { name: "Overview" }); + expect(metadata).toHaveClass("task-resource-summary"); + expect(metadata.querySelectorAll(".task-resource-item")).toHaveLength(4); + const requirements = screen + .getAllByText("Requirements")[0] + ?.closest("details"); + expect(requirements).toHaveAttribute("open"); }); it("treats awaiting_report as running and never as completed", async () => { @@ -411,6 +418,10 @@ it("shows verification evidence and updates the delivery gate switch", async () }); render(); + const verificationDetails = screen + .getByText("Independent verification") + .closest("details"); + expect(verificationDetails).toHaveAttribute("open"); expect( screen.getByText("The invalid-input path is broken"), ).toBeInTheDocument(); @@ -428,6 +439,38 @@ it("shows verification evidence and updates the delivery gate switch", async () expect(onChange).toHaveBeenCalled(); }); +it("keeps successful verification details collapsed by default", () => { + const verified = taskDetail({ + ...reviewTask, + verificationEnabled: true, + verification: { + id: "verification-1", + codingRunId: "run-1", + worktreeId: "worktree-1", + commitSha: "abcdef1234567890", + verdict: "pass", + summary: "All criteria passed", + evidence: { acceptanceCriteria: [], commands: [], issues: [] }, + sessionId: null, + createdAt: 2, + finishedAt: 3, + }, + }); + render( + , + ); + + const verificationDetails = screen + .getByText("Independent verification") + .closest("details"); + expect(verificationDetails).not.toHaveAttribute("open"); + expect(verificationDetails).toHaveClass("task-verification-details"); +}); + it("shows session navigation errors inline", async () => { render( -
-

{t("tasks.verification.title")}

- - {t( - `tasks.verification.${verification.verdict ?? "running"}` as TranslationKey, - )} + + {t("tasks.verification.title")} + + {t(`tasks.verification.${verdict}` as TranslationKey)} + +
+ {verification.commitSha.slice(0, 12)} + {verification.summary ?

{verification.summary}

: null} + {verification.evidence?.acceptanceCriteria.length ? ( +
+ {t("tasks.verification.criteria")} +
    + {verification.evidence.acceptanceCriteria.map((check, index) => ( +
  • + + {t(`tasks.verification.${check.status}` as TranslationKey)} + {" "} + {check.criterion} + {check.evidence.length ? ( +
      + {check.evidence.map((item, evidenceIndex) => ( +
    • {item}
    • + ))} +
    + ) : null} +
  • + ))} +
+
+ ) : null} + {verification.evidence?.commands.length ? ( +
+ {t("tasks.verification.commands")} +
    + {verification.evidence.commands.map((command, index) => ( +
  • + {command.command}: {command.summary} +
  • + ))} +
+
+ ) : null} + {verification.evidence?.issues.length ? ( +
+ {t("tasks.verification.issues")} +
    + {verification.evidence.issues.map((issue, index) => ( +
  • + {issue.severity}: {issue.summary} + {issue.evidence ? ` — ${issue.evidence}` : ""} +
  • + ))} +
+
+ ) : null}
- {verification.commitSha.slice(0, 12)} - {verification.summary ?

{verification.summary}

: null} - {verification.evidence?.acceptanceCriteria.length ? ( -
- {t("tasks.verification.criteria")} -
    - {verification.evidence.acceptanceCriteria.map((check, index) => ( -
  • - - {t(`tasks.verification.${check.status}` as TranslationKey)} - {" "} - {check.criterion} - {check.evidence.length ? ( -
      - {check.evidence.map((item, evidenceIndex) => ( -
    • {item}
    • - ))} -
    - ) : null} -
  • - ))} -
-
- ) : null} - {verification.evidence?.commands.length ? ( -
- {t("tasks.verification.commands")} -
    - {verification.evidence.commands.map((command, index) => ( -
  • - {command.command}: {command.summary} -
  • - ))} -
-
- ) : null} - {verification.evidence?.issues.length ? ( -
- {t("tasks.verification.issues")} -
    - {verification.evidence.issues.map((issue, index) => ( -
  • - {issue.severity}: {issue.summary} - {issue.evidence ? ` — ${issue.evidence}` : ""} -
  • - ))} -
-
- ) : null} - + ); } @@ -278,14 +278,14 @@ export function TaskDetail({ return (
- {onBack ? ( - - ) : null}
-
+ {onBack ? ( + + ) : null} +

{task.title}

{t("tasks.statusLabel", { status: t(taskStatusKeys[status]) })} @@ -447,6 +447,47 @@ export function TaskDetail({ ) : null}
+ {latestRun?.report ? ( +
+ + {task.status === "needs_review" ? ( +
+ +