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",
|
"lint": "eslint",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "vitest run --coverage",
|
"test": "vitest run --coverage",
|
||||||
"test:watch": "vitest"
|
"test:watch": "vitest",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
@@ -66,6 +68,7 @@
|
|||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.1.1",
|
"eslint-config-next": "16.1.1",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
|
"prettier": "^3.8.5",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
|
|||||||
Generated
+13
-3
@@ -109,7 +109,7 @@ importers:
|
|||||||
version: 10.1.0(@types/react@19.2.8)(react@19.2.3)
|
version: 10.1.0(@types/react@19.2.8)(react@19.2.3)
|
||||||
recharts:
|
recharts:
|
||||||
specifier: ^3.8.1
|
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:
|
remark-gfm:
|
||||||
specifier: ^4.0.1
|
specifier: ^4.0.1
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
@@ -162,6 +162,9 @@ importers:
|
|||||||
jsdom:
|
jsdom:
|
||||||
specifier: ^29.1.1
|
specifier: ^29.1.1
|
||||||
version: 29.1.1
|
version: 29.1.1
|
||||||
|
prettier:
|
||||||
|
specifier: ^3.8.5
|
||||||
|
version: 3.8.5
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4
|
specifier: ^4
|
||||||
version: 4.1.18
|
version: 4.1.18
|
||||||
@@ -3230,6 +3233,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
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:
|
pretty-format@27.5.1:
|
||||||
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
||||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||||
@@ -7228,6 +7236,8 @@ snapshots:
|
|||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
|
prettier@3.8.5: {}
|
||||||
|
|
||||||
pretty-format@27.5.1:
|
pretty-format@27.5.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex: 5.0.1
|
ansi-regex: 5.0.1
|
||||||
@@ -7317,7 +7327,7 @@ snapshots:
|
|||||||
|
|
||||||
react@19.2.3: {}
|
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:
|
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)
|
'@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
|
clsx: 2.1.1
|
||||||
@@ -7327,7 +7337,7 @@ snapshots:
|
|||||||
immer: 10.2.0
|
immer: 10.2.0
|
||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
react-dom: 19.2.3(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)
|
react-redux: 9.3.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1)
|
||||||
reselect: 5.1.1
|
reselect: 5.1.1
|
||||||
tiny-invariant: 1.3.3
|
tiny-invariant: 1.3.3
|
||||||
|
|||||||
@@ -20,15 +20,50 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { useProjects } from "@/hooks/use-projects";
|
import { useProjects } from "@/hooks/use-projects";
|
||||||
import type { BatchProposal, StartRoute } from "@/hooks/use-prompter";
|
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 {
|
interface BatchReviewCardProps {
|
||||||
batch: BatchProposal;
|
batch: BatchProposal;
|
||||||
/** The conflict-free waves (lists of draft indices), once previewed. */
|
/** The conflict-free waves (lists of draft indices), once previewed. */
|
||||||
waves: number[][] | null;
|
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[];
|
projectIds: string[];
|
||||||
onKeepChatting: () => void;
|
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;
|
onConfirm: (route: StartRoute) => void;
|
||||||
/** A launch is in flight — disable the actions so a double-click can't dupe. */
|
/** A launch is in flight — disable the actions so a double-click can't dupe. */
|
||||||
isLaunching?: boolean;
|
isLaunching?: boolean;
|
||||||
@@ -36,9 +71,11 @@ interface BatchReviewCardProps {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The MegaTask review card: every task the agent proposed in one batch, each
|
* The MegaTask review card: every task the agent proposed in one batch, each
|
||||||
* with its target project (editable) and collision surface, plus the
|
* with its per-cell target projects (editable) and collision surface, plus the
|
||||||
* conflict-free wave plan. The human reviews the whole batch and the sequencing,
|
* conflict-free wave plan. A multi-cell task (be+fe, fe+uxui) shows one project
|
||||||
* fixes any task in the wrong repo, then picks one start path for all of them.
|
* 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({
|
export function BatchReviewCard({
|
||||||
batch,
|
batch,
|
||||||
@@ -51,14 +88,21 @@ export function BatchReviewCard({
|
|||||||
}: BatchReviewCardProps) {
|
}: BatchReviewCardProps) {
|
||||||
const { data: allProjects = [] } = useProjects();
|
const { data: allProjects = [] } = useProjects();
|
||||||
// Only the scoped repos are valid targets (the agent read only those).
|
// 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 scoped = new Set(projectIds);
|
||||||
const titleOf = (i: number): string =>
|
const titleOf = (i: number): string =>
|
||||||
batch.drafts[i]?.title ?? `Task ${i + 1}`;
|
batch.drafts[i]?.title ?? `Task ${i + 1}`;
|
||||||
// A task is mis-targeted unless its project is one of the scoped repos.
|
// A task is mis-targeted when any of its cells lacks a scoped project (a
|
||||||
const missingProject = batch.drafts.some(
|
// multi-cell draft checks every the_work entry; a legacy single-cell draft
|
||||||
(d) => !d.project_id || !scoped.has(d.project_id),
|
// 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 (
|
return (
|
||||||
<Card className="border-primary/40 bg-primary/5">
|
<Card className="border-primary/40 bg-primary/5">
|
||||||
@@ -79,67 +123,121 @@ export function BatchReviewCard({
|
|||||||
|
|
||||||
<CardContent className="space-y-3 pb-3">
|
<CardContent className="space-y-3 pb-3">
|
||||||
<ol className="space-y-2">
|
<ol className="space-y-2">
|
||||||
{batch.drafts.map((draft, i) => (
|
{batch.drafts.map((draft, i) => {
|
||||||
<li
|
const entries = cellEntries(draft);
|
||||||
key={i}
|
return (
|
||||||
className="rounded-md border bg-background/60 px-3 py-2 text-sm"
|
<li
|
||||||
>
|
key={i}
|
||||||
<div className="flex items-start justify-between gap-2">
|
className="rounded-md border bg-background/60 px-3 py-2 text-sm"
|
||||||
<span className="font-medium leading-tight">
|
>
|
||||||
{i + 1}. {draft.title}
|
<div className="flex items-start justify-between gap-2">
|
||||||
</span>
|
<span className="font-medium leading-tight">
|
||||||
<div className="flex shrink-0 items-center gap-1">
|
{i + 1}. {draft.title}
|
||||||
{draft.adds_migration && (
|
</span>
|
||||||
<Badge variant="outline" className="gap-1 text-xs">
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
<Database className="h-3 w-3" />
|
{draft.adds_migration && (
|
||||||
migration
|
<Badge variant="outline" className="gap-1 text-xs">
|
||||||
</Badge>
|
<Database className="h-3 w-3" />
|
||||||
)}
|
migration
|
||||||
{draft.touches_shared && (
|
</Badge>
|
||||||
<Badge variant="outline" className="gap-1 text-xs">
|
)}
|
||||||
<Share2 className="h-3 w-3" />
|
{draft.touches_shared && (
|
||||||
shared
|
<Badge variant="outline" className="gap-1 text-xs">
|
||||||
</Badge>
|
<Share2 className="h-3 w-3" />
|
||||||
)}
|
shared
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{(draft.objective || draft.description) && (
|
||||||
{(draft.objective || draft.description) && (
|
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
||||||
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
{draft.objective || draft.description}
|
||||||
{draft.objective || draft.description}
|
</p>
|
||||||
</p>
|
)}
|
||||||
)}
|
{entries.length > 0 ? (
|
||||||
{/* Per-task project — editable so a misfiled task can be fixed */}
|
/* Per-cell project picker — one Select per the_work entry,
|
||||||
<div className="mt-1.5 flex items-center gap-2">
|
scoped to that cell's repos (a project is per-cell). */
|
||||||
<span className="text-xs text-muted-foreground">Project</span>
|
<div className="mt-1.5 space-y-1">
|
||||||
<Select
|
{entries.map(({ entry, entryIndex, team }) => {
|
||||||
value={
|
const cellProjects = allProjects.filter(
|
||||||
draft.project_id && scoped.has(draft.project_id)
|
(p) => scoped.has(p.id) && p.assigned_cell === team,
|
||||||
? draft.project_id
|
);
|
||||||
: ""
|
const pid = entry.project_id ?? "";
|
||||||
}
|
const ok = pid !== "" && scoped.has(pid);
|
||||||
onValueChange={(v) => onProjectChange(i, v)}
|
return (
|
||||||
disabled={isLaunching}
|
<div
|
||||||
>
|
key={entryIndex}
|
||||||
<SelectTrigger
|
className="flex items-center gap-2"
|
||||||
className={`h-7 flex-1 text-xs ${
|
>
|
||||||
draft.project_id && scoped.has(draft.project_id)
|
<span className="w-16 shrink-0 text-xs text-muted-foreground">
|
||||||
? ""
|
{CELL_LABEL[team] ?? team}
|
||||||
: "border-destructive"
|
</span>
|
||||||
}`}
|
<Select
|
||||||
>
|
value={ok ? pid : ""}
|
||||||
<SelectValue placeholder="Pick a project…" />
|
onValueChange={(v) =>
|
||||||
</SelectTrigger>
|
onProjectChange(i, entryIndex, v)
|
||||||
<SelectContent>
|
}
|
||||||
{projects.map((p) => (
|
disabled={isLaunching}
|
||||||
<SelectItem key={p.id} value={p.id}>
|
>
|
||||||
{p.name}
|
<SelectTrigger
|
||||||
</SelectItem>
|
className={`h-7 flex-1 text-xs ${
|
||||||
))}
|
ok ? "" : "border-destructive"
|
||||||
</SelectContent>
|
}`}
|
||||||
</Select>
|
>
|
||||||
</div>
|
<SelectValue placeholder="Pick a project…" />
|
||||||
</li>
|
</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>
|
</ol>
|
||||||
|
|
||||||
{/* Wave plan — how the batch will be sequenced */}
|
{/* Wave plan — how the batch will be sequenced */}
|
||||||
@@ -161,7 +259,8 @@ export function BatchReviewCard({
|
|||||||
|
|
||||||
{missingProject && (
|
{missingProject && (
|
||||||
<p className="text-xs text-destructive">
|
<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>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,26 @@ export interface BatchProposal {
|
|||||||
/** Which start button the human pressed on the draft card. */
|
/** Which start button the human pressed on the draft card. */
|
||||||
export type StartRoute = "board" | "main_pm";
|
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 {
|
export interface EditableDraft {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -784,19 +804,25 @@ export function usePrompter() {
|
|||||||
// Confirm a MegaTask — create the umbrella + sequenced root-subtasks, reap
|
// Confirm a MegaTask — create the umbrella + sequenced root-subtasks, reap
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
/** Reassign one task in the proposed MegaTask to a different project. Lets the
|
/** Reassign one cell of one task in the proposed MegaTask to a different
|
||||||
* human fix a draft the agent put in the wrong (or no) repo before launch.
|
* 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),
|
* Project does not affect the wave plan (waves derive from collision surface),
|
||||||
* so the previewed waves stay valid. */
|
* so the previewed waves stay valid. */
|
||||||
const updateBatchDraftProject = useCallback(
|
const updateBatchDraftProject = useCallback(
|
||||||
(index: number, projectId: string) => {
|
(index: number, entryIndex: number, projectId: string) => {
|
||||||
setBatch((prev) => {
|
setBatch((prev) => {
|
||||||
if (!prev) return prev;
|
if (!prev) return prev;
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
drafts: prev.drafts.map((d, i) =>
|
drafts: prev.drafts.map((d, i) => {
|
||||||
i === index ? { ...d, project_id: projectId } : d,
|
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;
|
return;
|
||||||
}
|
}
|
||||||
// Every task must target one of the scoped repos (the agent assigns it,
|
// Every cell of every task must target one of the scoped repos (the agent
|
||||||
// the human can fix it). The backend re-asserts this authoritatively.
|
// 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 scoped = scopeRef.current.projectIds;
|
||||||
const offender = batch.drafts.findIndex(
|
const scopedSet = new Set(scoped);
|
||||||
(d) => !d.project_id || !scoped.includes(d.project_id),
|
for (let i = 0; i < batch.drafts.length; i++) {
|
||||||
);
|
const d = batch.drafts[i];
|
||||||
if (offender !== -1) {
|
const pids = draftCellProjectIds(d);
|
||||||
toast.error(
|
const targets =
|
||||||
`Task ${offender + 1} ("${batch.drafts[offender].title}") needs one of ` +
|
pids.length > 0 ? pids : d.project_id ? [d.project_id] : [];
|
||||||
"this MegaTask's selected projects. Pick it in the review card.",
|
if (targets.length === 0) {
|
||||||
);
|
toast.error(
|
||||||
return;
|
`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;
|
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.
|
// 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 {
|
export interface CellWork {
|
||||||
team: Team;
|
team: Team;
|
||||||
summary: string;
|
summary: string;
|
||||||
items: string[];
|
items: string[];
|
||||||
|
project_id?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A structured task draft, mirroring the backend PrompterDraftTask. */
|
/** A structured task draft, mirroring the backend PrompterDraftTask. */
|
||||||
|
|||||||
Reference in New Issue
Block a user