mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
chore(panel): one disclosure primitive, DialogFooter everywhere, three dialog widths (Wave C-2) (#695)
* feat(panel): promote project settings to a full page The edit-project dialog carried ~30 fields across 7 concerns in one flat scroll with a per-tab width swap — outgrown. Project settings now live at /projects/[id]/settings as a card-per-concern grid (the settings page's own pattern) with per-card save and Conventions as a page-level tab at natural width; the list Edit action routes there, and a slim quick-edit dialog (name/cell/active) replaces the kitchen-sink. * chore(panel): one disclosure primitive, DialogFooter everywhere, three dialog widths collapsible-section moves to ui/ as the single sectioned-disclosure primitive (task dialogs' raw Collapsible and create-project's ad-hoc showAdvanced converge onto it); every hand-rolled dialog footer becomes DialogFooter; dialog widths collapse from ten ad-hoc classes to three named sizes, with deliberate outliers annotated. No behavioral change. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
@@ -67,7 +68,9 @@ export function ResolveWaitDialog({ agentId }: ResolveWaitDialogProps) {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<HelpTip label="Delivered to the agent verbatim as the answer to what it's waiting on">
|
<HelpTip label="Delivered to the agent verbatim as the answer to what it's waiting on">
|
||||||
<Label htmlFor="resolution" className="w-fit">Resolution Message</Label>
|
<Label htmlFor="resolution" className="w-fit">
|
||||||
|
Resolution Message
|
||||||
|
</Label>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="resolution"
|
id="resolution"
|
||||||
@@ -77,7 +80,7 @@ export function ResolveWaitDialog({ agentId }: ResolveWaitDialogProps) {
|
|||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-2">
|
<DialogFooter>
|
||||||
<HelpTip label="Closes without sending anything to the agent">
|
<HelpTip label="Closes without sending anything to the agent">
|
||||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -85,12 +88,15 @@ export function ResolveWaitDialog({ agentId }: ResolveWaitDialogProps) {
|
|||||||
</HelpTip>
|
</HelpTip>
|
||||||
<HelpTip label="Delivers the message above to the agent so it can resume">
|
<HelpTip label="Delivers the message above to the agent so it can resume">
|
||||||
<span>
|
<span>
|
||||||
<Button onClick={handleResolve} disabled={resolveWait.isPending}>
|
<Button
|
||||||
|
onClick={handleResolve}
|
||||||
|
disabled={resolveWait.isPending}
|
||||||
|
>
|
||||||
{resolveWait.isPending ? "Sending..." : "Send Resolution"}
|
{resolveWait.isPending ? "Sending..." : "Send Resolution"}
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
</div>
|
</DialogFooter>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
@@ -124,7 +125,7 @@ export function SpawnAgentDialog({
|
|||||||
placeholder="Initial instructions for the agent"
|
placeholder="Initial instructions for the agent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-2">
|
<DialogFooter>
|
||||||
<HelpTip label="Closes without spawning">
|
<HelpTip label="Closes without spawning">
|
||||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -137,7 +138,7 @@ export function SpawnAgentDialog({
|
|||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
</div>
|
</DialogFooter>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ import { AgentSelector } from "@/components/agents/agent-selector";
|
|||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
|
DIALOG_SIZES,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -96,7 +98,7 @@ export function CreateFlagDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-lg">
|
<DialogContent className={DIALOG_SIZES.md}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create Quality Flag</DialogTitle>
|
<DialogTitle>Create Quality Flag</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -194,7 +196,7 @@ export function CreateFlagDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-4">
|
<DialogFooter className="pt-4">
|
||||||
<HelpTip label="Discards this draft and closes the dialog">
|
<HelpTip label="Discards this draft and closes the dialog">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -214,7 +216,7 @@ export function CreateFlagDialog({
|
|||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
</div>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
|
DIALOG_SIZES,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -121,12 +122,12 @@ function QuickActionsCustomizeDialog() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
<DialogContent className="max-w-md">
|
<DialogContent className={DIALOG_SIZES.sm}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Customize Quick Actions</DialogTitle>
|
<DialogTitle>Customize Quick Actions</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Pick which shortcuts show on the Overview dashboard and reorder
|
Pick which shortcuts show on the Overview dashboard and reorder them
|
||||||
them with the arrows. Saved to this browser only.
|
with the arrows. Saved to this browser only.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
|
DIALOG_SIZES,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@@ -306,7 +307,7 @@ export function GitActionsPanel({
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
</span>
|
</span>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
<DialogContent className="max-w-lg">
|
<DialogContent className={DIALOG_SIZES.md}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create Pull Request</DialogTitle>
|
<DialogTitle>Create Pull Request</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -378,7 +379,7 @@ export function GitActionsPanel({
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
</span>
|
</span>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
<DialogContent className="max-w-sm">
|
<DialogContent className={DIALOG_SIZES.sm}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Merge Pull Request</DialogTitle>
|
<DialogTitle>Merge Pull Request</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ export function CommandPalette() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
{/* Outlier: kept explicit — this isn't just a width, it's a full custom
|
||||||
|
position/spacing override (top-anchored, no gap, no padding) that a
|
||||||
|
DIALOG_SIZES bucket can't express. */}
|
||||||
<DialogContent
|
<DialogContent
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
className="top-[20%] max-w-xl translate-y-0 gap-0 overflow-hidden p-0"
|
className="top-[20%] max-w-xl translate-y-0 gap-0 overflow-hidden p-0"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
|
DIALOG_SIZES,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -113,7 +114,9 @@ export function CreateProductDialog() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
<DialogContent className="sm:max-w-[525px] max-h-[90vh] overflow-y-auto">
|
<DialogContent
|
||||||
|
className={`${DIALOG_SIZES.md} max-h-[90vh] overflow-y-auto`}
|
||||||
|
>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create Product</DialogTitle>
|
<DialogTitle>Create Product</DialogTitle>
|
||||||
@@ -172,7 +175,9 @@ export function CreateProductDialog() {
|
|||||||
</HelpTip>
|
</HelpTip>
|
||||||
{cells.map((cell) => (
|
{cells.map((cell) => (
|
||||||
<div key={cell.value} className="grid gap-2">
|
<div key={cell.value} className="grid gap-2">
|
||||||
<HelpTip label={`Which project the ${cell.label} cell works on for this product.`}>
|
<HelpTip
|
||||||
|
label={`Which project the ${cell.label} cell works on for this product.`}
|
||||||
|
>
|
||||||
<Label
|
<Label
|
||||||
htmlFor={`cell-${cell.value}`}
|
htmlFor={`cell-${cell.value}`}
|
||||||
className="text-sm text-muted-foreground"
|
className="text-sm text-muted-foreground"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
DIALOG_SIZES,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -165,7 +166,9 @@ function EditProductForm({
|
|||||||
</HelpTip>
|
</HelpTip>
|
||||||
{cells.map((cell) => (
|
{cells.map((cell) => (
|
||||||
<div key={cell.value} className="grid gap-2">
|
<div key={cell.value} className="grid gap-2">
|
||||||
<HelpTip label={`Which project the ${cell.label} cell works on for this product.`}>
|
<HelpTip
|
||||||
|
label={`Which project the ${cell.label} cell works on for this product.`}
|
||||||
|
>
|
||||||
<Label
|
<Label
|
||||||
htmlFor={`cell-${cell.value}`}
|
htmlFor={`cell-${cell.value}`}
|
||||||
className="text-sm text-muted-foreground"
|
className="text-sm text-muted-foreground"
|
||||||
@@ -216,7 +219,9 @@ export function EditProductDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-[525px] max-h-[90vh] overflow-y-auto">
|
<DialogContent
|
||||||
|
className={`${DIALOG_SIZES.md} max-h-[90vh] overflow-y-auto`}
|
||||||
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<Skeleton className="h-8 w-48" />
|
<Skeleton className="h-8 w-48" />
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
|
DIALOG_SIZES,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -31,6 +32,7 @@ import {
|
|||||||
type SelectedRepo,
|
type SelectedRepo,
|
||||||
} from "@/components/projects/select-repo-picker";
|
} from "@/components/projects/select-repo-picker";
|
||||||
import { HelpTip } from "@/components/ui/help-tip";
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
import { CollapsibleSection } from "@/components/ui/collapsible-section";
|
||||||
|
|
||||||
const cells: { value: Team; label: string }[] = [
|
const cells: { value: Team; label: string }[] = [
|
||||||
{ value: Team.BACKEND, label: "Backend" },
|
{ value: Team.BACKEND, label: "Backend" },
|
||||||
@@ -152,7 +154,7 @@ export function CreateProjectDialog() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
<DialogContent className="sm:max-w-[525px]">
|
<DialogContent className={DIALOG_SIZES.md}>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create Project</DialogTitle>
|
<DialogTitle>Create Project</DialogTitle>
|
||||||
@@ -315,151 +317,145 @@ export function CreateProjectDialog() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Advanced Options Toggle */}
|
{/* Advanced Options */}
|
||||||
<HelpTip label="Test/Format/Build are reference-only today; Lint + Typecheck (or Quality Gate below, which replaces both) run automatically at the dev's pre-submit gate.">
|
<CollapsibleSection
|
||||||
<Button
|
variant="button"
|
||||||
type="button"
|
open={showAdvanced}
|
||||||
variant="ghost"
|
onOpenChange={setShowAdvanced}
|
||||||
className="justify-start px-0 text-muted-foreground"
|
title={
|
||||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
<HelpTip label="Test/Format/Build are reference-only today; Lint + Typecheck (or Quality Gate below, which replaces both) run automatically at the dev's pre-submit gate.">
|
||||||
>
|
<span>CI/CD Commands</span>
|
||||||
{showAdvanced ? "Hide" : "Show"} CI/CD Commands
|
</HelpTip>
|
||||||
</Button>
|
}
|
||||||
</HelpTip>
|
>
|
||||||
|
{/* CI/CD Commands */}
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<HelpTip label="Reference only — not yet wired into any automated gate or CI run by RoboCo itself.">
|
||||||
|
<Label htmlFor="test_command">Test Command</Label>
|
||||||
|
</HelpTip>
|
||||||
|
<Input
|
||||||
|
id="test_command"
|
||||||
|
value={formData.test_command || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, test_command: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="uv run pytest"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{showAdvanced && (
|
<div className="grid gap-2">
|
||||||
<>
|
<HelpTip label="Runs at the dev's pre-submit gate (i_am_done) alongside Typecheck — unless Quality Gate Command below is set, which replaces both.">
|
||||||
{/* CI/CD Commands */}
|
<Label htmlFor="lint_command">Lint Command</Label>
|
||||||
<div className="grid gap-2">
|
</HelpTip>
|
||||||
<HelpTip label="Reference only — not yet wired into any automated gate or CI run by RoboCo itself.">
|
<Input
|
||||||
<Label htmlFor="test_command">Test Command</Label>
|
id="lint_command"
|
||||||
</HelpTip>
|
value={formData.lint_command || ""}
|
||||||
<Input
|
onChange={(e) =>
|
||||||
id="test_command"
|
setFormData({ ...formData, lint_command: e.target.value })
|
||||||
value={formData.test_command || ""}
|
}
|
||||||
onChange={(e) =>
|
placeholder="uv run ruff check ."
|
||||||
setFormData({ ...formData, test_command: e.target.value })
|
/>
|
||||||
}
|
</div>
|
||||||
placeholder="uv run pytest"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<HelpTip label="Runs at the dev's pre-submit gate (i_am_done) alongside Typecheck — unless Quality Gate Command below is set, which replaces both.">
|
<HelpTip label="Reference only — deliberately excluded from the automated gate since formatting mutates files.">
|
||||||
<Label htmlFor="lint_command">Lint Command</Label>
|
<Label htmlFor="format_command">Format Command</Label>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
<Input
|
<Input
|
||||||
id="lint_command"
|
id="format_command"
|
||||||
value={formData.lint_command || ""}
|
value={formData.format_command || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, lint_command: e.target.value })
|
setFormData({
|
||||||
}
|
...formData,
|
||||||
placeholder="uv run ruff check ."
|
format_command: e.target.value,
|
||||||
/>
|
})
|
||||||
</div>
|
}
|
||||||
|
placeholder="uv run ruff format ."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<HelpTip label="Reference only — deliberately excluded from the automated gate since formatting mutates files.">
|
<HelpTip label="Runs at the dev's pre-submit gate (i_am_done) alongside Lint — unless Quality Gate Command below is set, which replaces both.">
|
||||||
<Label htmlFor="format_command">Format Command</Label>
|
<Label htmlFor="typecheck_command">Typecheck Command</Label>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
<Input
|
<Input
|
||||||
id="format_command"
|
id="typecheck_command"
|
||||||
value={formData.format_command || ""}
|
value={formData.typecheck_command || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
format_command: e.target.value,
|
typecheck_command: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
placeholder="uv run ruff format ."
|
placeholder="uv run mypy src/"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<HelpTip label="Runs at the dev's pre-submit gate (i_am_done) alongside Lint — unless Quality Gate Command below is set, which replaces both.">
|
<HelpTip label="Reference only — not run automatically; the slow build/test suite is left to CI.">
|
||||||
<Label htmlFor="typecheck_command">Typecheck Command</Label>
|
<Label htmlFor="build_command">Build Command</Label>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
<Input
|
<Input
|
||||||
id="typecheck_command"
|
id="build_command"
|
||||||
value={formData.typecheck_command || ""}
|
value={formData.build_command || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
typecheck_command: e.target.value,
|
build_command: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
placeholder="uv run mypy src/"
|
placeholder="pnpm build"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<HelpTip label="Reference only — not run automatically; the slow build/test suite is left to CI.">
|
|
||||||
<Label htmlFor="build_command">Build Command</Label>
|
|
||||||
</HelpTip>
|
|
||||||
<Input
|
|
||||||
id="build_command"
|
|
||||||
value={formData.build_command || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
build_command: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder="pnpm build"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<HelpTip label="When set, replaces the Lint + Typecheck pair as the dev's complete pre-submit gate command.">
|
|
||||||
<Label htmlFor="quality_command">
|
|
||||||
Quality Gate Command
|
|
||||||
</Label>
|
|
||||||
</HelpTip>
|
|
||||||
<Input
|
|
||||||
id="quality_command"
|
|
||||||
value={formData.quality_command || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
quality_command: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder="make gate"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Fast pre-submit gate (lint + types + complexity, no tests)
|
|
||||||
run in the dev's workspace at hand-off to QA.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<HelpTip label="Command that regenerates checked-in generated files, e.g. `make codegen`; run and committed before push so codegen drift never fails CI. Leave blank if the project has no generated artifacts.">
|
|
||||||
<Label htmlFor="codegen_command">Codegen Command</Label>
|
|
||||||
</HelpTip>
|
|
||||||
<Input
|
|
||||||
id="codegen_command"
|
|
||||||
value={formData.codegen_command || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
codegen_command: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder="make codegen"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Regenerates checked-in generated artifacts; any drift is
|
|
||||||
committed in the task's workspace before each push.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<HelpTip label="When set, replaces the Lint + Typecheck pair as the dev's complete pre-submit gate command.">
|
||||||
|
<Label htmlFor="quality_command">Quality Gate Command</Label>
|
||||||
|
</HelpTip>
|
||||||
|
<Input
|
||||||
|
id="quality_command"
|
||||||
|
value={formData.quality_command || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
quality_command: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="make gate"
|
||||||
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Autonomous maintenance (CI-watch, video engine,
|
Fast pre-submit gate (lint + types + complexity, no tests) run
|
||||||
dependency-update bot, sandbox DB/Redis/Mongo) is configured
|
in the dev's workspace at hand-off to QA.
|
||||||
after creation, from this project's Edit Project dialog.
|
|
||||||
</p>
|
</p>
|
||||||
</>
|
</div>
|
||||||
)}
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<HelpTip label="Command that regenerates checked-in generated files, e.g. `make codegen`; run and committed before push so codegen drift never fails CI. Leave blank if the project has no generated artifacts.">
|
||||||
|
<Label htmlFor="codegen_command">Codegen Command</Label>
|
||||||
|
</HelpTip>
|
||||||
|
<Input
|
||||||
|
id="codegen_command"
|
||||||
|
value={formData.codegen_command || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
codegen_command: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="make codegen"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Regenerates checked-in generated artifacts; any drift is
|
||||||
|
committed in the task's workspace before each push.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Autonomous maintenance (CI-watch, video engine,
|
||||||
|
dependency-update bot, sandbox DB/Redis/Mongo) is configured
|
||||||
|
after creation, from this project's Edit Project dialog.
|
||||||
|
</p>
|
||||||
|
</CollapsibleSection>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
DIALOG_SIZES,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -174,7 +175,7 @@ export function QuickEditProjectDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className={DIALOG_SIZES.sm}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<Skeleton className="h-8 w-48" />
|
<Skeleton className="h-8 w-48" />
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
DIALOG_SIZES,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
@@ -124,7 +125,7 @@ export function SelectRepoDialog({ onSelect }: SelectRepoDialogProps) {
|
|||||||
</span>
|
</span>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
|
|
||||||
<DialogContent className="sm:max-w-[480px]">
|
<DialogContent className={DIALOG_SIZES.md}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Select a repository</DialogTitle>
|
<DialogTitle>Select a repository</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
@@ -22,12 +23,8 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
import { CollapsibleSection } from "@/components/ui/collapsible-section";
|
||||||
Collapsible,
|
import { Plus, GitBranch } from "lucide-react";
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible";
|
|
||||||
import { Plus, ChevronDown, ChevronRight, GitBranch } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AcceptanceCriteriaEditor } from "./acceptance-criteria-editor";
|
import { AcceptanceCriteriaEditor } from "./acceptance-criteria-editor";
|
||||||
import { MarkdownEditor } from "./markdown-editor";
|
import { MarkdownEditor } from "./markdown-editor";
|
||||||
@@ -72,7 +69,8 @@ const TASK_TYPE_OPTIONS = [
|
|||||||
// (branch, commits, PR) — this only classifies the kind of artifact.
|
// (branch, commits, PR) — this only classifies the kind of artifact.
|
||||||
const TASK_TYPE_DESCRIPTIONS: Record<TaskType, string> = {
|
const TASK_TYPE_DESCRIPTIONS: Record<TaskType, string> = {
|
||||||
[TaskType.CODE]: "Source code changes. Follows the full git workflow.",
|
[TaskType.CODE]: "Source code changes. Follows the full git workflow.",
|
||||||
[TaskType.DOCUMENTATION]: "Documentation updates. Follows the full git workflow.",
|
[TaskType.DOCUMENTATION]:
|
||||||
|
"Documentation updates. Follows the full git workflow.",
|
||||||
[TaskType.RESEARCH]:
|
[TaskType.RESEARCH]:
|
||||||
"Research findings, committed as notes. Follows the full git workflow.",
|
"Research findings, committed as notes. Follows the full git workflow.",
|
||||||
[TaskType.PLANNING]:
|
[TaskType.PLANNING]:
|
||||||
@@ -157,7 +155,8 @@ export function CreateTaskDialog() {
|
|||||||
const trimmedSequence = sequence.trim();
|
const trimmedSequence = sequence.trim();
|
||||||
if (
|
if (
|
||||||
trimmedSequence &&
|
trimmedSequence &&
|
||||||
(!Number.isInteger(Number(trimmedSequence)) || Number(trimmedSequence) < 0)
|
(!Number.isInteger(Number(trimmedSequence)) ||
|
||||||
|
Number(trimmedSequence) < 0)
|
||||||
) {
|
) {
|
||||||
newErrors.sequence = "Sequence must be a non-negative whole number";
|
newErrors.sequence = "Sequence must be a non-negative whole number";
|
||||||
}
|
}
|
||||||
@@ -235,6 +234,9 @@ export function CreateTaskDialog() {
|
|||||||
New Task
|
New Task
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
{/* Outlier: kept explicit — the lg:grid-cols-5 team/priority/complexity/
|
||||||
|
status/nature row needs real width; DIALOG_SIZES.lg (max-w-2xl)
|
||||||
|
would cramp it into a single column. */}
|
||||||
<DialogContent className="w-full max-w-[95vw] sm:max-w-xl md:max-w-3xl lg:max-w-5xl max-h-[90vh] overflow-y-auto">
|
<DialogContent className="w-full max-w-[95vw] sm:max-w-xl md:max-w-3xl lg:max-w-5xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create New Task</DialogTitle>
|
<DialogTitle>Create New Task</DialogTitle>
|
||||||
@@ -390,169 +392,158 @@ export function CreateTaskDialog() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Advanced Options */}
|
{/* Advanced Options */}
|
||||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
<CollapsibleSection
|
||||||
<CollapsibleTrigger asChild>
|
variant="button"
|
||||||
<Button
|
open={advancedOpen}
|
||||||
variant="ghost"
|
onOpenChange={setAdvancedOpen}
|
||||||
type="button"
|
title={
|
||||||
className="w-full justify-between"
|
<HelpTip label="Parent task, agent assignment, task type, and repo/product routing.">
|
||||||
>
|
<span>Advanced Options</span>
|
||||||
<HelpTip label="Parent task, agent assignment, task type, and repo/product routing.">
|
</HelpTip>
|
||||||
<span>Advanced Options</span>
|
}
|
||||||
|
>
|
||||||
|
{/* Parent Task */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<HelpTip label="Nests this task under another — its claim order is then gated by sequence relative to siblings under the same parent.">
|
||||||
|
<Label>Parent Task (for subtasks)</Label>
|
||||||
|
</HelpTip>
|
||||||
|
<TaskSelector
|
||||||
|
value={parentTaskId}
|
||||||
|
onChange={setParentTaskId}
|
||||||
|
placeholder="Select parent task (optional)..."
|
||||||
|
filterByTeam={team}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Make this task a subtask of an existing task
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sequence */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<HelpTip label="Order within siblings under the same parent — lower runs first, ties run in parallel. Leave blank to default to 0.">
|
||||||
|
<Label htmlFor="sequence">Sequence</Label>
|
||||||
|
</HelpTip>
|
||||||
|
<Input
|
||||||
|
id="sequence"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
placeholder="0"
|
||||||
|
value={sequence}
|
||||||
|
onChange={(e) => setSequence(e.target.value)}
|
||||||
|
className={errors.sequence ? "border-destructive" : ""}
|
||||||
|
/>
|
||||||
|
{errors.sequence && (
|
||||||
|
<p className="text-xs text-destructive">{errors.sequence}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Assign To */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<HelpTip label="Pin a specific agent; leave unassigned to let the orchestrator route by role, team, and availability.">
|
||||||
|
<Label>Assign To</Label>
|
||||||
|
</HelpTip>
|
||||||
|
<AgentSelector
|
||||||
|
value={assignedTo}
|
||||||
|
onChange={setAssignedTo}
|
||||||
|
placeholder="Unassigned (orchestrator will route)"
|
||||||
|
filterByTeam={team}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Leave unassigned to let the orchestrator route automatically, or
|
||||||
|
manually assign to a specific agent
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Git Configuration Section */}
|
||||||
|
<div className="space-y-4 pt-4 border-t">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<HelpTip label="Every task type follows the same branch → commits → PR workflow; these fields just route it.">
|
||||||
|
<span className="font-medium text-sm">
|
||||||
|
Git & Work Configuration
|
||||||
|
</span>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
{advancedOpen ? (
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent className="space-y-4 pt-4">
|
|
||||||
{/* Parent Task */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<HelpTip label="Nests this task under another — its claim order is then gated by sequence relative to siblings under the same parent.">
|
|
||||||
<Label>Parent Task (for subtasks)</Label>
|
|
||||||
</HelpTip>
|
|
||||||
<TaskSelector
|
|
||||||
value={parentTaskId}
|
|
||||||
onChange={setParentTaskId}
|
|
||||||
placeholder="Select parent task (optional)..."
|
|
||||||
filterByTeam={team}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Make this task a subtask of an existing task
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sequence */}
|
{/* Task Type */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<HelpTip label="Order within siblings under the same parent — lower runs first, ties run in parallel. Leave blank to default to 0.">
|
<HelpTip label={TASK_TYPE_DESCRIPTIONS[taskType]}>
|
||||||
<Label htmlFor="sequence">Sequence</Label>
|
<Label>Task Type</Label>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
<Input
|
<Select
|
||||||
id="sequence"
|
value={taskType}
|
||||||
type="number"
|
onValueChange={(v) => setTaskType(v as TaskType)}
|
||||||
min="0"
|
>
|
||||||
step="1"
|
<SelectTrigger className="w-full">
|
||||||
placeholder="0"
|
<SelectValue />
|
||||||
value={sequence}
|
</SelectTrigger>
|
||||||
onChange={(e) => setSequence(e.target.value)}
|
<SelectContent>
|
||||||
className={errors.sequence ? "border-destructive" : ""}
|
{TASK_TYPE_OPTIONS.map((t) => (
|
||||||
/>
|
<SelectItem key={t.value} value={t.value}>
|
||||||
{errors.sequence && (
|
{t.label}
|
||||||
<p className="text-xs text-destructive">{errors.sequence}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Assign To */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<HelpTip label="Pin a specific agent; leave unassigned to let the orchestrator route by role, team, and availability.">
|
|
||||||
<Label>Assign To</Label>
|
|
||||||
</HelpTip>
|
|
||||||
<AgentSelector
|
|
||||||
value={assignedTo}
|
|
||||||
onChange={setAssignedTo}
|
|
||||||
placeholder="Unassigned (orchestrator will route)"
|
|
||||||
filterByTeam={team}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Leave unassigned to let the orchestrator route automatically,
|
|
||||||
or manually assign to a specific agent
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Git Configuration Section */}
|
|
||||||
<div className="space-y-4 pt-4 border-t">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<HelpTip label="Every task type follows the same branch → commits → PR workflow; these fields just route it.">
|
|
||||||
<span className="font-medium text-sm">
|
|
||||||
Git & Work Configuration
|
|
||||||
</span>
|
|
||||||
</HelpTip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Task Type */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<HelpTip label={TASK_TYPE_DESCRIPTIONS[taskType]}>
|
|
||||||
<Label>Task Type</Label>
|
|
||||||
</HelpTip>
|
|
||||||
<Select
|
|
||||||
value={taskType}
|
|
||||||
onValueChange={(v) => setTaskType(v as TaskType)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{TASK_TYPE_OPTIONS.map((t) => (
|
|
||||||
<SelectItem key={t.value} value={t.value}>
|
|
||||||
{t.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Type of work: code, documentation, research, etc.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Project — required UNLESS a Product is picked (fan-out task) */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<HelpTip label="The repo the branch/PR are opened against once this task is claimed.">
|
|
||||||
<Label>Project</Label>
|
|
||||||
</HelpTip>
|
|
||||||
<ProjectSelector
|
|
||||||
value={projectId || null}
|
|
||||||
onChange={(value) => setProjectId(value || "")}
|
|
||||||
placeholder="Select project..."
|
|
||||||
/>
|
|
||||||
{errors.project_id && (
|
|
||||||
<p className="text-xs text-destructive">
|
|
||||||
{errors.project_id}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
The repo this task targets. Optional if you pick a Product
|
|
||||||
below — a fan-out task routes each cell's subtask via
|
|
||||||
the Product instead.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Product (optional) — drives per-cell project routing of subtasks */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<HelpTip label="Fan-out: each delegated subtask routes to that cell's own mapped project instead of one repo.">
|
|
||||||
<Label>Product</Label>
|
|
||||||
</HelpTip>
|
|
||||||
<Select
|
|
||||||
value={productId || "none"}
|
|
||||||
onValueChange={(v) => setProductId(v === "none" ? "" : v)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue placeholder="None" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">
|
|
||||||
None (single project)
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
{products.map((p) => (
|
))}
|
||||||
<SelectItem key={p.id} value={p.id}>
|
</SelectContent>
|
||||||
{p.name}
|
</Select>
|
||||||
</SelectItem>
|
<p className="text-xs text-muted-foreground">
|
||||||
))}
|
Type of work: code, documentation, research, etc.
|
||||||
</SelectContent>
|
</p>
|
||||||
</Select>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Optional. When set, delegated subtasks route to each
|
|
||||||
cell's mapped project (manage these in Products).
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
{/* Project — required UNLESS a Product is picked (fan-out task) */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<HelpTip label="The repo the branch/PR are opened against once this task is claimed.">
|
||||||
|
<Label>Project</Label>
|
||||||
|
</HelpTip>
|
||||||
|
<ProjectSelector
|
||||||
|
value={projectId || null}
|
||||||
|
onChange={(value) => setProjectId(value || "")}
|
||||||
|
placeholder="Select project..."
|
||||||
|
/>
|
||||||
|
{errors.project_id && (
|
||||||
|
<p className="text-xs text-destructive">
|
||||||
|
{errors.project_id}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
The repo this task targets. Optional if you pick a Product
|
||||||
|
below — a fan-out task routes each cell's subtask via the
|
||||||
|
Product instead.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Product (optional) — drives per-cell project routing of subtasks */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<HelpTip label="Fan-out: each delegated subtask routes to that cell's own mapped project instead of one repo.">
|
||||||
|
<Label>Product</Label>
|
||||||
|
</HelpTip>
|
||||||
|
<Select
|
||||||
|
value={productId || "none"}
|
||||||
|
onValueChange={(v) => setProductId(v === "none" ? "" : v)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="None" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">None (single project)</SelectItem>
|
||||||
|
{products.map((p) => (
|
||||||
|
<SelectItem key={p.id} value={p.id}>
|
||||||
|
{p.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Optional. When set, delegated subtasks route to each
|
||||||
|
cell's mapped project (manage these in Products).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
<DialogFooter className="border-t pt-4">
|
||||||
<HelpTip label="Discards everything entered above without creating a task.">
|
<HelpTip label="Discards everything entered above without creating a task.">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -575,7 +566,7 @@ export function CreateTaskDialog() {
|
|||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
</div>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import { Label } from "@/components/ui/label";
|
|||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
|
DIALOG_SIZES,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -20,12 +22,8 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
import { CollapsibleSection } from "@/components/ui/collapsible-section";
|
||||||
Collapsible,
|
import { GitBranch } from "lucide-react";
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible";
|
|
||||||
import { ChevronDown, ChevronRight, GitBranch } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { MarkdownEditor } from "./markdown-editor";
|
import { MarkdownEditor } from "./markdown-editor";
|
||||||
import { AcceptanceCriteriaEditor } from "./acceptance-criteria-editor";
|
import { AcceptanceCriteriaEditor } from "./acceptance-criteria-editor";
|
||||||
@@ -202,7 +200,9 @@ function EditTaskDialogInner({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={true} onOpenChange={onOpenChange}>
|
<Dialog open={true} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
<DialogContent
|
||||||
|
className={`${DIALOG_SIZES.lg} max-h-[90vh] overflow-y-auto`}
|
||||||
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Edit Task</DialogTitle>
|
<DialogTitle>Edit Task</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -321,149 +321,140 @@ function EditTaskDialogInner({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Advanced Options */}
|
{/* Advanced Options */}
|
||||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
<CollapsibleSection
|
||||||
<CollapsibleTrigger asChild>
|
variant="button"
|
||||||
<Button
|
open={advancedOpen}
|
||||||
variant="ghost"
|
onOpenChange={setAdvancedOpen}
|
||||||
type="button"
|
title={
|
||||||
className="w-full justify-between"
|
<HelpTip label="Agent assignment, target date, task type, and repo routing.">
|
||||||
>
|
<span>Advanced Options</span>
|
||||||
<HelpTip label="Agent assignment, target date, task type, and repo routing.">
|
</HelpTip>
|
||||||
<span>Advanced Options</span>
|
}
|
||||||
</HelpTip>
|
>
|
||||||
{advancedOpen ? (
|
{/* Assigned To */}
|
||||||
<ChevronDown className="h-4 w-4" />
|
<div className="space-y-2">
|
||||||
) : (
|
<HelpTip label="Pin a specific agent; leave unassigned to let the orchestrator route by role, team, and availability.">
|
||||||
<ChevronRight className="h-4 w-4" />
|
<Label>Assigned To</Label>
|
||||||
)}
|
</HelpTip>
|
||||||
</Button>
|
<AgentSelector
|
||||||
</CollapsibleTrigger>
|
value={assignedTo}
|
||||||
<CollapsibleContent className="space-y-4 pt-4">
|
onChange={setAssignedTo}
|
||||||
{/* Assigned To */}
|
placeholder="Unassigned"
|
||||||
<div className="space-y-2">
|
filterByTeam={team}
|
||||||
<HelpTip label="Pin a specific agent; leave unassigned to let the orchestrator route by role, team, and availability.">
|
/>
|
||||||
<Label>Assigned To</Label>
|
</div>
|
||||||
</HelpTip>
|
|
||||||
<AgentSelector
|
|
||||||
value={assignedTo}
|
|
||||||
onChange={setAssignedTo}
|
|
||||||
placeholder="Unassigned"
|
|
||||||
filterByTeam={team}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Target Date */}
|
{/* Target Date */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<HelpTip label="Informational deadline only — nothing in the lifecycle enforces or auto-escalates on it.">
|
<HelpTip label="Informational deadline only — nothing in the lifecycle enforces or auto-escalates on it.">
|
||||||
<Label>Target Date</Label>
|
<Label>Target Date</Label>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
<Input
|
<Input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={targetDate}
|
value={targetDate}
|
||||||
onChange={(e) => setTargetDate(e.target.value)}
|
onChange={(e) => setTargetDate(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sequence */}
|
{/* Sequence */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<HelpTip label="Order within siblings under the same parent — lower runs first. A sibling with a lower sequence must reach a terminal state before this one is claimable; ties run in parallel.">
|
<HelpTip label="Order within siblings under the same parent — lower runs first. A sibling with a lower sequence must reach a terminal state before this one is claimable; ties run in parallel.">
|
||||||
<Label htmlFor="edit-sequence">Sequence</Label>
|
<Label htmlFor="edit-sequence">Sequence</Label>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
<Input
|
<Input
|
||||||
id="edit-sequence"
|
id="edit-sequence"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
step="1"
|
step="1"
|
||||||
value={sequence}
|
value={sequence}
|
||||||
onChange={(e) => setSequence(e.target.value)}
|
onChange={(e) => setSequence(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Budget (USD) */}
|
{/* Budget (USD) */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<HelpTip label="Caps this task's own agent-spawn spend; only enforced when the task-budgets feature flag is on. Empty = no cap.">
|
<HelpTip label="Caps this task's own agent-spawn spend; only enforced when the task-budgets feature flag is on. Empty = no cap.">
|
||||||
<Label>Budget (USD)</Label>
|
<Label>Budget (USD)</Label>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min="0.01"
|
min="0.01"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
placeholder="No cap"
|
placeholder="No cap"
|
||||||
value={budgetUsd}
|
value={budgetUsd}
|
||||||
onChange={(e) => setBudgetUsd(e.target.value)}
|
onChange={(e) => setBudgetUsd(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Must be greater than 0 — a 0 budget would block the task
|
Must be greater than 0 — a 0 budget would block the task before
|
||||||
before it spends a cent. Leave blank for no cap: budgets
|
it spends a cent. Leave blank for no cap: budgets enforce only
|
||||||
enforce only when explicitly set.
|
when explicitly set.
|
||||||
|
</p>
|
||||||
|
{spendUsd != null && (
|
||||||
|
<p
|
||||||
|
className="text-xs text-muted-foreground"
|
||||||
|
data-testid="task-spend"
|
||||||
|
>
|
||||||
|
Spent: ${spendUsd.toFixed(2)}
|
||||||
|
{budgetUsd.trim() && !Number.isNaN(Number(budgetUsd))
|
||||||
|
? ` / $${Number(budgetUsd).toFixed(2)}`
|
||||||
|
: ""}
|
||||||
</p>
|
</p>
|
||||||
{spendUsd != null && (
|
)}
|
||||||
<p
|
</div>
|
||||||
className="text-xs text-muted-foreground"
|
|
||||||
data-testid="task-spend"
|
{/* Git Configuration Section */}
|
||||||
>
|
<div className="space-y-4 pt-4 border-t">
|
||||||
Spent: ${spendUsd.toFixed(2)}
|
<div className="flex items-center gap-2 mb-2">
|
||||||
{budgetUsd.trim() && !Number.isNaN(Number(budgetUsd))
|
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||||
? ` / $${Number(budgetUsd).toFixed(2)}`
|
<span className="font-medium text-sm">
|
||||||
: ""}
|
Git & Work Configuration
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Task Type */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<HelpTip label={TASK_TYPE_DESCRIPTIONS[taskType]}>
|
||||||
|
<Label>Task Type</Label>
|
||||||
|
</HelpTip>
|
||||||
|
<Select
|
||||||
|
value={taskType}
|
||||||
|
onValueChange={(v) => setTaskType(v as TaskType)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{TASK_TYPE_OPTIONS.map((t) => (
|
||||||
|
<SelectItem key={t.value} value={t.value}>
|
||||||
|
{t.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Project Selector (required - all tasks follow git workflow) */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<HelpTip label="The repo the branch/PR are opened against. Locked once a branch exists — see below.">
|
||||||
|
<Label>Project</Label>
|
||||||
|
</HelpTip>
|
||||||
|
<ProjectSelector
|
||||||
|
value={projectId || null}
|
||||||
|
onChange={(value) => setProjectId(value || "")}
|
||||||
|
placeholder="Select project..."
|
||||||
|
disabled={!!task.branch_name}
|
||||||
|
/>
|
||||||
|
{task.branch_name && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Project cannot be changed after work has started
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Git Configuration Section */}
|
</CollapsibleSection>
|
||||||
<div className="space-y-4 pt-4 border-t">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<span className="font-medium text-sm">
|
|
||||||
Git & Work Configuration
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Task Type */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<HelpTip label={TASK_TYPE_DESCRIPTIONS[taskType]}>
|
|
||||||
<Label>Task Type</Label>
|
|
||||||
</HelpTip>
|
|
||||||
<Select
|
|
||||||
value={taskType}
|
|
||||||
onValueChange={(v) => setTaskType(v as TaskType)}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{TASK_TYPE_OPTIONS.map((t) => (
|
|
||||||
<SelectItem key={t.value} value={t.value}>
|
|
||||||
{t.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Project Selector (required - all tasks follow git workflow) */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<HelpTip label="The repo the branch/PR are opened against. Locked once a branch exists — see below.">
|
|
||||||
<Label>Project</Label>
|
|
||||||
</HelpTip>
|
|
||||||
<ProjectSelector
|
|
||||||
value={projectId || null}
|
|
||||||
onChange={(value) => setProjectId(value || "")}
|
|
||||||
placeholder="Select project..."
|
|
||||||
disabled={!!task.branch_name}
|
|
||||||
/>
|
|
||||||
{task.branch_name && (
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Project cannot be changed after work has started
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
<DialogFooter className="border-t pt-4">
|
||||||
<HelpTip label="Discards any edits made above and closes without saving.">
|
<HelpTip label="Discards any edits made above and closes without saving.">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -483,7 +474,7 @@ function EditTaskDialogInner({
|
|||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
</div>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Checkbox } from "@/components/ui/checkbox";
|
|||||||
import { Plus, Trash2, Edit3, Check, X } from "lucide-react";
|
import { Plus, Trash2, Edit3, Check, X } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { HelpTip } from "@/components/ui/help-tip";
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
import { CollapsibleSection } from "./collapsible-section";
|
import { CollapsibleSection } from "@/components/ui/collapsible-section";
|
||||||
|
|
||||||
interface AcceptanceCriteriaProps {
|
interface AcceptanceCriteriaProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
@@ -196,7 +196,11 @@ export function AcceptanceCriteria({ task }: AcceptanceCriteriaProps) {
|
|||||||
</HelpTip>
|
</HelpTip>
|
||||||
{!isAdding && (
|
{!isAdding && (
|
||||||
<HelpTip label="Adds a new empty criterion to this list">
|
<HelpTip label="Adds a new empty criterion to this list">
|
||||||
<Button size="sm" variant="ghost" onClick={() => setIsAdding(true)}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setIsAdding(true)}
|
||||||
|
>
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
import { HelpTip } from "@/components/ui/help-tip";
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
import { ListChecks } from "lucide-react";
|
import { ListChecks } from "lucide-react";
|
||||||
import { CodeSnippet } from "@/components/git/code-snippet";
|
import { CodeSnippet } from "@/components/git/code-snippet";
|
||||||
import { CollapsibleSection } from "./collapsible-section";
|
import { CollapsibleSection } from "@/components/ui/collapsible-section";
|
||||||
|
|
||||||
// Mirrors the server-side shape gate (Finding._file_repo_relative in
|
// Mirrors the server-side shape gate (Finding._file_repo_relative in
|
||||||
// roboco/foundation/policy/content/models.py) — keep the character classes
|
// roboco/foundation/policy/content/models.py) — keep the character classes
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Markdown } from "@/components/ui/markdown";
|
import { Markdown } from "@/components/ui/markdown";
|
||||||
import { CollapsibleSection } from "./collapsible-section";
|
import { CollapsibleSection } from "@/components/ui/collapsible-section";
|
||||||
import { exceedsReadabilityThreshold } from "@/lib/content-readability";
|
import { exceedsReadabilityThreshold } from "@/lib/content-readability";
|
||||||
import {
|
import {
|
||||||
FileText,
|
FileText,
|
||||||
@@ -42,9 +42,7 @@ type NoteField =
|
|||||||
function prReviewBadge(task: Task): React.ReactNode {
|
function prReviewBadge(task: Task): React.ReactNode {
|
||||||
const verdict = (
|
const verdict = (
|
||||||
task.notes_structured as
|
task.notes_structured as
|
||||||
| { pr_review?: { verdict?: string } }
|
{ pr_review?: { verdict?: string } } | null | undefined
|
||||||
| null
|
|
||||||
| undefined
|
|
||||||
)?.pr_review?.verdict;
|
)?.pr_review?.verdict;
|
||||||
if (!verdict) {
|
if (!verdict) {
|
||||||
return (
|
return (
|
||||||
@@ -80,9 +78,7 @@ function prReviewBadge(task: Task): React.ReactNode {
|
|||||||
function prReviewCardBg(task: Task): string {
|
function prReviewCardBg(task: Task): string {
|
||||||
const verdict = (
|
const verdict = (
|
||||||
task.notes_structured as
|
task.notes_structured as
|
||||||
| { pr_review?: { verdict?: string } }
|
{ pr_review?: { verdict?: string } } | null | undefined
|
||||||
| null
|
|
||||||
| undefined
|
|
||||||
)?.pr_review?.verdict;
|
)?.pr_review?.verdict;
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
approved:
|
approved:
|
||||||
@@ -116,9 +112,7 @@ const FIELD_TO_SECTION: Record<NoteField, string> = {
|
|||||||
// populated field never renders with no timestamp at all.
|
// populated field never renders with no timestamp at all.
|
||||||
function writtenAt(task: Task, field: NoteField): string | null {
|
function writtenAt(task: Task, field: NoteField): string | null {
|
||||||
const sections = task.notes_structured as
|
const sections = task.notes_structured as
|
||||||
| Record<string, { written_at?: string }>
|
Record<string, { written_at?: string }> | null | undefined;
|
||||||
| null
|
|
||||||
| undefined;
|
|
||||||
const stamp =
|
const stamp =
|
||||||
sections?.[FIELD_TO_SECTION[field]]?.written_at ?? task.created_at;
|
sections?.[FIELD_TO_SECTION[field]]?.written_at ?? task.created_at;
|
||||||
if (!stamp) return null;
|
if (!stamp) return null;
|
||||||
@@ -168,8 +162,8 @@ function EditableNoteCard({
|
|||||||
const [localEditValue, setLocalEditValue] = useState("");
|
const [localEditValue, setLocalEditValue] = useState("");
|
||||||
const [editMode, setEditMode] = useState<"write" | "preview">("write");
|
const [editMode, setEditMode] = useState<"write" | "preview">("write");
|
||||||
// Long content starts collapsed; short content starts expanded.
|
// Long content starts collapsed; short content starts expanded.
|
||||||
const [sectionOpen, setSectionOpen] = useState(() =>
|
const [sectionOpen, setSectionOpen] = useState(
|
||||||
!exceedsReadabilityThreshold(currentValue ?? ""),
|
() => !exceedsReadabilityThreshold(currentValue ?? ""),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Display prop value when not editing, local value when editing
|
// Display prop value when not editing, local value when editing
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { Checkbox } from "@/components/ui/checkbox";
|
|||||||
import { HelpTip } from "@/components/ui/help-tip";
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Markdown } from "@/components/ui/markdown";
|
import { Markdown } from "@/components/ui/markdown";
|
||||||
import { CollapsibleSection } from "./collapsible-section";
|
import { CollapsibleSection } from "@/components/ui/collapsible-section";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -312,7 +312,11 @@ function SubTasksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
|
|||||||
</span>
|
</span>
|
||||||
{!isAdding && (
|
{!isAdding && (
|
||||||
<HelpTip label="Add a new sub-task to this plan">
|
<HelpTip label="Add a new sub-task to this plan">
|
||||||
<Button size="sm" variant="ghost" onClick={() => setIsAdding(true)}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setIsAdding(true)}
|
||||||
|
>
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
DIALOG_SIZES,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -159,7 +160,11 @@ export function CeoRejectDialog({
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
<HelpTip label={!notes.trim() ? "Explain what changes are needed to enable" : null}>
|
<HelpTip
|
||||||
|
label={
|
||||||
|
!notes.trim() ? "Explain what changes are needed to enable" : null
|
||||||
|
}
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
className="inline-block"
|
className="inline-block"
|
||||||
tabIndex={!notes.trim() ? 0 : undefined}
|
tabIndex={!notes.trim() ? 0 : undefined}
|
||||||
@@ -298,7 +303,9 @@ export function CeoApproveDialog({
|
|||||||
</HelpTip>
|
</HelpTip>
|
||||||
{/* Button's own disabled:pointer-events-none would swallow hover, so
|
{/* Button's own disabled:pointer-events-none would swallow hover, so
|
||||||
the tip sits on a wrapping span rather than the Button itself. */}
|
the tip sits on a wrapping span rather than the Button itself. */}
|
||||||
<HelpTip label={remainingCharsTip(notes.trim().length, _CEO_NOTES_MIN)}>
|
<HelpTip
|
||||||
|
label={remainingCharsTip(notes.trim().length, _CEO_NOTES_MIN)}
|
||||||
|
>
|
||||||
<span className="inline-block" tabIndex={tooShort ? 0 : undefined}>
|
<span className="inline-block" tabIndex={tooShort ? 0 : undefined}>
|
||||||
<Button onClick={handleConfirm} disabled={tooShort || isPending}>
|
<Button onClick={handleConfirm} disabled={tooShort || isPending}>
|
||||||
{isPending ? "Approving..." : "Approve & Merge"}
|
{isPending ? "Approving..." : "Approve & Merge"}
|
||||||
@@ -521,7 +528,7 @@ export function CreatePRDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent className="sm:max-w-[525px]">
|
<DialogContent className={DIALOG_SIZES.md}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create Pull Request</DialogTitle>
|
<DialogTitle>Create Pull Request</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Markdown } from "@/components/ui/markdown";
|
import { Markdown } from "@/components/ui/markdown";
|
||||||
import { HelpTip } from "@/components/ui/help-tip";
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
import { CollapsibleSection } from "./collapsible-section";
|
import { CollapsibleSection } from "@/components/ui/collapsible-section";
|
||||||
import { Edit3, Eye, Check, X, ShieldAlert } from "lucide-react";
|
import { Edit3, Eye, Check, X, ShieldAlert } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
|||||||
+48
-8
@@ -12,6 +12,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { exceedsReadabilityThreshold } from "@/lib/content-readability";
|
import { exceedsReadabilityThreshold } from "@/lib/content-readability";
|
||||||
@@ -19,7 +20,7 @@ import { exceedsReadabilityThreshold } from "@/lib/content-readability";
|
|||||||
interface CollapsibleSectionProps {
|
interface CollapsibleSectionProps {
|
||||||
/** Card title content (icon + text + badges as needed) */
|
/** Card title content (icon + text + badges as needed) */
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
/** Right-aligned header controls (edit/preview toggles, buttons) — always visible */
|
/** Right-aligned header controls (edit/preview toggles, buttons) — always visible. "button" variant ignores this. */
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
/** Controlled open state (e.g. force-open while a section is mid-edit). Omit for uncontrolled. */
|
/** Controlled open state (e.g. force-open while a section is mid-edit). Omit for uncontrolled. */
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
@@ -39,6 +40,15 @@ interface CollapsibleSectionProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
headerClassName?: string;
|
headerClassName?: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
/**
|
||||||
|
* "card" (default): the existing Card/CardHeader/CardContent chrome used
|
||||||
|
* across task-detail tabs. "button": no Card, no hover tooltip — a
|
||||||
|
* full-width ghost-button trigger (title left, chevron right), for a
|
||||||
|
* dialog's inline "Advanced Options" disclosure.
|
||||||
|
*/
|
||||||
|
variant?: "card" | "button";
|
||||||
|
/** "button" variant only: CollapsibleContent wrapper classes (default "space-y-4 pt-4"). */
|
||||||
|
contentClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,6 +76,8 @@ export function CollapsibleSection({
|
|||||||
className,
|
className,
|
||||||
headerClassName,
|
headerClassName,
|
||||||
children,
|
children,
|
||||||
|
variant = "card",
|
||||||
|
contentClassName,
|
||||||
}: CollapsibleSectionProps) {
|
}: CollapsibleSectionProps) {
|
||||||
// Resolve the starting state: explicit defaultOpen > content-derived > default to true
|
// Resolve the starting state: explicit defaultOpen > content-derived > default to true
|
||||||
const resolvedDefaultOpen =
|
const resolvedDefaultOpen =
|
||||||
@@ -78,6 +90,40 @@ export function CollapsibleSection({
|
|||||||
if (openProp === undefined) setInternalOpen(next);
|
if (openProp === undefined) setInternalOpen(next);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const motionClassName = cn(
|
||||||
|
"duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||||
|
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
"data-[state=closed]:slide-out-to-top-1 data-[state=open]:slide-in-from-top-1",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (variant === "button") {
|
||||||
|
return (
|
||||||
|
<Collapsible open={open} onOpenChange={setOpen} className={className}>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
type="button"
|
||||||
|
className={cn("w-full justify-between", headerClassName)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
<ChevronDown
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4 shrink-0 transition-transform duration-200",
|
||||||
|
!open && "-rotate-90",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent
|
||||||
|
className={cn(motionClassName, contentClassName ?? "space-y-4 pt-4")}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={className}>
|
<Card className={className}>
|
||||||
<Collapsible open={open} onOpenChange={setOpen} className="contents">
|
<Collapsible open={open} onOpenChange={setOpen} className="contents">
|
||||||
@@ -113,13 +159,7 @@ export function CollapsibleSection({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CollapsibleContent
|
<CollapsibleContent className={motionClassName}>
|
||||||
className={cn(
|
|
||||||
"duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
||||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
||||||
"data-[state=closed]:slide-out-to-top-1 data-[state=open]:slide-in-from-top-1",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CardContent>{children}</CardContent>
|
<CardContent>{children}</CardContent>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
@@ -6,6 +6,21 @@ import { XIcon } from "lucide-react";
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
// Three width buckets collapsing 10+ ad-hoc DialogContent max-w overrides
|
||||||
|
// audited across panel/src. Chosen to minimize visual drift from what was
|
||||||
|
// already shipped: sm covers the compact single-purpose dialogs (previously
|
||||||
|
// max-w-sm/max-w-md/~425px); md equals DialogContent's own unstyled default
|
||||||
|
// (sm:max-w-lg) and covers the common ~480-525px form dialogs (project/
|
||||||
|
// product create-edit, PR/repo pickers); lg covers the wider edit-task
|
||||||
|
// dialog (max-w-2xl). A dialog whose width is load-bearing and far from any
|
||||||
|
// bucket (a wide multi-column form, a custom-positioned command palette)
|
||||||
|
// keeps its own explicit class with a one-line comment instead.
|
||||||
|
export const DIALOG_SIZES = {
|
||||||
|
sm: "sm:max-w-md",
|
||||||
|
md: "sm:max-w-lg",
|
||||||
|
lg: "max-w-2xl",
|
||||||
|
} as const;
|
||||||
|
|
||||||
function Dialog({
|
function Dialog({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
|
|||||||
Reference in New Issue
Block a user