diff --git a/src/renderer/src/pages/WorkPage.tsx b/src/renderer/src/pages/WorkPage.tsx
index 89c4a55d..4bd7be7e 100644
--- a/src/renderer/src/pages/WorkPage.tsx
+++ b/src/renderer/src/pages/WorkPage.tsx
@@ -20,7 +20,9 @@ import { GoalDialog } from "./work/GoalDialog";
import { ProjectDialog } from "./work/ProjectDialog";
import { TaskDetailDialog } from "./work/TaskDetailDialog";
import { ProjectOverview } from "./work/ProjectOverview";
-import { canRun, latestTaskRun, nextStatus, prettyRunState, prettyStatus, providerFor, runTone } from "./work/task-helpers";
+import { ListView } from "./work/ListView";
+import { TimelineView } from "./work/TimelineView";
+import { canRun, latestTaskRun, nextStatus, prettyRunState, providerFor, runTone } from "./work/task-helpers";
import type { PageProps } from "./types";
import { activeOrgId } from "./types";
@@ -268,119 +270,6 @@ function BoardView({
);
}
-function ListView({
- tasks,
- agents,
- projects,
- providers,
- onRun,
- onOpen,
-}: {
- tasks: TaskRecord[];
- agents: AgentRecord[];
- projects: ProjectRecord[];
- providers: ProviderRecord[];
- onRun: (task: TaskRecord) => void;
- onOpen: (task: TaskRecord) => void;
-}) {
- return (
-
-
-
Title
Status
Priority
Assignee
-
- {tasks.map((task, index) => {
- const assignee = agents.find((agent) => agent.id === task.assigneeAgentId) ?? null;
- const provider = providerFor(assignee, providers);
- const project = projects.find((p) => p.id === task.projectId) ?? null;
- const last = index === tasks.length - 1;
- return (
-
onOpen(task)}
- onKeyDown={(event) => { if (event.key === "Enter") onOpen(task); }}
- style={{ gridTemplateColumns: "minmax(0, 3fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.2fr) 88px", borderBottom: last ? "none" : "1px solid var(--border-soft)" }}
- >
-
- {task.risk === "high" &&
}
-
{task.title}
- {project &&
{project.name}}
-
-
{prettyStatus(task.status)}
-
-
- {assignee ? (
-
- ) : (
-
—
- )}
-
-
event.stopPropagation()}>
- {assignee && onRun(task)} disabled={!canRun(provider)} />}
- onOpen(task)} />
-
-
- );
- })}
-
- );
-}
-
-function TimelineView({ tasks, goals, projects }: { tasks: TaskRecord[]; goals: GoalRecord[]; projects: ProjectRecord[] }) {
- const unlinked = tasks.filter((t) => !t.goalId);
- return (
-
-
By goal
- {goals.map((goal) => {
- const linked = tasks.filter((task) => task.goalId === goal.id);
- const done = linked.filter((task) => task.status === TASK_STATUS.DONE).length;
- const pct = linked.length === 0 ? 0 : Math.round((done / linked.length) * 100);
- const goalProjects = projects.filter((p) => p.goalId === goal.id);
- return (
-
-
- {goal.title}
- {done} / {linked.length}
-
-
- {goalProjects.length > 0 && (
-
- {goalProjects.map((p) => {
- const projectTasks = linked.filter((t) => t.projectId === p.id);
- const projectDone = projectTasks.filter((t) => t.status === TASK_STATUS.DONE).length;
- return (
-
- {truncate(p.name, 24)} {projectDone}/{projectTasks.length}
-
- );
- })}
-
- )}
-
- );
- })}
- {unlinked.length > 0 && (
-
-
- Unlinked tasks
- {unlinked.length}
-
-
- )}
-
- );
-}
-
function TaskCard({
task,
assignee,
diff --git a/src/renderer/src/pages/work/ListView.tsx b/src/renderer/src/pages/work/ListView.tsx
new file mode 100644
index 00000000..ffb1e4d0
--- /dev/null
+++ b/src/renderer/src/pages/work/ListView.tsx
@@ -0,0 +1,77 @@
+import { Avatar, IconButton, Pill } from "../../shared/ui";
+import type { AgentRecord, ProjectRecord, ProviderRecord, TaskRecord } from "../../../../shared/domain";
+import { canRun, prettyStatus, providerFor } from "./task-helpers";
+
+const GRID = "minmax(0, 3fr) 110px 96px minmax(0, 1.2fr) 88px";
+
+export function ListView({
+ tasks,
+ agents,
+ projects,
+ providers,
+ onRun,
+ onOpen,
+}: {
+ tasks: TaskRecord[];
+ agents: AgentRecord[];
+ projects: ProjectRecord[];
+ providers: ProviderRecord[];
+ onRun: (task: TaskRecord) => void;
+ onOpen: (task: TaskRecord) => void;
+}) {
+ return (
+
+
+
Title
Status
Priority
Assignee
+
+ {tasks.map((task, index) => {
+ const assignee = agents.find((agent) => agent.id === task.assigneeAgentId) ?? null;
+ const provider = providerFor(assignee, providers);
+ const project = projects.find((p) => p.id === task.projectId) ?? null;
+ const last = index === tasks.length - 1;
+ return (
+
onOpen(task)}
+ onKeyDown={(event) => { if (event.key === "Enter") onOpen(task); }}
+ style={{ gridTemplateColumns: GRID, borderBottom: last ? "none" : "1px solid var(--border-soft)" }}
+ >
+
+ {task.risk === "high" &&
}
+
+ {task.title}
+
+ {project &&
{truncate(project.name, 24)}}
+
+
{prettyStatus(task.status)}
+
+
+ {assignee ? (
+
+
+
+ {assignee.name}
+
+
+ ) : (
+
—
+ )}
+
+
event.stopPropagation()}>
+ {assignee && onRun(task)} disabled={!canRun(provider)} />}
+ onOpen(task)} />
+
+
+ );
+ })}
+
+ );
+}
+
+function truncate(value: string, max: number): string {
+ if (value.length <= max) return value;
+ return `${value.slice(0, max - 1)}…`;
+}
diff --git a/src/renderer/src/pages/work/TimelineView.tsx b/src/renderer/src/pages/work/TimelineView.tsx
new file mode 100644
index 00000000..6a2a0c46
--- /dev/null
+++ b/src/renderer/src/pages/work/TimelineView.tsx
@@ -0,0 +1,131 @@
+import { TASK_STATUS, type GoalRecord, type ProjectRecord, type TaskRecord, type TaskStatus } from "../../../../shared/domain";
+
+const SEGMENTS: ReadonlyArray<{ id: TaskStatus; label: string; tone: string }> = [
+ { id: TASK_STATUS.DONE, label: "Done", tone: "var(--green)" },
+ { id: TASK_STATUS.REVIEW, label: "In review", tone: "var(--amber)" },
+ { id: TASK_STATUS.IN_PROGRESS, label: "In progress", tone: "var(--accent)" },
+ { id: TASK_STATUS.BACKLOG, label: "Backlog", tone: "var(--panel-3)" },
+];
+
+/**
+ * Execution timeline by goal: each goal carries a segmented bar of its tasks
+ * by status (done → review → in progress → backlog), then one row per
+ * project with the same bar at project scope.
+ */
+export function TimelineView({ tasks, goals, projects }: { tasks: TaskRecord[]; goals: GoalRecord[]; projects: ProjectRecord[] }) {
+ const unlinked = tasks.filter((task) => !task.goalId);
+ return (
+
+ {goals.map((goal) => {
+ const linked = tasks.filter((task) => task.goalId === goal.id);
+ const goalProjects = projects.filter((project) => project.goalId === goal.id);
+ return (
+
+
+
+ {goal.title}
+
+
+
+
+
+ {goalProjects.length > 0 && (
+
+ {goalProjects.map((project) => {
+ const projectTasks = linked.filter((task) => task.projectId === project.id);
+ return (
+
+
+ {project.name}
+
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+ })}
+ {unlinked.length > 0 && (
+
+
+ Unlinked tasks
+
+
+
+
+
+ )}
+
+ );
+}
+
+function counts(tasks: TaskRecord[]): Map {
+ const map = new Map();
+ for (const segment of SEGMENTS) map.set(segment.id, 0);
+ for (const task of tasks) map.set(task.status, (map.get(task.status) ?? 0) + 1);
+ return map;
+}
+
+function StatusBar({ tasks, height, style }: { tasks: TaskRecord[]; height: number; style?: React.CSSProperties }) {
+ const byStatus = counts(tasks);
+ if (tasks.length === 0) {
+ return ;
+ }
+ return (
+
+ {SEGMENTS.map((segment) => {
+ const count = byStatus.get(segment.id) ?? 0;
+ if (count === 0) return null;
+ return (
+
+ );
+ })}
+
+ );
+}
+
+function Legend({ tasks }: { tasks: TaskRecord[] }) {
+ const byStatus = counts(tasks);
+ return (
+
+ {SEGMENTS.map((segment) => (
+
+
+ {byStatus.get(segment.id) ?? 0}
+ {segment.label}
+
+ ))}
+
+ );
+}
+
+function CompletionReadout({ tasks }: { tasks: TaskRecord[] }) {
+ const done = tasks.filter((task) => task.status === TASK_STATUS.DONE).length;
+ return (
+
+ {done} / {tasks.length}
+
+ );
+}