Q43: manual runs refuse done tasks and tasks with a session in flight
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9be5ad06c1
commit
5698e61e5a
8 files changed
+86
-12
No files matched your search
@@ -79,6 +79,12 @@ export class TaskExecutionService {
|
||||
|
||||
async runTask(input: RunTaskInput): Promise<string> {
|
||||
const task = requireTask(this.db, input.orgId, input.taskId);
|
||||
if (task.status === TASK_STATUS.DONE) throw new Error(`"${task.title}" is done. Reopen it before running it again.`);
|
||||
const inFlight = this.db.prepare("select state from task_runs where task_id = ? and state in ('queued', 'running') limit 1")
|
||||
.get(task.id) as { state: string } | undefined;
|
||||
if (inFlight) {
|
||||
throw new Error(inFlight.state === "queued" ? `"${task.title}" already has a queued session; it will run shortly.` : `"${task.title}" already has a running session.`);
|
||||
}
|
||||
assertExecutionApproval(this.db, input.orgId, task);
|
||||
const agent = requireAssignedAgent(this.db, input.orgId, task);
|
||||
const provider = requireAgentProvider(this.db, input.orgId, agent);
|
||||
|
||||
@@ -29,6 +29,8 @@ export const zh: Record<string, string> = {
|
||||
"Integrations": "集成密钥",
|
||||
"Document": "文档",
|
||||
"Writing": "写作方式",
|
||||
"Done tasks do not run": "已完成的任务不能再运行",
|
||||
"A session is already queued or running": "已有会话在排队或运行中",
|
||||
"Conversational (house style)": "日常沟通(公司文风)",
|
||||
"Technical spec": "技术规格",
|
||||
"Structured output": "结构化输出",
|
||||
|
||||
@@ -20,7 +20,7 @@ import { TaskDetailDialog } from "./work/TaskDetailDialog";
|
||||
import { ProjectOverview } from "./work/ProjectOverview";
|
||||
import { ListView } from "./work/ListView";
|
||||
import { TimelineView } from "./work/TimelineView";
|
||||
import { canRun, latestTaskRun, nextStatus, prettyRunState, providerFor, runTone } from "./work/task-helpers";
|
||||
import { latestTaskRun, nextStatus, prettyRunState, providerFor, runBlocker, runTone } from "./work/task-helpers";
|
||||
import type { PageProps } from "./types";
|
||||
import { activeOrgId } from "./types";
|
||||
import { t } from "../i18n/i18n";
|
||||
@@ -125,6 +125,7 @@ export function WorkPage({ snapshot, actions }: PageProps) {
|
||||
agents={agents}
|
||||
projects={projects}
|
||||
providers={providers}
|
||||
taskRuns={taskRuns}
|
||||
onRun={runTask}
|
||||
onOpen={(task) => setDetailTaskId(task.id)}
|
||||
/>
|
||||
@@ -256,7 +257,7 @@ function BoardView({
|
||||
task={task}
|
||||
assignee={agents.find((agent) => agent.id === task.assigneeAgentId) ?? null}
|
||||
project={projects.find((project) => project.id === task.projectId) ?? null}
|
||||
provider={providerFor(agents.find((agent) => agent.id === task.assigneeAgentId) ?? null, providers)}
|
||||
blocker={runBlocker(task, providerFor(agents.find((agent) => agent.id === task.assigneeAgentId) ?? null, providers), taskRuns)}
|
||||
latestRun={latestTaskRun(task.id, taskRuns)}
|
||||
onAdvance={() => onAdvance(task)}
|
||||
onRun={() => onRun(task)}
|
||||
@@ -275,7 +276,7 @@ function TaskCard({
|
||||
task,
|
||||
assignee,
|
||||
project,
|
||||
provider,
|
||||
blocker,
|
||||
latestRun,
|
||||
onAdvance,
|
||||
onRun,
|
||||
@@ -284,7 +285,7 @@ function TaskCard({
|
||||
task: TaskRecord;
|
||||
assignee: AgentRecord | null;
|
||||
project: ProjectRecord | null;
|
||||
provider: ProviderRecord | null;
|
||||
blocker: string | null;
|
||||
latestRun: TaskRunRecord | null;
|
||||
onAdvance: () => void;
|
||||
onRun: () => void;
|
||||
@@ -337,7 +338,7 @@ function TaskCard({
|
||||
)}
|
||||
<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 && <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={blocker ?? t("Run {title}", { title: task.title })} onClick={onRun} disabled={blocker !== null} />}
|
||||
{nextStatus(task.status) && <IconButton icon="check" label={t("Advance")} onClick={onAdvance} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Avatar, IconButton, Pill } from "../../shared/ui";
|
||||
import type { AgentRecord, ProjectRecord, ProviderRecord, TaskRecord } from "../../../../shared/domain";
|
||||
import { canRun, prettyStatus, providerFor } from "./task-helpers";
|
||||
import type { AgentRecord, ProjectRecord, ProviderRecord, TaskRecord, TaskRunRecord } from "../../../../shared/domain";
|
||||
import { prettyStatus, providerFor, runBlocker } from "./task-helpers";
|
||||
import { t } from "../../i18n/i18n";
|
||||
|
||||
const GRID = "minmax(0, 3fr) 110px 96px minmax(0, 1.2fr) 88px";
|
||||
@@ -10,6 +10,7 @@ export function ListView({
|
||||
agents,
|
||||
projects,
|
||||
providers,
|
||||
taskRuns,
|
||||
onRun,
|
||||
onOpen,
|
||||
}: {
|
||||
@@ -17,6 +18,7 @@ export function ListView({
|
||||
agents: AgentRecord[];
|
||||
projects: ProjectRecord[];
|
||||
providers: ProviderRecord[];
|
||||
taskRuns: TaskRunRecord[];
|
||||
onRun: (task: TaskRecord) => void;
|
||||
onOpen: (task: TaskRecord) => void;
|
||||
}) {
|
||||
@@ -62,7 +64,7 @@ export function ListView({
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: 4 }} onClick={(event) => event.stopPropagation()}>
|
||||
{assignee && <IconButton icon="send" label={t("Run {title}", { title: task.title })} onClick={() => onRun(task)} disabled={!canRun(provider)} />}
|
||||
{assignee && <IconButton icon="send" label={t("Run {title}", { title: task.title })} onClick={() => onRun(task)} disabled={runBlocker(task, provider, taskRuns) !== null} />}
|
||||
<IconButton icon="more" label={t("Open {title}", { title: task.title })} onClick={() => onOpen(task)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
} from "../../../../shared/domain";
|
||||
import { TASK_STATUS } from "../../../../shared/domain";
|
||||
import { auditEventLabel, formatRelative } from "../types";
|
||||
import { canRun, nextStatus, prettyRunState, prettyStatus, providerFor, runsForTask, runTone } from "./task-helpers";
|
||||
import { nextStatus, prettyRunState, prettyStatus, providerFor, runBlocker, runsForTask, runTone } from "./task-helpers";
|
||||
import type { SnapshotActions } from "../../app/useSnapshot";
|
||||
import { t } from "../../i18n/i18n";
|
||||
|
||||
@@ -123,7 +123,7 @@ export function TaskDetailDialog({
|
||||
|
||||
<div className="modal-actions">
|
||||
<Button tone="ghost" onClick={deleteTask}><Icon name="x" size={16} /> {t("Delete")}</Button>
|
||||
{assignee && <Button onClick={() => onRun(task)} disabled={!canRun(provider)}><Icon name="send" size={16} /> {t("Run")}</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>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
TASK_STATUS,
|
||||
type AgentRecord,
|
||||
type ProviderRecord,
|
||||
type TaskRecord,
|
||||
type TaskRunRecord,
|
||||
type TaskStatus,
|
||||
} from "../../../../shared/domain";
|
||||
@@ -41,8 +42,12 @@ export function providerFor(assignee: AgentRecord | null, providers: ProviderRec
|
||||
return providers.find((provider) => provider.id === assignee.modelProviderId) ?? null;
|
||||
}
|
||||
|
||||
export function canRun(provider: ProviderRecord | null): boolean {
|
||||
return provider?.enabled === true && provider.authState === "configured";
|
||||
/** Why a manual run is not possible right now, or null when it is. */
|
||||
export function runBlocker(task: TaskRecord, provider: ProviderRecord | null, taskRuns: TaskRunRecord[]): string | null {
|
||||
if (provider?.enabled !== true || provider.authState !== "configured") return t("Connect a provider to run");
|
||||
if (task.status === "done") return t("Done tasks do not run");
|
||||
if (taskRuns.some((run) => run.taskId === task.id && (run.state === "queued" || run.state === "running"))) return t("A session is already queued or running");
|
||||
return null;
|
||||
}
|
||||
|
||||
export function latestTaskRun(taskId: string, taskRuns: TaskRunRecord[]): TaskRunRecord | null {
|
||||
|
||||
Reference in new issue
Block a user