Implement full task inspector with subtasks, activity, and inline edit
Migration 041 adds parent_task_id to tasks for subtask hierarchy. TaskDetailDialog now has three tabs: Overview (description, assignee, project, goal, workspace, runs with error messages), Subtasks (child tasks with status), and Activity (audit events and approval history). Inline edit overlay covers title, description, assignee, risk, priority, and parent task. Task delete is available from the dialog footer.
This commit is contained in:
1 parent
7a36a12d08
commit
75e5d17a86
12 files changed
+289
-69
No files matched your search
@@ -96,6 +96,8 @@ export function registerIpcHandlers(deps: IpcDeps): void {
|
||||
ipcMain.handle(IPC_CHANNELS.createAgent, (_event, input) => mutate(() => organization.createAgent(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.updateAgent, (_event, input) => mutate(() => organization.updateAgent(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.createTask, (_event, input) => mutate(() => organization.createTask(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.updateTask, (_event, input) => mutate(() => organization.updateTask(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.deleteTask, (_event, input) => mutate(() => organization.deleteTask(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.updateTaskStatus, (_event, input) => mutate(() => organization.updateTaskStatus(input.orgId, input.taskId, input.status)));
|
||||
ipcMain.handle(IPC_CHANNELS.runTask, async (_event, input) => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const taskSubtasksDeleteSchema = `
|
||||
alter table tasks add column parent_task_id text references tasks(id) on delete set null;
|
||||
`;
|
||||
@@ -39,6 +39,7 @@ import { customProvidersSchema } from "./migrations/037-custom-providers";
|
||||
import { providerAuthProfileCascadeSchema } from "./migrations/038-provider-auth-profile-cascade";
|
||||
import { auditImmutabilityOrgTeardownSchema } from "./migrations/039-audit-immutability-org-teardown";
|
||||
import { documentsKnowledgeAutomationSchema } from "./migrations/040-documents-knowledge-automation";
|
||||
import { taskSubtasksDeleteSchema } from "./migrations/041-task-subtasks-delete";
|
||||
|
||||
interface Migration {
|
||||
version: number;
|
||||
@@ -87,6 +88,7 @@ const MIGRATIONS: Migration[] = [
|
||||
{ version: 38, name: "provider_auth_profile_cascade", sql: providerAuthProfileCascadeSchema },
|
||||
{ version: 39, name: "audit_immutability_org_teardown", sql: auditImmutabilityOrgTeardownSchema },
|
||||
{ version: 40, name: "documents_knowledge_automation", sql: documentsKnowledgeAutomationSchema },
|
||||
{ version: 41, name: "task_subtasks_delete", sql: taskSubtasksDeleteSchema },
|
||||
];
|
||||
|
||||
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { DatabaseSync } from "node:sqlite";
|
||||
import { APPROVAL_STATE, RISK_LEVEL, TASK_STATUS, type ApprovalState } from "../../shared/domain";
|
||||
import { PROVIDER_CATALOG } from "../../shared/provider-catalog";
|
||||
import { createId } from "../../shared/ids";
|
||||
import type { CreateAgentInput, CreateGoalInput, CreateOrganizationInput, CreateProjectInput, CreateTaskInput, CreateWorkspaceInput, UpdateAgentInput } from "../../shared/commands";
|
||||
import type { CreateAgentInput, CreateGoalInput, CreateOrganizationInput, CreateProjectInput, CreateTaskInput, CreateWorkspaceInput, DeleteTaskInput, UpdateAgentInput, UpdateTaskInput } from "../../shared/commands";
|
||||
import { AuditService } from "./audit-service";
|
||||
import { PolicyService } from "./policy-service";
|
||||
import { IdentityAccessService } from "./identity-access-service";
|
||||
@@ -445,6 +445,43 @@ export class OrganizationService {
|
||||
return id;
|
||||
}
|
||||
|
||||
updateTask(input: UpdateTaskInput): void {
|
||||
const now = new Date().toISOString();
|
||||
if (input.assigneeAgentId) this.verifyOptionalOrgEntity("agents", input.assigneeAgentId, input.orgId, "Assignee agent");
|
||||
if (input.parentTaskId) {
|
||||
const parent = this.db.prepare("select id from tasks where id = ? and org_id = ?").get(input.parentTaskId, input.orgId);
|
||||
if (!parent) throw new Error("Parent task was not found.");
|
||||
}
|
||||
const result = this.db.prepare(`
|
||||
update tasks set title = ?, description = ?, assignee_agent_id = ?, risk = ?, priority = ?, parent_task_id = ?, updated_at = ?
|
||||
where id = ? and org_id = ?
|
||||
`).run(input.title.trim(), input.description.trim(), input.assigneeAgentId, input.risk, input.priority, input.parentTaskId, now, input.taskId, input.orgId);
|
||||
if (result.changes === 0) throw new Error("Task was not found.");
|
||||
this.audit.record({
|
||||
orgId: input.orgId,
|
||||
entityType: "task",
|
||||
entityId: input.taskId,
|
||||
actorType: "employee",
|
||||
actorId: null,
|
||||
action: "updated",
|
||||
payload: { title: input.title.trim() },
|
||||
});
|
||||
}
|
||||
|
||||
deleteTask(input: DeleteTaskInput): void {
|
||||
const result = this.db.prepare("delete from tasks where id = ? and org_id = ?").run(input.taskId, input.orgId);
|
||||
if (result.changes === 0) throw new Error("Task was not found.");
|
||||
this.audit.record({
|
||||
orgId: input.orgId,
|
||||
entityType: "task",
|
||||
entityId: input.taskId,
|
||||
actorType: "employee",
|
||||
actorId: null,
|
||||
action: "deleted",
|
||||
payload: {},
|
||||
});
|
||||
}
|
||||
|
||||
updateTaskStatus(orgId: string, taskId: string, status: string): void {
|
||||
const result = this.db.prepare("update tasks set status = ?, updated_at = ? where id = ? and org_id = ?")
|
||||
.run(status, new Date().toISOString(), taskId, orgId);
|
||||
|
||||
@@ -15,6 +15,8 @@ const api: DesktopApi = {
|
||||
createAgent: (input) => invoke(IPC_CHANNELS.createAgent, input),
|
||||
updateAgent: (input) => invoke(IPC_CHANNELS.updateAgent, input),
|
||||
createTask: (input) => invoke(IPC_CHANNELS.createTask, input),
|
||||
updateTask: (input) => invoke(IPC_CHANNELS.updateTask, input),
|
||||
deleteTask: (input) => invoke(IPC_CHANNELS.deleteTask, input),
|
||||
updateTaskStatus: (input) => invoke(IPC_CHANNELS.updateTaskStatus, input),
|
||||
runTask: (input) => invoke(IPC_CHANNELS.runTask, input),
|
||||
createGoal: (input) => invoke(IPC_CHANNELS.createGoal, input),
|
||||
|
||||
@@ -141,6 +141,11 @@ export function WorkPage({ snapshot, actions }: PageProps) {
|
||||
workspaces={workspaces}
|
||||
providers={providers}
|
||||
taskRuns={taskRuns}
|
||||
allTasks={allTasks}
|
||||
approvals={snapshot.approvals.filter((a) => a.orgId === orgId)}
|
||||
auditEvents={snapshot.auditEvents.filter((e) => e.orgId === orgId)}
|
||||
orgId={orgId}
|
||||
actions={actions}
|
||||
onClose={() => setDetailTaskId(null)}
|
||||
onAdvance={advance}
|
||||
onRun={runTask}
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import { Avatar, Button, Icon, IconButton, Pill } from "../../shared/ui";
|
||||
import {
|
||||
type AgentRecord,
|
||||
type GoalRecord,
|
||||
type ProjectRecord,
|
||||
type ProviderRecord,
|
||||
type TaskRecord,
|
||||
type TaskRunRecord,
|
||||
type WorkspaceRecord,
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { Avatar, Button, Field, Icon, IconButton, Pill, Segmented } from "../../shared/ui";
|
||||
import type {
|
||||
AgentRecord,
|
||||
ApprovalRecord,
|
||||
AuditEventRecord,
|
||||
GoalRecord,
|
||||
ProjectRecord,
|
||||
ProviderRecord,
|
||||
TaskRecord,
|
||||
TaskRunRecord,
|
||||
WorkspaceRecord,
|
||||
} from "../../../../shared/domain";
|
||||
import { formatRelative } from "../types";
|
||||
import { TASK_PRIORITY, TASK_STATUS } from "../../../../shared/domain";
|
||||
import { auditEventLabel, formatRelative } from "../types";
|
||||
import { canRun, nextStatus, prettyRunState, prettyStatus, providerFor, runsForTask, runTone } from "./task-helpers";
|
||||
import type { SnapshotActions } from "../../app/useSnapshot";
|
||||
|
||||
type Tab = "overview" | "subtasks" | "activity";
|
||||
const TABS: Array<{ id: Tab; label: string }> = [
|
||||
{ id: "overview", label: "Overview" },
|
||||
{ id: "subtasks", label: "Subtasks" },
|
||||
{ id: "activity", label: "Activity" },
|
||||
];
|
||||
|
||||
export function TaskDetailDialog({
|
||||
task,
|
||||
@@ -19,6 +31,11 @@ export function TaskDetailDialog({
|
||||
workspaces,
|
||||
providers,
|
||||
taskRuns,
|
||||
allTasks,
|
||||
approvals,
|
||||
auditEvents,
|
||||
orgId,
|
||||
actions,
|
||||
onClose,
|
||||
onAdvance,
|
||||
onRun,
|
||||
@@ -30,94 +47,223 @@ export function TaskDetailDialog({
|
||||
workspaces: WorkspaceRecord[];
|
||||
providers: ProviderRecord[];
|
||||
taskRuns: TaskRunRecord[];
|
||||
allTasks: TaskRecord[];
|
||||
approvals: ApprovalRecord[];
|
||||
auditEvents: AuditEventRecord[];
|
||||
orgId: string;
|
||||
actions: SnapshotActions;
|
||||
onClose: () => void;
|
||||
onAdvance: (task: TaskRecord) => void;
|
||||
onRun: (task: TaskRecord) => void;
|
||||
}) {
|
||||
const [tab, setTab] = useState<Tab>("overview");
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
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 assignee = agents.find((a) => a.id === task.assigneeAgentId) ?? null;
|
||||
const project = projects.find((p) => p.id === task.projectId) ?? null;
|
||||
const goal = goals.find((g) => g.id === task.goalId) ?? null;
|
||||
const workspace = workspaces.find((w) => w.id === task.workspaceId) ?? null;
|
||||
const provider = providerFor(assignee, providers);
|
||||
const runs = runsForTask(task.id, taskRuns);
|
||||
const advanceTo = nextStatus(task.status);
|
||||
const subtasks = allTasks.filter((t) => t.parentTaskId === task.id);
|
||||
const taskApprovals = approvals.filter((a) => a.sourceEntityType === "task" && a.sourceEntityId === task.id);
|
||||
const taskAudit = auditEvents.filter((e) => e.entityType === "task" && e.entityId === task.id);
|
||||
|
||||
const submitEdit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const form = new FormData(event.currentTarget);
|
||||
const val = (key: string) => String(form.get(key) ?? "").trim();
|
||||
void actions.run(() => window.agentCompany.updateTask({
|
||||
orgId,
|
||||
taskId: task.id,
|
||||
title: val("title"),
|
||||
description: val("description"),
|
||||
assigneeAgentId: val("assigneeAgentId") || null,
|
||||
risk: (val("risk") || "low") as "low" | "medium" | "high",
|
||||
priority: (val("priority") || "normal") as "low" | "normal" | "high",
|
||||
parentTaskId: val("parentTaskId") || null,
|
||||
})).then((r) => { if (r) setEditing(false); });
|
||||
};
|
||||
|
||||
const deleteTask = () => {
|
||||
void actions.run(() => window.agentCompany.deleteTask({ orgId, taskId: task.id }));
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" role="presentation" onMouseDown={onClose}>
|
||||
<div className="modal" role="dialog" aria-label={task.title} onMouseDown={(event) => event.stopPropagation()}>
|
||||
<div className="row" style={{ alignItems: "flex-start", gap: 10, marginBottom: 12 }}>
|
||||
<div className="modal" style={{ maxWidth: 560 }} role="dialog" aria-label={task.title} onMouseDown={(e) => e.stopPropagation()}>
|
||||
<div className="row" style={{ alignItems: "flex-start", gap: 10, marginBottom: 8 }}>
|
||||
{task.priority === "high" && <span style={{ width: 3, height: 18, background: "var(--red)", borderRadius: 2, marginTop: 2, flexShrink: 0 }} />}
|
||||
<h2 className="modal-title" style={{ margin: 0, flex: 1 }}>{task.title}</h2>
|
||||
<IconButton icon="edit" label="Edit" onClick={() => setEditing(true)} />
|
||||
<IconButton icon="x" label="Close" onClick={onClose} />
|
||||
</div>
|
||||
|
||||
<div className="row" style={{ flexWrap: "wrap", gap: 6, marginBottom: 14 }}>
|
||||
<div className="row" style={{ flexWrap: "wrap", gap: 6, marginBottom: 12 }}>
|
||||
<Pill>{prettyStatus(task.status)}</Pill>
|
||||
<Pill tone={task.priority === "high" ? "amber" : undefined}>{task.priority}</Pill>
|
||||
{task.risk === "high" && <Pill tone="red">high risk</Pill>}
|
||||
{task.requiresFilesystem && <Pill>workspace</Pill>}
|
||||
</div>
|
||||
|
||||
{task.description && (
|
||||
<p style={{ margin: "0 0 16px", color: "var(--text-2)", fontSize: 13, lineHeight: 1.55, whiteSpace: "pre-wrap" }}>
|
||||
{task.description}
|
||||
</p>
|
||||
<div style={{ marginBottom: 12 }}><Segmented value={tab} onChange={setTab} options={TABS} /></div>
|
||||
|
||||
{tab === "overview" && (
|
||||
<OverviewTab task={task} assignee={assignee} project={project} goal={goal} workspace={workspace} provider={provider} runs={runs} />
|
||||
)}
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "100px 1fr", gap: "10px 14px", fontSize: 12.5 }}>
|
||||
<MetaLabel>Assignee</MetaLabel>
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
{assignee ? (
|
||||
<>
|
||||
<Avatar name={assignee.name} size="sm" bot />
|
||||
<span>{assignee.name}</span>
|
||||
{provider && <span style={{ color: "var(--text-3)" }}>· {provider.label}</span>}
|
||||
</>
|
||||
) : (
|
||||
<span style={{ color: "var(--text-4)" }}>Unassigned</span>
|
||||
)}
|
||||
</div>
|
||||
{project && (<><MetaLabel>Project</MetaLabel><div>{project.name}</div></>)}
|
||||
{goal && (<><MetaLabel>Goal</MetaLabel><div>{goal.title}</div></>)}
|
||||
{workspace && (<><MetaLabel>Workspace</MetaLabel><div>{workspace.name}</div></>)}
|
||||
</div>
|
||||
|
||||
{runs.length > 0 && (
|
||||
<div style={{ marginTop: 18 }}>
|
||||
<div className="section-label">Runs</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{runs.map((run) => (
|
||||
<div key={run.id} className="row" style={{ gap: 8 }}>
|
||||
<Pill tone={runTone(run.state)}>{prettyRunState(run.state)}</Pill>
|
||||
<span style={{ color: "var(--text-3)", fontSize: 12 }}>{run.trigger}</span>
|
||||
<span className="num" style={{ marginLeft: "auto", color: "var(--text-3)", fontSize: 12 }}>{formatRelative(run.createdAt)}</span>
|
||||
{run.state === "failed" && run.errorMessage && (
|
||||
<span style={{ color: "var(--red)", fontSize: 12, flexBasis: "100%" }}>{run.errorMessage}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{tab === "subtasks" && (
|
||||
<SubtasksTab subtasks={subtasks} agents={agents} />
|
||||
)}
|
||||
{tab === "activity" && (
|
||||
<ActivityTab audit={taskAudit} approvals={taskApprovals} />
|
||||
)}
|
||||
|
||||
<div className="modal-actions">
|
||||
{assignee && (
|
||||
<Button onClick={() => onRun(task)} disabled={!canRun(provider)}>
|
||||
<Icon name="send" size={13} /> Run
|
||||
</Button>
|
||||
)}
|
||||
{advanceTo && (
|
||||
<Button tone="primary" onClick={() => onAdvance(task)}>
|
||||
<Icon name="check" size={13} /> Advance to {prettyStatus(advanceTo)}
|
||||
</Button>
|
||||
)}
|
||||
<Button tone="ghost" onClick={deleteTask}><Icon name="x" size={13} /> Delete</Button>
|
||||
{assignee && <Button onClick={() => onRun(task)} disabled={!canRun(provider)}><Icon name="send" size={13} /> Run</Button>}
|
||||
{advanceTo && <Button tone="primary" onClick={() => onAdvance(task)}><Icon name="check" size={13} /> {prettyStatus(advanceTo)}</Button>}
|
||||
</div>
|
||||
|
||||
{editing && <EditOverlay task={task} agents={agents} allTasks={allTasks} onSubmit={submitEdit} onCancel={() => setEditing(false)} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OverviewTab({ task, assignee, project, goal, workspace, provider, runs }: {
|
||||
task: TaskRecord; assignee: AgentRecord | null; project: ProjectRecord | null;
|
||||
goal: GoalRecord | null; workspace: WorkspaceRecord | null; provider: ProviderRecord | null;
|
||||
runs: TaskRunRecord[];
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{task.description && (
|
||||
<p style={{ margin: "0 0 14px", color: "var(--text-2)", fontSize: 13, lineHeight: 1.55, whiteSpace: "pre-wrap" }}>{task.description}</p>
|
||||
)}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "100px 1fr", gap: "8px 14px", fontSize: 12.5 }}>
|
||||
<MetaLabel>Assignee</MetaLabel>
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
{assignee ? (<><Avatar name={assignee.name} size="sm" bot /><span>{assignee.name}</span>{provider && <span style={{ color: "var(--text-3)" }}>({provider.label})</span>}</>) : <span style={{ color: "var(--text-4)" }}>Unassigned</span>}
|
||||
</div>
|
||||
{project && (<><MetaLabel>Project</MetaLabel><div>{project.name}</div></>)}
|
||||
{goal && (<><MetaLabel>Goal</MetaLabel><div>{goal.title}</div></>)}
|
||||
{workspace && (<><MetaLabel>Workspace</MetaLabel><div>{workspace.name}</div></>)}
|
||||
</div>
|
||||
{runs.length > 0 && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<div className="section-label">Runs</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||
{runs.map((run) => (
|
||||
<div key={run.id} style={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
<Pill tone={runTone(run.state)}>{prettyRunState(run.state)}</Pill>
|
||||
<span style={{ color: "var(--text-3)", fontSize: 12 }}>{run.trigger}</span>
|
||||
<span className="num" style={{ marginLeft: "auto", color: "var(--text-3)", fontSize: 12 }}>{formatRelative(run.createdAt)}</span>
|
||||
</div>
|
||||
{run.state === "failed" && run.errorMessage && (
|
||||
<span style={{ color: "var(--red)", fontSize: 12, paddingLeft: 4 }}>{run.errorMessage}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SubtasksTab({ subtasks, agents }: { subtasks: TaskRecord[]; agents: AgentRecord[] }) {
|
||||
return (
|
||||
<div style={{ minHeight: 60 }}>
|
||||
{subtasks.length === 0 && <div style={{ fontSize: 13, color: "var(--text-3)" }}>No subtasks</div>}
|
||||
{subtasks.map((st) => {
|
||||
const owner = agents.find((a) => a.id === st.assigneeAgentId);
|
||||
return (
|
||||
<div key={st.id} className="row" style={{ gap: 10, padding: "8px 0", borderBottom: "1px solid var(--border-soft)" }}>
|
||||
<Icon name={st.status === TASK_STATUS.DONE ? "check" : "work"} size={13} color={st.status === TASK_STATUS.DONE ? "var(--green)" : "var(--text-3)"} />
|
||||
<span style={{ flex: 1, fontSize: 13, fontWeight: 480, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{st.title}</span>
|
||||
<Pill>{prettyStatus(st.status)}</Pill>
|
||||
{owner && <Avatar name={owner.name} size="sm" bot />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ActivityTab({ audit, approvals }: { audit: AuditEventRecord[]; approvals: ApprovalRecord[] }) {
|
||||
return (
|
||||
<div style={{ minHeight: 60 }}>
|
||||
{approvals.length > 0 && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div className="section-label">Approvals</div>
|
||||
{approvals.map((a) => (
|
||||
<div key={a.id} className="row" style={{ gap: 8, padding: "6px 0" }}>
|
||||
<Pill tone={a.state === "approved" ? "green" : a.state === "pending" ? "amber" : "red"}>{a.state}</Pill>
|
||||
<span style={{ fontSize: 12.5 }}>{a.title}</span>
|
||||
<span className="mono" style={{ marginLeft: "auto", fontSize: 11, color: "var(--text-3)" }}>{formatRelative(a.createdAt)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="section-label">Events</div>
|
||||
{audit.length === 0 && <div style={{ fontSize: 13, color: "var(--text-3)" }}>No events</div>}
|
||||
{audit.map((e) => (
|
||||
<div key={e.id} className="row" style={{ gap: 8, padding: "5px 0" }}>
|
||||
<span className="mono" style={{ fontSize: 11, color: "var(--text-3)", flexShrink: 0 }}>{formatRelative(e.createdAt)}</span>
|
||||
<span style={{ fontSize: 12.5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{auditEventLabel(e.entityType, e.action)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EditOverlay({ task, agents, allTasks, onSubmit, onCancel }: {
|
||||
task: TaskRecord; agents: AgentRecord[]; allTasks: TaskRecord[];
|
||||
onSubmit: (e: FormEvent<HTMLFormElement>) => void; onCancel: () => void;
|
||||
}) {
|
||||
const otherTasks = allTasks.filter((t) => t.id !== task.id && t.orgId === task.orgId);
|
||||
return (
|
||||
<div style={{ position: "absolute", inset: 0, background: "var(--bg)", borderRadius: 14, padding: 24, overflow: "auto", zIndex: 2 }}>
|
||||
<form onSubmit={onSubmit}>
|
||||
<h3 style={{ margin: "0 0 14px", fontSize: 15, fontWeight: 600 }}>Edit task</h3>
|
||||
<Field label="Title"><input className="input" name="title" defaultValue={task.title} required /></Field>
|
||||
<Field label="Description"><textarea className="input" name="description" rows={4} defaultValue={task.description} /></Field>
|
||||
<Field label="Assignee">
|
||||
<select className="input" name="assigneeAgentId" defaultValue={task.assigneeAgentId ?? ""}>
|
||||
<option value="">Unassigned</option>
|
||||
{agents.map((a) => <option key={a.id} value={a.id}>{a.name}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
|
||||
<Field label="Risk">
|
||||
<select className="input" name="risk" defaultValue={task.risk}>
|
||||
<option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option>
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Priority">
|
||||
<select className="input" name="priority" defaultValue={task.priority}>
|
||||
<option value="low">Low</option><option value="normal">Normal</option><option value="high">High</option>
|
||||
</select>
|
||||
</Field>
|
||||
</div>
|
||||
<Field label="Parent task">
|
||||
<select className="input" name="parentTaskId" defaultValue={task.parentTaskId ?? ""}>
|
||||
<option value="">None</option>
|
||||
{otherTasks.map((t) => <option key={t.id} value={t.id}>{t.title}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
<div className="modal-actions">
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
<Button tone="primary" type="submit">Save</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MetaLabel({ children }: { children: string }) {
|
||||
return <div style={{ color: "var(--text-3)" }}>{children}</div>;
|
||||
}
|
||||
@@ -90,6 +90,22 @@ export interface RunTaskInput {
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
export interface UpdateTaskInput {
|
||||
orgId: string;
|
||||
taskId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
assigneeAgentId: string | null;
|
||||
risk: RiskLevel;
|
||||
priority: TaskPriority;
|
||||
parentTaskId: string | null;
|
||||
}
|
||||
|
||||
export interface DeleteTaskInput {
|
||||
orgId: string;
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
export interface CreateGoalInput {
|
||||
orgId: string;
|
||||
title: string;
|
||||
|
||||
@@ -17,9 +17,11 @@ import type {
|
||||
CreateWorkspaceInput,
|
||||
DeleteAutomationRuleInput,
|
||||
DeleteDocumentInput,
|
||||
DeleteTaskInput,
|
||||
DeleteKnowledgeEntryInput,
|
||||
UpdateAutomationRuleInput,
|
||||
UpdateDocumentInput,
|
||||
UpdateTaskInput,
|
||||
CreateAuditReplayInput,
|
||||
CreateChatSpaceInput,
|
||||
CreateComplianceExportInput,
|
||||
@@ -99,6 +101,8 @@ export interface DesktopApi {
|
||||
createAgent(input: CreateAgentInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
updateAgent(input: UpdateAgentInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
createTask(input: CreateTaskInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
updateTask(input: UpdateTaskInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
deleteTask(input: DeleteTaskInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
updateTaskStatus(input: UpdateTaskStatusInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
runTask(input: RunTaskInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
createGoal(input: CreateGoalInput): Promise<DesktopResult<AppSnapshot>>;
|
||||
|
||||
@@ -105,6 +105,7 @@ export interface TaskRecord {
|
||||
goalId: string | null;
|
||||
workspaceId: string | null;
|
||||
priority: TaskPriority;
|
||||
parentTaskId: string | null;
|
||||
requiresFilesystem: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
@@ -7,6 +7,8 @@ export const IPC_CHANNELS = {
|
||||
createAgent: "agentcompany:create-agent",
|
||||
updateAgent: "agentcompany:update-agent",
|
||||
createTask: "agentcompany:create-task",
|
||||
updateTask: "agentcompany:update-task",
|
||||
deleteTask: "agentcompany:delete-task",
|
||||
updateTaskStatus: "agentcompany:update-task-status",
|
||||
runTask: "agentcompany:run-task",
|
||||
createApproval: "agentcompany:create-approval",
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("domain services", () => {
|
||||
const connection = createConnection();
|
||||
|
||||
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40]);
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41]);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user