Q150: work can be sent back, reopened, moved to backlog, or blocked with a reason

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 01:12:17 -04:00
1 parent 7ab46bb2de
commit 809c67a54a
21 files changed
+153 -7

No files matched your search

+8
View File
@@ -31,6 +31,14 @@ export const zh: Record<string, string> = {
"Writing": "写作方式",
"superseded": "已被取代",
"stalled": "已停滞",
"blocked": "已阻塞",
"Block": "阻塞",
"Unblock": "解除阻塞",
"Block task": "阻塞任务",
"Why is it blocked?": "为什么阻塞?",
"Send back": "退回",
"Reopen": "重新打开",
"Back to backlog": "退回待办",
"needs you": "待你处理",
"waiting": "等待中",
"Resumes on its own at {time}.": "将于 {time} 自动恢复。",
+10 -1
View File
@@ -177,6 +177,12 @@ export function WorkPage({ snapshot, actions, navigate, focus }: PageProps) {
onAdvance={advance}
onRun={runTask}
onOpenTask={setDetailTaskId}
onMove={async (task, status) => {
await actions.run(() => window.agentCompany.updateTaskStatus({ orgId, taskId: task.id, status }));
}}
onBlock={async (task, reason) => {
await actions.run(() => window.agentCompany.setTaskBlocked({ orgId, taskId: task.id, reason }));
}}
onScope={(next) => {
setScope(next);
setDetailTaskId(null);
@@ -357,7 +363,10 @@ function TaskCard({
<div className="row" style={{ flexWrap: "wrap", gap: 6, minWidth: 0 }}>
{project ? <Pill>{project.name}</Pill> : <Pill>{capitalize(task.risk)}</Pill>}
{task.stalledReason && task.status !== "done" && <Pill tone="red">{t("stalled")}</Pill>}
{task.hold && !task.stalledReason && (
{task.blockedReason && task.status !== "done" && (
<span title={task.blockedReason} style={{ display: "inline-flex" }}><Pill tone="red">{t("blocked")}</Pill></span>
)}
{task.hold && !task.stalledReason && !task.blockedReason && (
<span title={task.hold.reason} style={{ display: "inline-flex" }}>
<Pill tone={task.hold.waitingOn === "operator" ? "red" : "amber"}>{task.hold.waitingOn === "operator" ? t("needs you") : t("waiting")}</Pill>
</span>
@@ -12,7 +12,7 @@ import type {
TaskRunRecord,
WorkspaceRecord,
} from "../../../../shared/domain";
import { TASK_STATUS, type WritingMode } from "../../../../shared/domain";
import { TASK_STATUS, type TaskStatus, type WritingMode } from "../../../../shared/domain";
import { auditEventLabel, formatRelative } from "../types";
import { WRITING_OPTIONS, descendantIds, nextStatus, prettyRunState, prettyStatus, providerFor, runBlocker, runsForTask, runTone } from "./task-helpers";
import type { SnapshotActions } from "../../app/useSnapshot";
@@ -43,6 +43,8 @@ export function TaskDetailDialog({
onAdvance,
onRun,
onOpenTask,
onMove,
onBlock,
onScope,
navigate,
}: {
@@ -62,10 +64,20 @@ export function TaskDetailDialog({
onAdvance: (task: TaskRecord) => void;
onRun: (task: TaskRecord) => void;
onOpenTask: (taskId: string) => void;
/** Moves the task to any status, backwards included: send back, reopen, back to backlog. */
onMove: (task: TaskRecord, status: TaskStatus) => Promise<void>;
/** Blocks the task with a reason, or unblocks it with null. */
onBlock: (task: TaskRecord, reason: string | null) => Promise<void>;
onScope: (scope: Focus) => void;
navigate: PageProps["navigate"];
}) {
const [tab, setTab] = useState<Tab>("overview");
const [blocking, setBlocking] = useState(false);
const [actionError, setActionError] = useState<string | null>(null);
const act = (operation: () => Promise<void>, after?: () => void) => {
setActionError(null);
operation().then(after, (error: unknown) => setActionError(error instanceof Error ? error.message : String(error)));
};
const [editing, setEditing] = useState(false);
const [editError, setEditError] = useState<string | null>(null);
@@ -148,8 +160,30 @@ export function TaskDetailDialog({
<ActivityTab audit={taskAudit} approvals={taskApprovals} onOpenApproval={(approvalId) => navigate("approvals", { kind: "approval", id: approvalId })} />
)}
{blocking && (
<form
className="row"
style={{ gap: 8, marginTop: 12 }}
onSubmit={(event) => {
event.preventDefault();
const reason = String(new FormData(event.currentTarget).get("reason") ?? "");
act(() => onBlock(task, reason), () => setBlocking(false));
}}
>
<input className="input" name="reason" aria-label={t("Why is it blocked?")} placeholder={t("Why is it blocked?")} required autoFocus style={{ flex: 1 }} />
<Button tone="primary" size="small" type="submit">{t("Block task")}</Button>
<Button tone="ghost" size="small" onClick={() => setBlocking(false)}>{t("Cancel")}</Button>
</form>
)}
{actionError && <div className="form-error" role="alert" style={{ marginTop: 8 }}>{actionError}</div>}
<div className="modal-actions">
<Button tone="ghost" onClick={deleteTask}><Icon name="x" size={16} /> {t("Delete")}</Button>
{task.blockedReason
? <Button onClick={() => act(() => onBlock(task, null))}>{t("Unblock")}</Button>
: task.status !== TASK_STATUS.DONE && !blocking && <Button onClick={() => setBlocking(true)}>{t("Block")}</Button>}
{task.status === TASK_STATUS.REVIEW && <Button onClick={() => act(() => onMove(task, TASK_STATUS.IN_PROGRESS))}>{t("Send back")}</Button>}
{task.status === TASK_STATUS.DONE && <Button onClick={() => act(() => onMove(task, TASK_STATUS.IN_PROGRESS))}>{t("Reopen")}</Button>}
{task.status === TASK_STATUS.IN_PROGRESS && <Button onClick={() => act(() => onMove(task, TASK_STATUS.BACKLOG))}>{t("Back to backlog")}</Button>}
{assignee && <Button onClick={() => onRun(task)} disabled={runBlocker(task, provider, taskRuns) !== null}><Icon name="send" size={16} /> {t("Run")}</Button>}
{advanceTo && <Button tone="primary" onClick={() => onAdvance(task)}><Icon name="check" size={16} /> {prettyStatus(advanceTo)}</Button>}
</div>