From ae13111bffd72acfdab5a094744417e4c3939a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 28 May 2026 22:21:06 -0400 Subject: [PATCH] Open a task detail view from the Work board and list --- .../src/renderer/src/pages/WorkPage.tsx | 91 ++++++------- .../src/pages/work/TaskDetailDialog.tsx | 123 ++++++++++++++++++ .../renderer/src/pages/work/task-helpers.ts | 50 +++++++ AgentCompany-New/src/renderer/src/styles.css | 2 + 4 files changed, 222 insertions(+), 44 deletions(-) create mode 100644 AgentCompany-New/src/renderer/src/pages/work/TaskDetailDialog.tsx create mode 100644 AgentCompany-New/src/renderer/src/pages/work/task-helpers.ts diff --git a/AgentCompany-New/src/renderer/src/pages/WorkPage.tsx b/AgentCompany-New/src/renderer/src/pages/WorkPage.tsx index 54803b06..2be4392c 100644 --- a/AgentCompany-New/src/renderer/src/pages/WorkPage.tsx +++ b/AgentCompany-New/src/renderer/src/pages/WorkPage.tsx @@ -16,6 +16,8 @@ import { type WorkspaceRecord, } from "../../../shared/domain"; import { NewTaskDialog } from "./NewTaskDialog"; +import { TaskDetailDialog } from "./work/TaskDetailDialog"; +import { canRun, latestTaskRun, nextStatus, prettyRunState, prettyStatus, providerFor, runTone } from "./work/task-helpers"; import type { PageProps } from "./types"; import { activeOrgId } from "./types"; @@ -40,6 +42,7 @@ export function WorkPage({ snapshot, actions }: PageProps) { const [newTaskOpen, setNewTaskOpen] = useState(false); const [priorityFilter, setPriorityFilter] = useState("all"); const [assigneeFilter, setAssigneeFilter] = useState("all"); + const [detailTaskId, setDetailTaskId] = useState(null); const allTasks = useMemo(() => snapshot.tasks.filter((task) => task.orgId === orgId), [snapshot.tasks, orgId]); const tasks = useMemo( @@ -70,6 +73,7 @@ export function WorkPage({ snapshot, actions }: PageProps) { const runTask = (task: TaskRecord) => { void actions.run(() => window.agentCompany.runTask({ orgId, taskId: task.id })); }; + const detailTask = detailTaskId ? allTasks.find((task) => task.id === detailTaskId) ?? null : null; return (
@@ -101,12 +105,34 @@ export function WorkPage({ snapshot, actions }: PageProps) { onAdvance={advance} onRun={runTask} onCreate={() => setNewTaskOpen(true)} + onOpen={(task) => setDetailTaskId(task.id)} + /> + )} + {view === "list" && ( + setDetailTaskId(task.id)} /> )} - {view === "list" && } {view === "timeline" && } setNewTaskOpen(false)} /> + setDetailTaskId(null)} + onAdvance={advance} + onRun={runTask} + />
); } @@ -177,6 +203,7 @@ function BoardView({ onAdvance, onRun, onCreate, + onOpen, }: { tasks: TaskRecord[]; agents: AgentRecord[]; @@ -188,6 +215,7 @@ function BoardView({ onAdvance: (task: TaskRecord) => void; onRun: (task: TaskRecord) => void; onCreate: () => void; + onOpen: (task: TaskRecord) => void; }) { return (
@@ -213,6 +241,7 @@ function BoardView({ latestRun={latestTaskRun(task.id, taskRuns)} onAdvance={() => onAdvance(task)} onRun={() => onRun(task)} + onOpen={() => onOpen(task)} /> ))}
@@ -228,12 +257,14 @@ function ListView({ projects, providers, onRun, + onOpen, }: { tasks: TaskRecord[]; agents: AgentRecord[]; projects: ProjectRecord[]; providers: ProviderRecord[]; onRun: (task: TaskRecord) => void; + onOpen: (task: TaskRecord) => void; }) { return (
@@ -251,7 +282,11 @@ function ListView({ 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)" }} >
@@ -271,9 +306,9 @@ function ListView({ — )}
-
+
event.stopPropagation()}> {assignee && onRun(task)} disabled={!canRun(provider)} />} - + onOpen(task)} />
); @@ -316,6 +351,7 @@ function TaskCard({ latestRun, onAdvance, onRun, + onOpen, }: { task: TaskRecord; assignee: AgentRecord | null; @@ -326,9 +362,14 @@ function TaskCard({ latestRun: TaskRunRecord | null; onAdvance: () => void; onRun: () => void; + onOpen: () => void; }) { return (
{ if (event.key === "Enter") onOpen(); }} style={{ background: "var(--panel)", border: "1px solid var(--border)", @@ -337,6 +378,7 @@ function TaskCard({ display: "flex", flexDirection: "column", gap: 10, + cursor: "pointer", opacity: task.status === "done" ? 0.7 : 1, }} > @@ -362,7 +404,7 @@ function TaskCard({ {workspace && {workspace.name}} {task.requiresFilesystem && workspace} {latestRun && {prettyRunState(latestRun.state)}} -
+
event.stopPropagation()}> {assignee && } {assignee && } {nextStatus(task.status) && } @@ -372,45 +414,6 @@ function TaskCard({ ); } -function nextStatus(status: TaskStatus): TaskStatus | null { - if (status === TASK_STATUS.BACKLOG) return TASK_STATUS.IN_PROGRESS; - if (status === TASK_STATUS.IN_PROGRESS) return TASK_STATUS.REVIEW; - if (status === TASK_STATUS.REVIEW) return TASK_STATUS.DONE; - return null; -} - -function prettyStatus(status: TaskStatus): string { - return status.replaceAll("_", " "); -} - -function prettyRunState(state: string): string { - return state.replaceAll("_", " "); -} - -function providerFor(assignee: AgentRecord | null, providers: ProviderRecord[]): ProviderRecord | null { - if (!assignee?.modelProviderId) return null; - return providers.find((provider) => provider.id === assignee.modelProviderId) ?? null; -} - -function canRun(provider: ProviderRecord | null): boolean { - return provider?.enabled === true && provider.authState === "configured"; -} - -function latestTaskRun(taskId: string, taskRuns: TaskRunRecord[]): TaskRunRecord | null { - for (let index = taskRuns.length - 1; index >= 0; index -= 1) { - const run = taskRuns[index]; - if (run?.taskId === taskId) return run; - } - return null; -} - -function runTone(state: string): "amber" | "green" | "red" | undefined { - if (state === "running" || state === "queued") return "amber"; - if (state === "completed") return "green"; - if (state === "failed") return "red"; - return undefined; -} - function capitalize(value: string): string { return value.charAt(0).toUpperCase() + value.slice(1); } diff --git a/AgentCompany-New/src/renderer/src/pages/work/TaskDetailDialog.tsx b/AgentCompany-New/src/renderer/src/pages/work/TaskDetailDialog.tsx new file mode 100644 index 00000000..25433c1d --- /dev/null +++ b/AgentCompany-New/src/renderer/src/pages/work/TaskDetailDialog.tsx @@ -0,0 +1,123 @@ +import { Avatar, Button, Icon, IconButton, Pill } from "../../shared/ui"; +import { + type AgentRecord, + type GoalRecord, + type ProjectRecord, + type ProviderRecord, + type TaskRecord, + type TaskRunRecord, + type WorkspaceRecord, +} from "../../../../shared/domain"; +import { formatRelative } from "../types"; +import { canRun, nextStatus, prettyRunState, prettyStatus, providerFor, runsForTask, runTone } from "./task-helpers"; + +export function TaskDetailDialog({ + task, + agents, + projects, + goals, + workspaces, + providers, + taskRuns, + onClose, + onAdvance, + onRun, +}: { + task: TaskRecord | null; + agents: AgentRecord[]; + projects: ProjectRecord[]; + goals: GoalRecord[]; + workspaces: WorkspaceRecord[]; + providers: ProviderRecord[]; + taskRuns: TaskRunRecord[]; + onClose: () => void; + onAdvance: (task: TaskRecord) => void; + onRun: (task: TaskRecord) => void; +}) { + if (!task) return null; + const assignee = agents.find((agent) => agent.id === task.assigneeAgentId) ?? null; + const project = projects.find((item) => item.id === task.projectId) ?? null; + const goal = goals.find((item) => item.id === task.goalId) ?? null; + const workspace = workspaces.find((item) => item.id === task.workspaceId) ?? null; + const provider = providerFor(assignee, providers); + const runs = runsForTask(task.id, taskRuns); + const advanceTo = nextStatus(task.status); + + return ( +
+
event.stopPropagation()}> +
+ {task.priority === "high" && } +

{task.title}

+ +
+ +
+ {prettyStatus(task.status)} + {task.priority} + {task.risk === "high" && high risk} + {task.requiresFilesystem && workspace} +
+ + {task.description && ( +

+ {task.description} +

+ )} + +
+ Assignee +
+ {assignee ? ( + <> + + {assignee.name} + {provider && · {provider.label}} + + ) : ( + Unassigned + )} +
+ {project && (<>Project
{project.name}
)} + {goal && (<>Goal
{goal.title}
)} + {workspace && (<>Workspace
{workspace.name}
)} +
+ + {runs.length > 0 && ( +
+
Runs
+
+ {runs.map((run) => ( +
+ {prettyRunState(run.state)} + {run.trigger} + {formatRelative(run.createdAt)} + {run.state === "failed" && run.errorMessage && ( + {run.errorMessage} + )} +
+ ))} +
+
+ )} + +
+ {assignee && ( + + )} + {advanceTo && ( + + )} +
+
+
+ ); +} + +function MetaLabel({ children }: { children: string }) { + return
{children}
; +} diff --git a/AgentCompany-New/src/renderer/src/pages/work/task-helpers.ts b/AgentCompany-New/src/renderer/src/pages/work/task-helpers.ts new file mode 100644 index 00000000..52410ab0 --- /dev/null +++ b/AgentCompany-New/src/renderer/src/pages/work/task-helpers.ts @@ -0,0 +1,50 @@ +import { + TASK_STATUS, + type AgentRecord, + type ProviderRecord, + type TaskRunRecord, + type TaskStatus, +} from "../../../../shared/domain"; + +export function nextStatus(status: TaskStatus): TaskStatus | null { + if (status === TASK_STATUS.BACKLOG) return TASK_STATUS.IN_PROGRESS; + if (status === TASK_STATUS.IN_PROGRESS) return TASK_STATUS.REVIEW; + if (status === TASK_STATUS.REVIEW) return TASK_STATUS.DONE; + return null; +} + +export function prettyStatus(status: TaskStatus): string { + return status.replaceAll("_", " "); +} + +export function prettyRunState(state: string): string { + return state.replaceAll("_", " "); +} + +export function runTone(state: string): "amber" | "green" | "red" | undefined { + if (state === "running" || state === "queued") return "amber"; + if (state === "completed") return "green"; + if (state === "failed") return "red"; + return undefined; +} + +export function providerFor(assignee: AgentRecord | null, providers: ProviderRecord[]): ProviderRecord | null { + if (!assignee?.modelProviderId) return null; + return providers.find((provider) => provider.id === assignee.modelProviderId) ?? null; +} + +export function canRun(provider: ProviderRecord | null): boolean { + return provider?.enabled === true && provider.authState === "configured"; +} + +export function latestTaskRun(taskId: string, taskRuns: TaskRunRecord[]): TaskRunRecord | null { + for (let index = taskRuns.length - 1; index >= 0; index -= 1) { + const run = taskRuns[index]; + if (run?.taskId === taskId) return run; + } + return null; +} + +export function runsForTask(taskId: string, taskRuns: TaskRunRecord[]): TaskRunRecord[] { + return taskRuns.filter((run) => run.taskId === taskId); +} diff --git a/AgentCompany-New/src/renderer/src/styles.css b/AgentCompany-New/src/renderer/src/styles.css index cd5bbebf..13f4a4f3 100644 --- a/AgentCompany-New/src/renderer/src/styles.css +++ b/AgentCompany-New/src/renderer/src/styles.css @@ -517,6 +517,8 @@ textarea.input { min-height: 84px; resize: vertical; line-height: 1.5; } border-bottom: 1px solid var(--border-soft); } .table-row:last-child { border-bottom: 0; } +.table-row.clickable { cursor: pointer; } +.table-row.clickable:hover { background: var(--hover); } .split-main { display: flex; flex-direction: row; min-height: 0; flex: 1; } .secondary-pane {