mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[feature] Panel per-cell project picker + pnpm format infra
MegaTask root-subtasks can fan out across cells (be+fe, fe+uxui). Since a RoboCo project is per-cell (ProjectTable.assigned_cell), a monorepo is N per-cell projects sharing one git_url — so multi-cell IS multi-project. The batch-review card now shows one project Select per the_work entry, scoped to that cell's repos, instead of one Select bound to a single top-level project_id. confirmBatch validates each cell's project is in scope and the batch still spans >=2 distinct projects. - prompter.ts: CellWork gains optional project_id (the per-cell picker seam). - batch-review-card.tsx: per-cell Selects (one per the_work entry), scoped to the cell's projects; legacy single-cell drafts keep the one-Select path. - use-prompter.ts: updateBatchDraftProject edits per-cell (entryIndex); confirmBatch validates every cell; batchFromEvent parses per-cell map. Also adds the missing pnpm format infrastructure (the panel had no formatter at all): prettier devDep + .prettierrc.json (default-style config: 80-col, double-quote, semi, trailing-comma-all) + .prettierignore, plus format / format:check scripts. Only the 3 changed files above were reformatted; the ~222 pre-existing non-compliant files are left untouched (a wholesale reformat is a separate explicit decision, not bundled into this feature).
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
# Build / generated output
|
||||
.next/
|
||||
out/
|
||||
build/
|
||||
coverage/
|
||||
|
||||
# Deps + lockfiles
|
||||
node_modules/
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Generated
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
+4
-1
@@ -10,7 +10,9 @@
|
||||
"lint": "eslint",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --coverage",
|
||||
"test:watch": "vitest"
|
||||
"test:watch": "vitest",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
@@ -66,6 +68,7 @@
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.1",
|
||||
"jsdom": "^29.1.1",
|
||||
"prettier": "^3.8.5",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5",
|
||||
|
||||
Generated
+13
-3
@@ -109,7 +109,7 @@ importers:
|
||||
version: 10.1.0(@types/react@19.2.8)(react@19.2.3)
|
||||
recharts:
|
||||
specifier: ^3.8.1
|
||||
version: 3.8.1(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1)
|
||||
version: 3.8.1(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react-is@17.0.2)(react@19.2.3)(redux@5.0.1)
|
||||
remark-gfm:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
@@ -162,6 +162,9 @@ importers:
|
||||
jsdom:
|
||||
specifier: ^29.1.1
|
||||
version: 29.1.1
|
||||
prettier:
|
||||
specifier: ^3.8.5
|
||||
version: 3.8.5
|
||||
tailwindcss:
|
||||
specifier: ^4
|
||||
version: 4.1.18
|
||||
@@ -3230,6 +3233,11 @@ packages:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
prettier@3.8.5:
|
||||
resolution: {integrity: sha512-zxcTTCedNGJM4R8sj/Cq/F0W/c4iE0afWBcBwMTRtw4WHYP9TWkYjdiH3npPRUYsXQCPR0hTU9yjovOu+E6EQA==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
pretty-format@27.5.1:
|
||||
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||
@@ -7228,6 +7236,8 @@ snapshots:
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier@3.8.5: {}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
@@ -7317,7 +7327,7 @@ snapshots:
|
||||
|
||||
react@19.2.3: {}
|
||||
|
||||
recharts@3.8.1(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1):
|
||||
recharts@3.8.1(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react-is@17.0.2)(react@19.2.3)(redux@5.0.1):
|
||||
dependencies:
|
||||
'@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1))(react@19.2.3)
|
||||
clsx: 2.1.1
|
||||
@@ -7327,7 +7337,7 @@ snapshots:
|
||||
immer: 10.2.0
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
react-is: 16.13.1
|
||||
react-is: 17.0.2
|
||||
react-redux: 9.3.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1)
|
||||
reselect: 5.1.1
|
||||
tiny-invariant: 1.3.3
|
||||
|
||||
@@ -20,15 +20,50 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { useProjects } from "@/hooks/use-projects";
|
||||
import type { BatchProposal, StartRoute } from "@/hooks/use-prompter";
|
||||
import type { CellWork, DraftProposal } from "@/lib/api/prompter";
|
||||
import { Team } from "@/types";
|
||||
|
||||
/** The delivery cells a multi-cell draft fans out to (one the_work entry each). */
|
||||
const CELL_TEAMS: Team[] = [Team.BACKEND, Team.FRONTEND, Team.UX_UI];
|
||||
|
||||
const CELL_LABEL: Record<string, string> = {
|
||||
backend: "Backend",
|
||||
frontend: "Frontend",
|
||||
ux_ui: "UX/UI",
|
||||
};
|
||||
|
||||
/** One the_work entry whose team is a delivery cell — a per-cell project picker. */
|
||||
interface CellEntry {
|
||||
entry: CellWork;
|
||||
entryIndex: number;
|
||||
team: Team;
|
||||
}
|
||||
|
||||
/** A draft's per-cell entries (the the_work slots that carry a cell team), in
|
||||
* the_work order. Empty for a legacy single-cell draft with no cell the_work. */
|
||||
function cellEntries(draft: DraftProposal): CellEntry[] {
|
||||
return (draft.the_work ?? [])
|
||||
.map((entry, entryIndex) => ({ entry, entryIndex, team: entry?.team }))
|
||||
.filter(
|
||||
(e): e is CellEntry =>
|
||||
!!e.team && (CELL_TEAMS as readonly string[]).includes(e.team),
|
||||
);
|
||||
}
|
||||
|
||||
interface BatchReviewCardProps {
|
||||
batch: BatchProposal;
|
||||
/** The conflict-free waves (lists of draft indices), once previewed. */
|
||||
waves: number[][] | null;
|
||||
/** The repos this MegaTask is scoped to — each task must target one of them. */
|
||||
/** The repos this MegaTask is scoped to — each cell must target one of them. */
|
||||
projectIds: string[];
|
||||
onKeepChatting: () => void;
|
||||
onProjectChange: (index: number, projectId: string) => void;
|
||||
/** `entryIndex` is the the_work slot (the cell); -1 for a legacy single-cell
|
||||
* draft with no per-cell map (sets the top-level project_id). */
|
||||
onProjectChange: (
|
||||
index: number,
|
||||
entryIndex: number,
|
||||
projectId: string,
|
||||
) => void;
|
||||
onConfirm: (route: StartRoute) => void;
|
||||
/** A launch is in flight — disable the actions so a double-click can't dupe. */
|
||||
isLaunching?: boolean;
|
||||
@@ -36,9 +71,11 @@ interface BatchReviewCardProps {
|
||||
|
||||
/**
|
||||
* The MegaTask review card: every task the agent proposed in one batch, each
|
||||
* with its target project (editable) and collision surface, plus the
|
||||
* conflict-free wave plan. The human reviews the whole batch and the sequencing,
|
||||
* fixes any task in the wrong repo, then picks one start path for all of them.
|
||||
* with its per-cell target projects (editable) and collision surface, plus the
|
||||
* conflict-free wave plan. A multi-cell task (be+fe, fe+uxui) shows one project
|
||||
* picker per cell, scoped to that cell's repos — a RoboCo project is per-cell,
|
||||
* so each cell lands in its own repo. The human reviews the whole batch and the
|
||||
* sequencing, fixes any cell in the wrong repo, then picks one start path.
|
||||
*/
|
||||
export function BatchReviewCard({
|
||||
batch,
|
||||
@@ -51,14 +88,21 @@ export function BatchReviewCard({
|
||||
}: BatchReviewCardProps) {
|
||||
const { data: allProjects = [] } = useProjects();
|
||||
// Only the scoped repos are valid targets (the agent read only those).
|
||||
const projects = allProjects.filter((p) => projectIds.includes(p.id));
|
||||
const scoped = new Set(projectIds);
|
||||
const titleOf = (i: number): string =>
|
||||
batch.drafts[i]?.title ?? `Task ${i + 1}`;
|
||||
// A task is mis-targeted unless its project is one of the scoped repos.
|
||||
const missingProject = batch.drafts.some(
|
||||
(d) => !d.project_id || !scoped.has(d.project_id),
|
||||
);
|
||||
// A task is mis-targeted when any of its cells lacks a scoped project (a
|
||||
// multi-cell draft checks every the_work entry; a legacy single-cell draft
|
||||
// with no cell map checks its top-level project_id).
|
||||
const missingProject = batch.drafts.some((d) => {
|
||||
const entries = cellEntries(d);
|
||||
if (entries.length > 0) {
|
||||
return entries.some(
|
||||
(ce) => !ce.entry.project_id || !scoped.has(ce.entry.project_id),
|
||||
);
|
||||
}
|
||||
return !d.project_id || !scoped.has(d.project_id);
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="border-primary/40 bg-primary/5">
|
||||
@@ -79,67 +123,121 @@ export function BatchReviewCard({
|
||||
|
||||
<CardContent className="space-y-3 pb-3">
|
||||
<ol className="space-y-2">
|
||||
{batch.drafts.map((draft, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="rounded-md border bg-background/60 px-3 py-2 text-sm"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<span className="font-medium leading-tight">
|
||||
{i + 1}. {draft.title}
|
||||
</span>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{draft.adds_migration && (
|
||||
<Badge variant="outline" className="gap-1 text-xs">
|
||||
<Database className="h-3 w-3" />
|
||||
migration
|
||||
</Badge>
|
||||
)}
|
||||
{draft.touches_shared && (
|
||||
<Badge variant="outline" className="gap-1 text-xs">
|
||||
<Share2 className="h-3 w-3" />
|
||||
shared
|
||||
</Badge>
|
||||
)}
|
||||
{batch.drafts.map((draft, i) => {
|
||||
const entries = cellEntries(draft);
|
||||
return (
|
||||
<li
|
||||
key={i}
|
||||
className="rounded-md border bg-background/60 px-3 py-2 text-sm"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<span className="font-medium leading-tight">
|
||||
{i + 1}. {draft.title}
|
||||
</span>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{draft.adds_migration && (
|
||||
<Badge variant="outline" className="gap-1 text-xs">
|
||||
<Database className="h-3 w-3" />
|
||||
migration
|
||||
</Badge>
|
||||
)}
|
||||
{draft.touches_shared && (
|
||||
<Badge variant="outline" className="gap-1 text-xs">
|
||||
<Share2 className="h-3 w-3" />
|
||||
shared
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{(draft.objective || draft.description) && (
|
||||
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
||||
{draft.objective || draft.description}
|
||||
</p>
|
||||
)}
|
||||
{/* Per-task project — editable so a misfiled task can be fixed */}
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">Project</span>
|
||||
<Select
|
||||
value={
|
||||
draft.project_id && scoped.has(draft.project_id)
|
||||
? draft.project_id
|
||||
: ""
|
||||
}
|
||||
onValueChange={(v) => onProjectChange(i, v)}
|
||||
disabled={isLaunching}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={`h-7 flex-1 text-xs ${
|
||||
draft.project_id && scoped.has(draft.project_id)
|
||||
? ""
|
||||
: "border-destructive"
|
||||
}`}
|
||||
>
|
||||
<SelectValue placeholder="Pick a project…" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{projects.map((p) => (
|
||||
<SelectItem key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
{(draft.objective || draft.description) && (
|
||||
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
||||
{draft.objective || draft.description}
|
||||
</p>
|
||||
)}
|
||||
{entries.length > 0 ? (
|
||||
/* Per-cell project picker — one Select per the_work entry,
|
||||
scoped to that cell's repos (a project is per-cell). */
|
||||
<div className="mt-1.5 space-y-1">
|
||||
{entries.map(({ entry, entryIndex, team }) => {
|
||||
const cellProjects = allProjects.filter(
|
||||
(p) => scoped.has(p.id) && p.assigned_cell === team,
|
||||
);
|
||||
const pid = entry.project_id ?? "";
|
||||
const ok = pid !== "" && scoped.has(pid);
|
||||
return (
|
||||
<div
|
||||
key={entryIndex}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<span className="w-16 shrink-0 text-xs text-muted-foreground">
|
||||
{CELL_LABEL[team] ?? team}
|
||||
</span>
|
||||
<Select
|
||||
value={ok ? pid : ""}
|
||||
onValueChange={(v) =>
|
||||
onProjectChange(i, entryIndex, v)
|
||||
}
|
||||
disabled={isLaunching}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={`h-7 flex-1 text-xs ${
|
||||
ok ? "" : "border-destructive"
|
||||
}`}
|
||||
>
|
||||
<SelectValue placeholder="Pick a project…" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{cellProjects.map((p) => (
|
||||
<SelectItem key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
/* Legacy single-cell draft (no per-cell the_work) — one Select
|
||||
bound to the top-level project_id, scoped to all repos. */
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Project
|
||||
</span>
|
||||
<Select
|
||||
value={
|
||||
draft.project_id && scoped.has(draft.project_id)
|
||||
? draft.project_id
|
||||
: ""
|
||||
}
|
||||
onValueChange={(v) => onProjectChange(i, -1, v)}
|
||||
disabled={isLaunching}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={`h-7 flex-1 text-xs ${
|
||||
draft.project_id && scoped.has(draft.project_id)
|
||||
? ""
|
||||
: "border-destructive"
|
||||
}`}
|
||||
>
|
||||
<SelectValue placeholder="Pick a project…" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{allProjects
|
||||
.filter((p) => scoped.has(p.id))
|
||||
.map((p) => (
|
||||
<SelectItem key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
|
||||
{/* Wave plan — how the batch will be sequenced */}
|
||||
@@ -161,7 +259,8 @@ export function BatchReviewCard({
|
||||
|
||||
{missingProject && (
|
||||
<p className="text-xs text-destructive">
|
||||
Pick a project for every task before launching the MegaTask.
|
||||
Pick a project for every cell of every task before launching the
|
||||
MegaTask.
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
@@ -59,6 +59,26 @@ export interface BatchProposal {
|
||||
/** Which start button the human pressed on the draft card. */
|
||||
export type StartRoute = "board" | "main_pm";
|
||||
|
||||
/** The delivery cells that carry their own per-cell project in a MegaTask draft.
|
||||
* A RoboCo project is per-cell (assigned_cell); a multi-cell draft puts one
|
||||
* the_work entry per cell, each with its cell's project_id. */
|
||||
const CELL_TEAMS: Team[] = [Team.BACKEND, Team.FRONTEND, Team.UX_UI];
|
||||
|
||||
/** The per-cell project_ids a draft targets: one per the_work entry whose team
|
||||
* is a delivery cell and that carries a project_id. Empty for a legacy
|
||||
* single-cell draft that uses a top-level project_id instead. */
|
||||
function draftCellProjectIds(draft: DraftProposal): string[] {
|
||||
return (draft.the_work ?? [])
|
||||
.filter(
|
||||
(w): w is CellWork & { project_id: string } =>
|
||||
!!w?.team &&
|
||||
(CELL_TEAMS as readonly string[]).includes(w.team) &&
|
||||
typeof w.project_id === "string" &&
|
||||
w.project_id !== "",
|
||||
)
|
||||
.map((w) => w.project_id);
|
||||
}
|
||||
|
||||
export interface EditableDraft {
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -784,19 +804,25 @@ export function usePrompter() {
|
||||
// Confirm a MegaTask — create the umbrella + sequenced root-subtasks, reap
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/** Reassign one task in the proposed MegaTask to a different project. Lets the
|
||||
* human fix a draft the agent put in the wrong (or no) repo before launch.
|
||||
/** Reassign one cell of one task in the proposed MegaTask to a different
|
||||
* project. `entryIndex` is the the_work slot (the cell); pass -1 for a legacy
|
||||
* single-cell draft that has no per-cell map (sets the top-level project_id).
|
||||
* Project does not affect the wave plan (waves derive from collision surface),
|
||||
* so the previewed waves stay valid. */
|
||||
const updateBatchDraftProject = useCallback(
|
||||
(index: number, projectId: string) => {
|
||||
(index: number, entryIndex: number, projectId: string) => {
|
||||
setBatch((prev) => {
|
||||
if (!prev) return prev;
|
||||
return {
|
||||
...prev,
|
||||
drafts: prev.drafts.map((d, i) =>
|
||||
i === index ? { ...d, project_id: projectId } : d,
|
||||
),
|
||||
drafts: prev.drafts.map((d, i) => {
|
||||
if (i !== index) return d;
|
||||
if (entryIndex < 0) return { ...d, project_id: projectId };
|
||||
const the_work = (d.the_work ?? []).map((w, wi) =>
|
||||
wi === entryIndex ? { ...w, project_id: projectId } : w,
|
||||
);
|
||||
return { ...d, the_work };
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -819,18 +845,32 @@ export function usePrompter() {
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Every task must target one of the scoped repos (the agent assigns it,
|
||||
// the human can fix it). The backend re-asserts this authoritatively.
|
||||
// Every cell of every task must target one of the scoped repos (the agent
|
||||
// assigns it, the human can fix it). A multi-cell draft carries its
|
||||
// per-cell project_ids in the_work[]; a legacy single-cell draft falls back
|
||||
// to its top-level project_id. The backend re-asserts this authoritatively.
|
||||
const scoped = scopeRef.current.projectIds;
|
||||
const offender = batch.drafts.findIndex(
|
||||
(d) => !d.project_id || !scoped.includes(d.project_id),
|
||||
);
|
||||
if (offender !== -1) {
|
||||
toast.error(
|
||||
`Task ${offender + 1} ("${batch.drafts[offender].title}") needs one of ` +
|
||||
"this MegaTask's selected projects. Pick it in the review card.",
|
||||
);
|
||||
return;
|
||||
const scopedSet = new Set(scoped);
|
||||
for (let i = 0; i < batch.drafts.length; i++) {
|
||||
const d = batch.drafts[i];
|
||||
const pids = draftCellProjectIds(d);
|
||||
const targets =
|
||||
pids.length > 0 ? pids : d.project_id ? [d.project_id] : [];
|
||||
if (targets.length === 0) {
|
||||
toast.error(
|
||||
`Task ${i + 1} ("${d.title}") has no project. ` +
|
||||
"Pick one for each of its cells in the review card.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const bad = targets.find((pid) => !scopedSet.has(pid));
|
||||
if (bad !== undefined) {
|
||||
toast.error(
|
||||
`Task ${i + 1} ("${d.title}") targets a project outside this ` +
|
||||
"MegaTask's selected repos. Pick it in the review card.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
launchingRef.current = true;
|
||||
|
||||
@@ -7,11 +7,15 @@ import type { Team, TaskType, TaskNature, Complexity } from "@/types";
|
||||
// the structured draft the agent proposes and the human confirms.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** One cell's slice of the work — the per-cell breakdown of The Work. */
|
||||
/** One cell's slice of the work — the per-cell breakdown of The Work.
|
||||
* `project_id` is the per-cell repo for a multi-cell MegaTask draft (a RoboCo
|
||||
* project is per-cell; a monorepo is N per-cell projects sharing one git_url).
|
||||
* A single-cell draft keeps its project at the DraftProposal top level. */
|
||||
export interface CellWork {
|
||||
team: Team;
|
||||
summary: string;
|
||||
items: string[];
|
||||
project_id?: string | null;
|
||||
}
|
||||
|
||||
/** A structured task draft, mirroring the backend PrompterDraftTask. */
|
||||
|
||||
Reference in New Issue
Block a user