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 {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
@@ -67,7 +68,9 @@ export function ResolveWaitDialog({ agentId }: ResolveWaitDialogProps) {
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<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>
|
||||
<Textarea
|
||||
id="resolution"
|
||||
@@ -77,7 +80,7 @@ export function ResolveWaitDialog({ agentId }: ResolveWaitDialogProps) {
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<DialogFooter>
|
||||
<HelpTip label="Closes without sending anything to the agent">
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
@@ -85,12 +88,15 @@ export function ResolveWaitDialog({ agentId }: ResolveWaitDialogProps) {
|
||||
</HelpTip>
|
||||
<HelpTip label="Delivers the message above to the agent so it can resume">
|
||||
<span>
|
||||
<Button onClick={handleResolve} disabled={resolveWait.isPending}>
|
||||
<Button
|
||||
onClick={handleResolve}
|
||||
disabled={resolveWait.isPending}
|
||||
>
|
||||
{resolveWait.isPending ? "Sending..." : "Send Resolution"}
|
||||
</Button>
|
||||
</span>
|
||||
</HelpTip>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
@@ -124,7 +125,7 @@ export function SpawnAgentDialog({
|
||||
placeholder="Initial instructions for the agent"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<DialogFooter>
|
||||
<HelpTip label="Closes without spawning">
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
@@ -137,7 +138,7 @@ export function SpawnAgentDialog({
|
||||
</Button>
|
||||
</span>
|
||||
</HelpTip>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -13,9 +13,11 @@ import { AgentSelector } from "@/components/agents/agent-selector";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DIALOG_SIZES,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
@@ -96,7 +98,7 @@ export function CreateFlagDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogContent className={DIALOG_SIZES.md}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Quality Flag</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -194,7 +196,7 @@ export function CreateFlagDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<DialogFooter className="pt-4">
|
||||
<HelpTip label="Discards this draft and closes the dialog">
|
||||
<Button
|
||||
type="button"
|
||||
@@ -214,7 +216,7 @@ export function CreateFlagDialog({
|
||||
</Button>
|
||||
</span>
|
||||
</HelpTip>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogTrigger,
|
||||
DIALOG_SIZES,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -121,12 +122,12 @@ function QuickActionsCustomizeDialog() {
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</HelpTip>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogContent className={DIALOG_SIZES.sm}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Customize Quick Actions</DialogTitle>
|
||||
<DialogDescription>
|
||||
Pick which shortcuts show on the Overview dashboard and reorder
|
||||
them with the arrows. Saved to this browser only.
|
||||
Pick which shortcuts show on the Overview dashboard and reorder them
|
||||
with the arrows. Saved to this browser only.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
DialogTrigger,
|
||||
DIALOG_SIZES,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -306,7 +307,7 @@ export function GitActionsPanel({
|
||||
</DialogTrigger>
|
||||
</span>
|
||||
</HelpTip>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogContent className={DIALOG_SIZES.md}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Pull Request</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -378,7 +379,7 @@ export function GitActionsPanel({
|
||||
</DialogTrigger>
|
||||
</span>
|
||||
</HelpTip>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogContent className={DIALOG_SIZES.sm}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Merge Pull Request</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -74,6 +74,9 @@ export function CommandPalette() {
|
||||
|
||||
return (
|
||||
<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
|
||||
showCloseButton={false}
|
||||
className="top-[20%] max-w-xl translate-y-0 gap-0 overflow-hidden p-0"
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DIALOG_SIZES,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -113,7 +114,9 @@ export function CreateProductDialog() {
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</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}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Product</DialogTitle>
|
||||
@@ -172,7 +175,9 @@ export function CreateProductDialog() {
|
||||
</HelpTip>
|
||||
{cells.map((cell) => (
|
||||
<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
|
||||
htmlFor={`cell-${cell.value}`}
|
||||
className="text-sm text-muted-foreground"
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DIALOG_SIZES,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -165,7 +166,9 @@ function EditProductForm({
|
||||
</HelpTip>
|
||||
{cells.map((cell) => (
|
||||
<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
|
||||
htmlFor={`cell-${cell.value}`}
|
||||
className="text-sm text-muted-foreground"
|
||||
@@ -216,7 +219,9 @@ export function EditProductDialog({
|
||||
|
||||
return (
|
||||
<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 ? (
|
||||
<div className="space-y-4 py-4">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DIALOG_SIZES,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
type SelectedRepo,
|
||||
} from "@/components/projects/select-repo-picker";
|
||||
import { HelpTip } from "@/components/ui/help-tip";
|
||||
import { CollapsibleSection } from "@/components/ui/collapsible-section";
|
||||
|
||||
const cells: { value: Team; label: string }[] = [
|
||||
{ value: Team.BACKEND, label: "Backend" },
|
||||
@@ -152,7 +154,7 @@ export function CreateProjectDialog() {
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</HelpTip>
|
||||
<DialogContent className="sm:max-w-[525px]">
|
||||
<DialogContent className={DIALOG_SIZES.md}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Project</DialogTitle>
|
||||
@@ -315,151 +317,145 @@ export function CreateProjectDialog() {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Advanced Options Toggle */}
|
||||
<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.">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="justify-start px-0 text-muted-foreground"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
>
|
||||
{showAdvanced ? "Hide" : "Show"} CI/CD Commands
|
||||
</Button>
|
||||
</HelpTip>
|
||||
{/* Advanced Options */}
|
||||
<CollapsibleSection
|
||||
variant="button"
|
||||
open={showAdvanced}
|
||||
onOpenChange={setShowAdvanced}
|
||||
title={
|
||||
<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>
|
||||
</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 && (
|
||||
<>
|
||||
{/* 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>
|
||||
<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.">
|
||||
<Label htmlFor="lint_command">Lint Command</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="lint_command"
|
||||
value={formData.lint_command || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, lint_command: e.target.value })
|
||||
}
|
||||
placeholder="uv run ruff check ."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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.">
|
||||
<Label htmlFor="lint_command">Lint Command</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="lint_command"
|
||||
value={formData.lint_command || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, lint_command: e.target.value })
|
||||
}
|
||||
placeholder="uv run ruff check ."
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<HelpTip label="Reference only — deliberately excluded from the automated gate since formatting mutates files.">
|
||||
<Label htmlFor="format_command">Format Command</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="format_command"
|
||||
value={formData.format_command || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
format_command: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="uv run ruff format ."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<HelpTip label="Reference only — deliberately excluded from the automated gate since formatting mutates files.">
|
||||
<Label htmlFor="format_command">Format Command</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="format_command"
|
||||
value={formData.format_command || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
format_command: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="uv run ruff format ."
|
||||
/>
|
||||
</div>
|
||||
<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.">
|
||||
<Label htmlFor="typecheck_command">Typecheck Command</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="typecheck_command"
|
||||
value={formData.typecheck_command || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
typecheck_command: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="uv run mypy src/"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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.">
|
||||
<Label htmlFor="typecheck_command">Typecheck Command</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="typecheck_command"
|
||||
value={formData.typecheck_command || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
typecheck_command: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="uv run mypy src/"
|
||||
/>
|
||||
</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="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">
|
||||
Autonomous maintenance (CI-watch, video engine,
|
||||
dependency-update bot, sandbox DB/Redis/Mongo) is configured
|
||||
after creation, from this project's Edit Project dialog.
|
||||
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>
|
||||
|
||||
<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>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DIALOG_SIZES,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -174,7 +175,7 @@ export function QuickEditProjectDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogContent className={DIALOG_SIZES.sm}>
|
||||
{isLoading ? (
|
||||
<div className="space-y-4 py-4">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DIALOG_SIZES,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
@@ -124,7 +125,7 @@ export function SelectRepoDialog({ onSelect }: SelectRepoDialogProps) {
|
||||
</span>
|
||||
</HelpTip>
|
||||
|
||||
<DialogContent className="sm:max-w-[480px]">
|
||||
<DialogContent className={DIALOG_SIZES.md}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Select a repository</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
@@ -22,12 +23,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Plus, ChevronDown, ChevronRight, GitBranch } from "lucide-react";
|
||||
import { CollapsibleSection } from "@/components/ui/collapsible-section";
|
||||
import { Plus, GitBranch } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { AcceptanceCriteriaEditor } from "./acceptance-criteria-editor";
|
||||
import { MarkdownEditor } from "./markdown-editor";
|
||||
@@ -72,7 +69,8 @@ const TASK_TYPE_OPTIONS = [
|
||||
// (branch, commits, PR) — this only classifies the kind of artifact.
|
||||
const TASK_TYPE_DESCRIPTIONS: Record<TaskType, string> = {
|
||||
[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]:
|
||||
"Research findings, committed as notes. Follows the full git workflow.",
|
||||
[TaskType.PLANNING]:
|
||||
@@ -157,7 +155,8 @@ export function CreateTaskDialog() {
|
||||
const trimmedSequence = sequence.trim();
|
||||
if (
|
||||
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";
|
||||
}
|
||||
@@ -235,6 +234,9 @@ export function CreateTaskDialog() {
|
||||
New Task
|
||||
</Button>
|
||||
</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">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Task</DialogTitle>
|
||||
@@ -390,169 +392,158 @@ export function CreateTaskDialog() {
|
||||
/>
|
||||
|
||||
{/* Advanced Options */}
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
type="button"
|
||||
className="w-full justify-between"
|
||||
>
|
||||
<HelpTip label="Parent task, agent assignment, task type, and repo/product routing.">
|
||||
<span>Advanced Options</span>
|
||||
<CollapsibleSection
|
||||
variant="button"
|
||||
open={advancedOpen}
|
||||
onOpenChange={setAdvancedOpen}
|
||||
title={
|
||||
<HelpTip label="Parent task, agent assignment, task type, and repo/product routing.">
|
||||
<span>Advanced Options</span>
|
||||
</HelpTip>
|
||||
}
|
||||
>
|
||||
{/* 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>
|
||||
{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>
|
||||
|
||||
{/* Sequence */}
|
||||
{/* Task Type */}
|
||||
<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 label={TASK_TYPE_DESCRIPTIONS[taskType]}>
|
||||
<Label>Task Type</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>
|
||||
</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)
|
||||
<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>
|
||||
{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>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Type of work: code, documentation, research, etc.
|
||||
</p>
|
||||
</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 */}
|
||||
<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.">
|
||||
<Button
|
||||
type="button"
|
||||
@@ -575,7 +566,7 @@ export function CreateTaskDialog() {
|
||||
</Button>
|
||||
</span>
|
||||
</HelpTip>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -9,9 +9,11 @@ import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DIALOG_SIZES,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
@@ -20,12 +22,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { ChevronDown, ChevronRight, GitBranch } from "lucide-react";
|
||||
import { CollapsibleSection } from "@/components/ui/collapsible-section";
|
||||
import { GitBranch } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { MarkdownEditor } from "./markdown-editor";
|
||||
import { AcceptanceCriteriaEditor } from "./acceptance-criteria-editor";
|
||||
@@ -202,7 +200,9 @@ function EditTaskDialogInner({
|
||||
|
||||
return (
|
||||
<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>
|
||||
<DialogTitle>Edit Task</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -321,149 +321,140 @@ function EditTaskDialogInner({
|
||||
/>
|
||||
|
||||
{/* Advanced Options */}
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
type="button"
|
||||
className="w-full justify-between"
|
||||
>
|
||||
<HelpTip label="Agent assignment, target date, task type, and repo routing.">
|
||||
<span>Advanced Options</span>
|
||||
</HelpTip>
|
||||
{advancedOpen ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-4 pt-4">
|
||||
{/* Assigned 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>Assigned To</Label>
|
||||
</HelpTip>
|
||||
<AgentSelector
|
||||
value={assignedTo}
|
||||
onChange={setAssignedTo}
|
||||
placeholder="Unassigned"
|
||||
filterByTeam={team}
|
||||
/>
|
||||
</div>
|
||||
<CollapsibleSection
|
||||
variant="button"
|
||||
open={advancedOpen}
|
||||
onOpenChange={setAdvancedOpen}
|
||||
title={
|
||||
<HelpTip label="Agent assignment, target date, task type, and repo routing.">
|
||||
<span>Advanced Options</span>
|
||||
</HelpTip>
|
||||
}
|
||||
>
|
||||
{/* Assigned 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>Assigned To</Label>
|
||||
</HelpTip>
|
||||
<AgentSelector
|
||||
value={assignedTo}
|
||||
onChange={setAssignedTo}
|
||||
placeholder="Unassigned"
|
||||
filterByTeam={team}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Target Date */}
|
||||
<div className="space-y-2">
|
||||
<HelpTip label="Informational deadline only — nothing in the lifecycle enforces or auto-escalates on it.">
|
||||
<Label>Target Date</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={targetDate}
|
||||
onChange={(e) => setTargetDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* Target Date */}
|
||||
<div className="space-y-2">
|
||||
<HelpTip label="Informational deadline only — nothing in the lifecycle enforces or auto-escalates on it.">
|
||||
<Label>Target Date</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={targetDate}
|
||||
onChange={(e) => setTargetDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Sequence */}
|
||||
<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.">
|
||||
<Label htmlFor="edit-sequence">Sequence</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="edit-sequence"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
value={sequence}
|
||||
onChange={(e) => setSequence(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* Sequence */}
|
||||
<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.">
|
||||
<Label htmlFor="edit-sequence">Sequence</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
id="edit-sequence"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
value={sequence}
|
||||
onChange={(e) => setSequence(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Budget (USD) */}
|
||||
<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.">
|
||||
<Label>Budget (USD)</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
type="number"
|
||||
min="0.01"
|
||||
step="0.01"
|
||||
placeholder="No cap"
|
||||
value={budgetUsd}
|
||||
onChange={(e) => setBudgetUsd(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Must be greater than 0 — a 0 budget would block the task
|
||||
before it spends a cent. Leave blank for no cap: budgets
|
||||
enforce only when explicitly set.
|
||||
{/* Budget (USD) */}
|
||||
<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.">
|
||||
<Label>Budget (USD)</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
type="number"
|
||||
min="0.01"
|
||||
step="0.01"
|
||||
placeholder="No cap"
|
||||
value={budgetUsd}
|
||||
onChange={(e) => setBudgetUsd(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Must be greater than 0 — a 0 budget would block the task before
|
||||
it spends a cent. Leave blank for no cap: budgets enforce only
|
||||
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>
|
||||
{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)}`
|
||||
: ""}
|
||||
)}
|
||||
</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" />
|
||||
<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>
|
||||
|
||||
{/* 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" />
|
||||
<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>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* 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.">
|
||||
<Button
|
||||
type="button"
|
||||
@@ -483,7 +474,7 @@ function EditTaskDialogInner({
|
||||
</Button>
|
||||
</span>
|
||||
</HelpTip>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Plus, Trash2, Edit3, Check, X } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { HelpTip } from "@/components/ui/help-tip";
|
||||
import { CollapsibleSection } from "./collapsible-section";
|
||||
import { CollapsibleSection } from "@/components/ui/collapsible-section";
|
||||
|
||||
interface AcceptanceCriteriaProps {
|
||||
task: Task;
|
||||
@@ -196,7 +196,11 @@ export function AcceptanceCriteria({ task }: AcceptanceCriteriaProps) {
|
||||
</HelpTip>
|
||||
{!isAdding && (
|
||||
<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" />
|
||||
Add
|
||||
</Button>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { HelpTip } from "@/components/ui/help-tip";
|
||||
import { ListChecks } from "lucide-react";
|
||||
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
|
||||
// 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 { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
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 {
|
||||
FileText,
|
||||
@@ -42,9 +42,7 @@ type NoteField =
|
||||
function prReviewBadge(task: Task): React.ReactNode {
|
||||
const verdict = (
|
||||
task.notes_structured as
|
||||
| { pr_review?: { verdict?: string } }
|
||||
| null
|
||||
| undefined
|
||||
{ pr_review?: { verdict?: string } } | null | undefined
|
||||
)?.pr_review?.verdict;
|
||||
if (!verdict) {
|
||||
return (
|
||||
@@ -80,9 +78,7 @@ function prReviewBadge(task: Task): React.ReactNode {
|
||||
function prReviewCardBg(task: Task): string {
|
||||
const verdict = (
|
||||
task.notes_structured as
|
||||
| { pr_review?: { verdict?: string } }
|
||||
| null
|
||||
| undefined
|
||||
{ pr_review?: { verdict?: string } } | null | undefined
|
||||
)?.pr_review?.verdict;
|
||||
const map: Record<string, string> = {
|
||||
approved:
|
||||
@@ -116,9 +112,7 @@ const FIELD_TO_SECTION: Record<NoteField, string> = {
|
||||
// populated field never renders with no timestamp at all.
|
||||
function writtenAt(task: Task, field: NoteField): string | null {
|
||||
const sections = task.notes_structured as
|
||||
| Record<string, { written_at?: string }>
|
||||
| null
|
||||
| undefined;
|
||||
Record<string, { written_at?: string }> | null | undefined;
|
||||
const stamp =
|
||||
sections?.[FIELD_TO_SECTION[field]]?.written_at ?? task.created_at;
|
||||
if (!stamp) return null;
|
||||
@@ -168,8 +162,8 @@ function EditableNoteCard({
|
||||
const [localEditValue, setLocalEditValue] = useState("");
|
||||
const [editMode, setEditMode] = useState<"write" | "preview">("write");
|
||||
// Long content starts collapsed; short content starts expanded.
|
||||
const [sectionOpen, setSectionOpen] = useState(() =>
|
||||
!exceedsReadabilityThreshold(currentValue ?? ""),
|
||||
const [sectionOpen, setSectionOpen] = useState(
|
||||
() => !exceedsReadabilityThreshold(currentValue ?? ""),
|
||||
);
|
||||
|
||||
// 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 { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Markdown } from "@/components/ui/markdown";
|
||||
import { CollapsibleSection } from "./collapsible-section";
|
||||
import { CollapsibleSection } from "@/components/ui/collapsible-section";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -312,7 +312,11 @@ function SubTasksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
|
||||
</span>
|
||||
{!isAdding && (
|
||||
<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" />
|
||||
Add
|
||||
</Button>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DIALOG_SIZES,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -159,7 +160,11 @@ export function CeoRejectDialog({
|
||||
Cancel
|
||||
</Button>
|
||||
</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
|
||||
className="inline-block"
|
||||
tabIndex={!notes.trim() ? 0 : undefined}
|
||||
@@ -298,7 +303,9 @@ export function CeoApproveDialog({
|
||||
</HelpTip>
|
||||
{/* Button's own disabled:pointer-events-none would swallow hover, so
|
||||
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}>
|
||||
<Button onClick={handleConfirm} disabled={tooShort || isPending}>
|
||||
{isPending ? "Approving..." : "Approve & Merge"}
|
||||
@@ -521,7 +528,7 @@ export function CreatePRDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="sm:max-w-[525px]">
|
||||
<DialogContent className={DIALOG_SIZES.md}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Pull Request</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Markdown } from "@/components/ui/markdown";
|
||||
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 { toast } from "sonner";
|
||||
|
||||
|
||||
+48
-8
@@ -12,6 +12,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { exceedsReadabilityThreshold } from "@/lib/content-readability";
|
||||
@@ -19,7 +20,7 @@ import { exceedsReadabilityThreshold } from "@/lib/content-readability";
|
||||
interface CollapsibleSectionProps {
|
||||
/** Card title content (icon + text + badges as needed) */
|
||||
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;
|
||||
/** Controlled open state (e.g. force-open while a section is mid-edit). Omit for uncontrolled. */
|
||||
open?: boolean;
|
||||
@@ -39,6 +40,15 @@ interface CollapsibleSectionProps {
|
||||
className?: string;
|
||||
headerClassName?: string;
|
||||
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,
|
||||
headerClassName,
|
||||
children,
|
||||
variant = "card",
|
||||
contentClassName,
|
||||
}: CollapsibleSectionProps) {
|
||||
// Resolve the starting state: explicit defaultOpen > content-derived > default to true
|
||||
const resolvedDefaultOpen =
|
||||
@@ -78,6 +90,40 @@ export function CollapsibleSection({
|
||||
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 (
|
||||
<Card className={className}>
|
||||
<Collapsible open={open} onOpenChange={setOpen} className="contents">
|
||||
@@ -113,13 +159,7 @@ export function CollapsibleSection({
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CollapsibleContent
|
||||
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",
|
||||
)}
|
||||
>
|
||||
<CollapsibleContent className={motionClassName}>
|
||||
<CardContent>{children}</CardContent>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
@@ -6,6 +6,21 @@ import { XIcon } from "lucide-react";
|
||||
|
||||
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({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
|
||||
Reference in New Issue
Block a user