diff --git a/CHANGELOG.md b/CHANGELOG.md index d1303ed2..ee21945e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **`ollama-init` is best-effort and gates startup on the models being present**, so a slow or unreachable model registry can no longer down a fully-cached deployment. - **A PM can recover its own coordination task from `needs_revision`**, and lifecycle-transition notes are kept off the human-facing `quick_context` / `dev_notes` columns. - **Panel:** a copyable task-id chip with a stable, non-shifting task header, clickable Branch / PR links with a branch-copy button, and clearer agent status badges. +- **Panel:** the per-project Conventions editor lays out in a responsive two-column grid (Module boundaries | Rules, then Waivers | Custom rules) with Recent violations full-width, inside a wider modal on large viewports — instead of one long single column. It collapses to a single column on mobile and is capped so it stays sane up to a 27" display. ## [0.8.0] - 2026-06-20 diff --git a/panel/src/components/conventions/conventions-tab.tsx b/panel/src/components/conventions/conventions-tab.tsx index 2de0278f..c353a730 100644 --- a/panel/src/components/conventions/conventions-tab.tsx +++ b/panel/src/components/conventions/conventions-tab.tsx @@ -35,7 +35,9 @@ const FORBIDDABLE_KINDS: DefinitionKind[] = [ function actionToast(verb: string, result: ConventionsActionResult): void { if (result.created && result.pr_number != null) { - toast.success(`${verb}: opened PR #${result.pr_number} on ${result.branch}`); + toast.success( + `${verb}: opened PR #${result.pr_number} on ${result.branch}`, + ); } else { toast.success( `${verb}: prepared on ${result.branch} (no remote PR — workspace not cloned)`, @@ -108,10 +110,14 @@ export function ConventionsTab({ projectId }: { projectId: string }) { const updateModule = (index: number, next: Partial) => edit({ - modules: standard.modules.map((m, i) => (i === index ? { ...m, ...next } : m)), + modules: standard.modules.map((m, i) => + i === index ? { ...m, ...next } : m, + ), }); const addModule = () => - edit({ modules: [...standard.modules, { path: "", purpose: "", forbidden: [] }] }); + edit({ + modules: [...standard.modules, { path: "", purpose: "", forbidden: [] }], + }); const removeModule = (index: number) => edit({ modules: standard.modules.filter((_, i) => i !== index) }); const toggleForbidden = (index: number, kind: DefinitionKind) => { @@ -124,7 +130,9 @@ export function ConventionsTab({ projectId }: { projectId: string }) { const updateCustom = (index: number, next: Partial) => edit({ - custom: standard.custom.map((c, i) => (i === index ? { ...c, ...next } : c)), + custom: standard.custom.map((c, i) => + i === index ? { ...c, ...next } : c, + ), }); const addCustom = () => edit({ @@ -138,10 +146,14 @@ export function ConventionsTab({ projectId }: { projectId: string }) { const updateWaiver = (index: number, next: Partial) => edit({ - waivers: standard.waivers.map((w, i) => (i === index ? { ...w, ...next } : w)), + waivers: standard.waivers.map((w, i) => + i === index ? { ...w, ...next } : w, + ), }); const addWaiver = () => - edit({ waivers: [...standard.waivers, { path: "", rule: "", reason: "" }] }); + edit({ + waivers: [...standard.waivers, { path: "", rule: "", reason: "" }], + }); const removeWaiver = (index: number) => edit({ waivers: standard.waivers.filter((_, i) => i !== index) }); @@ -152,6 +164,240 @@ export function ConventionsTab({ projectId }: { projectId: string }) { const degraded = status === "degraded"; const usingDefaults = status === "missing" || status === "unknown"; + const moduleBoundaries = ( + + + Module boundaries + + Which definition kinds are forbidden in each module. Click a kind to + toggle it. + + + + {standard.modules.length === 0 && ( +

+ No modules mapped yet. +

+ )} + {standard.modules.map((module, index) => ( +
+
+ updateModule(index, { path: e.target.value })} + /> + +
+ updateModule(index, { purpose: e.target.value })} + /> +
+ {FORBIDDABLE_KINDS.map((kind) => ( + toggleForbidden(index, kind)} + > + no {kind} + + ))} +
+
+ ))} + +
+
+ ); + + const rules = ( + + + Rules + + Toggle a rule between warn (advisory) and block (refuses the gate). + + + + {Object.values(standard.rules).map((rule) => ( +
+ {rule.name.replace(/_/g, " ")} +
+ + {rule.level} + + + setRuleLevel(rule.name, checked ? "block" : "warn") + } + /> +
+
+ ))} +
+
+ ); + + const waivers = ( + + + Waivers + + Accountable escapes — exempt a file from a rule with a reason + (reviewed in the PR, never a silent in-code suppression). + + + + {standard.waivers.map((waiver, index) => ( +
+
+ updateWaiver(index, { path: e.target.value })} + /> + updateWaiver(index, { rule: e.target.value })} + /> + +
+ updateWaiver(index, { reason: e.target.value })} + /> +
+ ))} + +
+
+ ); + + const customRules = ( + + + Custom rules + + Project-specific regex rules — a pattern, a message, and a level. + + + + {standard.custom.map((rule, index) => ( +
+
+ updateCustom(index, { id: e.target.value })} + /> + + {rule.level} + + + updateCustom(index, { level: checked ? "block" : "warn" }) + } + /> + +
+ updateCustom(index, { pattern: e.target.value })} + /> + updateCustom(index, { message: e.target.value })} + /> +
+ ))} + +
+
+ ); + + const recentViolations = ( + + + Recent violations + + The latest findings recorded across this project's tasks. + + + + {(!findings || findings.length === 0) && ( +

+ No violations recorded yet. +

+ )} + {(findings ?? []).map((finding, index) => ( +
+
+ + {finding.file}:{finding.line} + {" "} + {finding.message} +
+ + {finding.rule} + +
+ ))} +
+
+ ); + return (
{degraded && ( @@ -171,7 +417,9 @@ export function ConventionsTab({ projectId }: { projectId: string }) { {usingDefaults && ( - Using auto-derived defaults + + Using auto-derived defaults + No .roboco/conventions.yml is committed yet. These rules are auto-derived from the repository and are already @@ -181,227 +429,18 @@ export function ConventionsTab({ projectId }: { projectId: string }) { )} - - - Module boundaries - - Which definition kinds are forbidden in each module. Click a kind to - toggle it. - - - - {standard.modules.length === 0 && ( -

No modules mapped yet.

- )} - {standard.modules.map((module, index) => ( -
-
- updateModule(index, { path: e.target.value })} - /> - -
- updateModule(index, { purpose: e.target.value })} - /> -
- {FORBIDDABLE_KINDS.map((kind) => ( - toggleForbidden(index, kind)} - > - no {kind} - - ))} -
-
- ))} - -
-
+ {/* Two columns on wide viewports so the modal isn't a long single column; + Module boundaries | Rules, then Waivers | Custom rules. Each cell keeps + its natural height (items-start) and stacks to one column on mobile. */} +
+ {moduleBoundaries} + {rules} + {waivers} + {customRules} +
- - - Rules - - Toggle a rule between warn (advisory) and block (refuses the gate). - - - - {Object.values(standard.rules).map((rule) => ( -
- {rule.name.replace(/_/g, " ")} -
- - {rule.level} - - - setRuleLevel(rule.name, checked ? "block" : "warn") - } - /> -
-
- ))} -
-
- - - - Custom rules - - Project-specific regex rules — a pattern, a message, and a level. - - - - {standard.custom.map((rule, index) => ( -
-
- updateCustom(index, { id: e.target.value })} - /> - - {rule.level} - - - updateCustom(index, { level: checked ? "block" : "warn" }) - } - /> - -
- updateCustom(index, { pattern: e.target.value })} - /> - updateCustom(index, { message: e.target.value })} - /> -
- ))} - -
-
- - - - Waivers - - Accountable escapes — exempt a file from a rule with a reason - (reviewed in the PR, never a silent in-code suppression). - - - - {standard.waivers.map((waiver, index) => ( -
-
- updateWaiver(index, { path: e.target.value })} - /> - updateWaiver(index, { rule: e.target.value })} - /> - -
- updateWaiver(index, { reason: e.target.value })} - /> -
- ))} - -
-
- - - - Recent violations - - The latest findings recorded across this project's tasks. - - - - {(!findings || findings.length === 0) && ( -

- No violations recorded yet. -

- )} - {(findings ?? []).map((finding, index) => ( -
-
- - {finding.file}:{finding.line} - {" "} - {finding.message} -
- - {finding.rule} - -
- ))} -
-
+ {/* Recent violations spans the full width on its own row. */} + {recentViolations}
diff --git a/panel/src/components/projects/edit-project-dialog.tsx b/panel/src/components/projects/edit-project-dialog.tsx index 0a87b1dc..c1ef6991 100644 --- a/panel/src/components/projects/edit-project-dialog.tsx +++ b/panel/src/components/projects/edit-project-dialog.tsx @@ -22,12 +22,7 @@ import { } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Skeleton } from "@/components/ui/skeleton"; -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@/components/ui/tabs"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ConventionsTab } from "@/components/conventions/conventions-tab"; import { Key, KeyRound } from "lucide-react"; import { toast } from "sonner"; @@ -65,10 +60,16 @@ function EditProjectForm({ const [isActive, setIsActive] = useState(project.is_active); const [testCommand, setTestCommand] = useState(project.test_command || ""); const [lintCommand, setLintCommand] = useState(project.lint_command || ""); - const [formatCommand, setFormatCommand] = useState(project.format_command || ""); - const [typecheckCommand, setTypecheckCommand] = useState(project.typecheck_command || ""); + const [formatCommand, setFormatCommand] = useState( + project.format_command || "", + ); + const [typecheckCommand, setTypecheckCommand] = useState( + project.typecheck_command || "", + ); const [buildCommand, setBuildCommand] = useState(project.build_command || ""); - const [qualityCommand, setQualityCommand] = useState(project.quality_command || ""); + const [qualityCommand, setQualityCommand] = useState( + project.quality_command || "", + ); // Token handling const [newToken, setNewToken] = useState(""); @@ -113,7 +114,7 @@ function EditProjectForm({ onSuccess(); } catch (error) { toast.error( - `Failed to update project: ${error instanceof Error ? error.message : "Unknown error"}` + `Failed to update project: ${error instanceof Error ? error.message : "Unknown error"}`, ); } }; @@ -122,7 +123,9 @@ function EditProjectForm({
Edit Project - Update project settings. Slug cannot be changed. + + Update project settings. Slug cannot be changed. +
{/* Slug (read-only) */} @@ -165,18 +168,25 @@ function EditProjectForm({ {project.has_git_token ? ( <> - Token is set + + Token is set + ) : ( <> - No token configured + + No token configured + )} {project.has_git_token && (
-
)} @@ -213,7 +224,10 @@ function EditProjectForm({ {/* Assigned Cell */}
- setAssignedCell(value)} + > @@ -241,7 +255,11 @@ function EditProjectForm({ {/* Active Status */}
- +
{/* Advanced Options Toggle */} @@ -315,8 +333,8 @@ function EditProjectForm({ placeholder="make gate" />

- Fast pre-submit gate (lint + types + complexity, no tests) run in - the dev's workspace at hand-off to QA. + Fast pre-submit gate (lint + types + complexity, no tests) run + in the dev's workspace at hand-off to QA.

@@ -335,12 +353,25 @@ function EditProjectForm({ } // Main dialog component - handles data fetching and dialog state -export function EditProjectDialog({ projectId, open, onOpenChange }: EditProjectDialogProps) { +export function EditProjectDialog({ + projectId, + open, + onOpenChange, +}: EditProjectDialogProps) { const { data: project, isLoading } = useProject(projectId); + const [tab, setTab] = useState("settings"); return ( - + {/* Settings is a compact form; Conventions uses a two-column grid, so it + gets a wider, responsive modal (capped so it stays sane on a 27"). */} + {isLoading ? (
@@ -349,7 +380,7 @@ export function EditProjectDialog({ projectId, open, onOpenChange }: EditProject
) : project ? ( - + Settings Conventions @@ -368,7 +399,9 @@ export function EditProjectDialog({ projectId, open, onOpenChange }: EditProject ) : ( -
Project not found
+
+ Project not found +
)}