[a1e2bfb4] Fix 26 verified UI bugs across the panel dashboard (#175)

* [41219301] Fix 26 verified UI bugs in the panel dashboard (#174)

* [115788ef] API/state bugs batch 1 — PATCH fix, WebSocket reconnect, agent roster, sessions export, timestamp (#173)

* fix(orchestrator): launch agent MCP servers with uv run --no-sync

Agent MCP servers (flow/do/git-readonly/optimal/docs/search) are launched as
`uv run python -m roboco.mcp.<server>` with cwd = the agent's workspace clone.
When that clone's uv.lock drifts from the baked image, `uv run` re-syncs the
dependency set mid-spawn and the servers never reach "connected" — they sit at
status="pending", so the agent gets zero gateway verbs. It then can't claim,
commit, or even i_am_idle (all MCP verbs), so its Stop is rejected and it
respawns in a loop, re-doing work it can't submit.

UV_PROJECT_ENVIRONMENT pins the venv location but does not stop the cwd-relative
resolve/sync; --no-sync does, so the servers reuse the baked /app/.venv as-is
and start instantly. PMs were unaffected only because they run from /app where
the env already matches the lock.

* fix(agent): launch agent uv-run subprocesses with --no-sync

Agents with a write workspace (developer/product_owner/head_marketing/documenter)
run with cwd = their git workspace clone. Claude Code launches each MCP server
(flow/do/git-readonly/optimal/docs/search) and the SDK server as
`uv run python -m ...` from that cwd. When the clone's uv.lock drifts from the
baked image, `uv run` re-resolves and re-syncs /app/.venv against the clone's
lock — a multi-minute stall on a cold wheel cache — so the servers never reach
"connected": they sit at status="pending" and the agent gets ZERO gateway
verbs. It then can't claim/commit/idle (all MCP verbs), its Stop is rejected,
and it respawns in a loop redoing work it can't submit.

UV_PROJECT_ENVIRONMENT pins the venv location but does NOT stop the cwd-relative
resolve/sync (confirmed empirically on uv 0.11.1); `--no-sync` does, so the
servers reuse the baked /app/.venv as-is and start instantly. The /app-cwd roles
(qa/cell_pm/main_pm/auditor) were unaffected because their env already matches.

- orchestrator.py: --no-sync on all 6 generated MCP servers
- docker/scripts/sdk-startup-hook.sh: --no-sync on the agent_sdk.server launch
- test_spawn_strict_mcp.py: assert every server's args start with run,--no-sync

* [115788ef] fix(api): use PATCH not PUT in tasksApi.update(), remove WS double-increment, fix staleTime/roster id, remove sessions groupsApi dup, add < 1h ago label

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* [db2c341f] UI/visual bugs batch 1 — priority labels, QA columns, DnD prompt, CEO dialog, dark mode (#172)

* fix(orchestrator): launch agent MCP servers with uv run --no-sync

Agent MCP servers (flow/do/git-readonly/optimal/docs/search) are launched as
`uv run python -m roboco.mcp.<server>` with cwd = the agent's workspace clone.
When that clone's uv.lock drifts from the baked image, `uv run` re-syncs the
dependency set mid-spawn and the servers never reach "connected" — they sit at
status="pending", so the agent gets zero gateway verbs. It then can't claim,
commit, or even i_am_idle (all MCP verbs), so its Stop is rejected and it
respawns in a loop, re-doing work it can't submit.

UV_PROJECT_ENVIRONMENT pins the venv location but does not stop the cwd-relative
resolve/sync; --no-sync does, so the servers reuse the baked /app/.venv as-is
and start instantly. PMs were unaffected only because they run from /app where
the env already matches the lock.

* fix(agent): launch agent uv-run subprocesses with --no-sync

Agents with a write workspace (developer/product_owner/head_marketing/documenter)
run with cwd = their git workspace clone. Claude Code launches each MCP server
(flow/do/git-readonly/optimal/docs/search) and the SDK server as
`uv run python -m ...` from that cwd. When the clone's uv.lock drifts from the
baked image, `uv run` re-resolves and re-syncs /app/.venv against the clone's
lock — a multi-minute stall on a cold wheel cache — so the servers never reach
"connected": they sit at status="pending" and the agent gets ZERO gateway
verbs. It then can't claim/commit/idle (all MCP verbs), its Stop is rejected,
and it respawns in a loop redoing work it can't submit.

UV_PROJECT_ENVIRONMENT pins the venv location but does NOT stop the cwd-relative
resolve/sync (confirmed empirically on uv 0.11.1); `--no-sync` does, so the
servers reuse the baked /app/.venv as-is and start instantly. The /app-cwd roles
(qa/cell_pm/main_pm/auditor) were unaffected because their env already matches.

- orchestrator.py: --no-sync on all 6 generated MCP servers
- docker/scripts/sdk-startup-hook.sh: --no-sync on the agent_sdk.server launch
- test_spawn_strict_mcp.py: assert every server's args start with run,--no-sync

* [db2c341f] fix(ui): priority badges, QA columns, DnD dialog, CEO label, dark mode

- priority-indicator.tsx: update labels P0→P0-Highest etc, add text-xs to all color strings, fix className operator precedence bug
- task-table.tsx: match priority label format and add text-xs to badge className
- kanban-column.tsx: show QA Pass/Fail buttons in VERIFYING column alongside AWAITING_QA
- kanban-board.tsx: intercept DnD drops onto NEEDS_REVISION/AWAITING_DOCUMENTATION to show notes dialog when showQaActions is true
- task-action-dialogs.tsx: change CeoApproveDialog Label from 'Approval notes' to 'Notes required'; fix all Cancel buttons to call handleOpenChange(false) so state is cleared on dismiss
- create-task-dialog.tsx: reset form when dialog is closed without submitting
- active-blockers-panel.tsx: add dark:border-red-900 dark:bg-red-950 dark:hover:bg-red-900 to blocker items
- notifications/page.tsx: add dark: Tailwind variants for NORMAL, HIGH, URGENT priority badge colors

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

* [123e2ec2] Fix remaining 15 UI bugs — revision pass after CEO rejection (#178)

* [cdb9b22a] fix(ui): task-header BACKLOG/NEEDS_REVISION actions, P0 priority label, chat-composer safe clear, and inline-edit double-mutation guards (#176)

- task-header.tsx: add BACKLOG ('Activate Task') and NEEDS_REVISION ('Start Revision') cases to getAvailableActions() switch so the Actions dropdown is never empty for those statuses
- draft-proposal-card.tsx: PRIORITY_LABELS[0] changed from 'Urgent' to 'Highest' to match backend contract
- chat-composer.tsx: move setValue('') inside try-block after onSend resolves; a failed send now preserves the textarea text
- acceptance-criteria.tsx: onMouseDown={(e)=>e.preventDefault()} on inline-edit save button to prevent onBlur+onClick double API mutation
- tab-dependencies.tsx: same onMouseDown guard on parent-task inline-edit save button
- tab-plan.tsx: onMouseDown guards on all inline-edit/add save buttons (ApproachSection, SubTasks, TechConsiderations, Risks, OpenQuestions)

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* [01852d69] fix(dashboard): wire real agent status, refetch all 4 queries, error indicator, Coming Soon tooltip on search, sentinel div auto-scroll in message-list and mentor-chat, and New Report / Generate Report button mutations in reports-panel and auditor-dashboard — all 9 files fixed (#177)

Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

* [3f35f502] feat(tasks): add case activate and case start-revision to handleAction switch in task detail page (#179) (#180)

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* fix(tasks): route Start Revision through the operator status override

The new "Start Revision" action on a NEEDS_REVISION task called
lifecycle.start (POST /tasks/{id}/start), which is assignee-only — so an
operator/CEO clicking it from the task detail page got a 403 ("Only the
assigned agent can start this task") instead of a transition.

Route it through useUpdateTask (PATCH /tasks/{id} with status=in_progress)
instead. The backend treats status as an audited admin override applied via
admin_set_status and gated on elevated (ASSIGN) permissions — the same
god-mode path the kanban board uses for operator status changes — so the
operator can nudge a needs_revision task back into progress for its assignee
to rework. Mirrors the existing kanban updateTask.mutateAsync shape.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
This commit is contained in:
Renzo F
2026-06-16 06:57:00 +02:00
committed by GitHub
co-authored by Renn F Frontend Developer 1 Frontend Developer 2
parent de1336c74d
commit a89d3cc885
29 changed files with 201 additions and 84 deletions
@@ -181,7 +181,7 @@ export function CreateTaskDialog() {
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<Dialog open={open} onOpenChange={(newOpen) => { setOpen(newOpen); if (!newOpen) resetForm(); }}>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
@@ -235,6 +235,7 @@ export function AcceptanceCriteria({ task }: AcceptanceCriteriaProps) {
<Button
size="sm"
onClick={handleSaveEdit}
onMouseDown={(e) => e.preventDefault()}
className="h-7 w-7 p-0"
>
<Check className="h-4 w-4" />
@@ -17,6 +17,7 @@ function formatTime(timestamp: string): string {
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffHours / 24);
if (diffHours < 1) return "< 1h ago";
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString("en-US", {
@@ -327,6 +327,7 @@ export function TabDependencies({ task }: TabDependenciesProps) {
<Button
size="sm"
onClick={handleParentSave}
onMouseDown={(e) => e.preventDefault()}
disabled={updateTask.isPending}
className="h-7 w-7 p-0"
>
@@ -121,7 +121,7 @@ function ApproachSection({ task, plan }: { task: Task; plan: TaskPlan }) {
</TabsList>
</Tabs>
<Button size="sm" variant="ghost" onClick={handleCancel}><X className="h-4 w-4" /></Button>
<Button size="sm" onClick={handleSave} disabled={updateTask.isPending}>
<Button size="sm" onClick={handleSave} onMouseDown={(e) => e.preventDefault()} disabled={updateTask.isPending}>
<Check className="h-4 w-4 mr-1" />Save
</Button>
</div>
@@ -333,7 +333,7 @@ function SubTasksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
<Button size="sm" variant="ghost" onClick={() => { setNewTitle(""); setIsAdding(false); }} className="h-7 w-7 p-0">
<X className="h-4 w-4" />
</Button>
<Button size="sm" onClick={handleAdd} disabled={!newTitle.trim()} className="h-7 w-7 p-0">
<Button size="sm" onClick={handleAdd} onMouseDown={(e) => e.preventDefault()} disabled={!newTitle.trim()} className="h-7 w-7 p-0">
<Check className="h-4 w-4" />
</Button>
</div>
@@ -489,7 +489,7 @@ function TechConsiderationsSection({ task, plan }: { task: Task; plan: TaskPlan
<Button size="sm" variant="ghost" onClick={() => { setNewItem(""); setIsAdding(false); }} className="h-7 w-7 p-0">
<X className="h-4 w-4" />
</Button>
<Button size="sm" onClick={handleAdd} disabled={!newItem.trim()} className="h-7 w-7 p-0">
<Button size="sm" onClick={handleAdd} onMouseDown={(e) => e.preventDefault()} disabled={!newItem.trim()} className="h-7 w-7 p-0">
<Check className="h-4 w-4" />
</Button>
</li>
@@ -620,7 +620,7 @@ function RisksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
</Select>
<div className="flex-1" />
<Button size="sm" variant="ghost" onClick={() => setEditingIdx(null)}><X className="h-4 w-4" /></Button>
<Button size="sm" onClick={handleEdit}><Check className="h-4 w-4" /></Button>
<Button size="sm" onClick={handleEdit} onMouseDown={(e) => e.preventDefault()}><Check className="h-4 w-4" /></Button>
</div>
</div>
) : (
@@ -682,7 +682,7 @@ function RisksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
</Select>
<div className="flex-1" />
<Button size="sm" variant="ghost" onClick={() => { setNewDesc(""); setNewMit(""); setIsAdding(false); }}><X className="h-4 w-4" /></Button>
<Button size="sm" onClick={handleAdd} disabled={!newDesc.trim()}><Check className="h-4 w-4" /></Button>
<Button size="sm" onClick={handleAdd} onMouseDown={(e) => e.preventDefault()} disabled={!newDesc.trim()}><Check className="h-4 w-4" /></Button>
</div>
</div>
)}
@@ -801,7 +801,7 @@ function OpenQuestionsSection({ task, plan }: { task: Task; plan: TaskPlan }) {
<div className="flex items-center gap-2">
<div className="flex-1" />
<Button size="sm" variant="ghost" onClick={() => setEditingIdx(null)}><X className="h-4 w-4" /></Button>
<Button size="sm" onClick={handleEdit}><Check className="h-4 w-4" /></Button>
<Button size="sm" onClick={handleEdit} onMouseDown={(e) => e.preventDefault()}><Check className="h-4 w-4" /></Button>
</div>
</div>
) : (
@@ -860,7 +860,7 @@ function OpenQuestionsSection({ task, plan }: { task: Task; plan: TaskPlan }) {
<div className="flex items-center gap-2">
<div className="flex-1" />
<Button size="sm" variant="ghost" onClick={() => { setNewQuestion(""); setIsAdding(false); }}><X className="h-4 w-4" /></Button>
<Button size="sm" onClick={handleAdd} disabled={!newQuestion.trim()}><Check className="h-4 w-4" /></Button>
<Button size="sm" onClick={handleAdd} onMouseDown={(e) => e.preventDefault()} disabled={!newQuestion.trim()}><Check className="h-4 w-4" /></Button>
</div>
</div>
)}
@@ -71,7 +71,7 @@ export function EscalateToCeoDialog({
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
<Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleConfirm} disabled={!reason.trim() || isPending}>
@@ -133,7 +133,7 @@ export function CeoRejectDialog({
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
<Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel
</Button>
<Button
@@ -232,7 +232,7 @@ export function CeoApproveDialog({
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="ceo-approve-notes">Approval notes</Label>
<Label htmlFor="ceo-approve-notes">Notes required</Label>
<Textarea
id="ceo-approve-notes"
value={notes}
@@ -246,7 +246,7 @@ export function CeoApproveDialog({
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
<Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleConfirm} disabled={tooShort || isPending}>
@@ -325,7 +325,7 @@ export function RequiredNotesDialog({
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
<Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel
</Button>
<Button
@@ -398,7 +398,7 @@ export function CreateBranchDialog({
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
<Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleConfirm} disabled={isPending}>
@@ -480,7 +480,7 @@ export function CreatePRDialog({
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
<Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleConfirm} disabled={!title.trim() || isPending}>
@@ -297,6 +297,12 @@ export function TaskHeader({ task, onAction }: TaskHeaderProps) {
case TaskStatus.CANCELLED:
actions.push({ label: "Reopen Task", action: "reopen", icon: <Play className="h-4 w-4 mr-2" /> });
break;
case TaskStatus.BACKLOG:
actions.push({ label: "Activate Task", action: "activate", icon: <Play className="h-4 w-4 mr-2" /> });
break;
case TaskStatus.NEEDS_REVISION:
actions.push({ label: "Start Revision", action: "start-revision", icon: <Play className="h-4 w-4 mr-2" /> });
break;
}
// Cancel is always available for non-terminal states
+6 -6
View File
@@ -47,10 +47,10 @@ const priorityColors: Record<number, string> = {
};
const priorityLabels: Record<number, string> = {
0: "P0",
1: "P1",
2: "P2",
3: "P3",
0: "P0 - Highest",
1: "P1 - High",
2: "P2 - Medium",
3: "P3 - Low",
};
// Sorting types - exported for parent components
@@ -514,8 +514,8 @@ export function TaskTable({
{task.team.replace(/_/g, " ")}
</TableCell>
<TableCell className="whitespace-nowrap">
<Badge className={priorityColors[task.priority] ?? priorityColors[2]}>
{priorityLabels[task.priority] ?? "P2"}
<Badge className={(priorityColors[task.priority] ?? priorityColors[2]) + " text-xs"}>
{priorityLabels[task.priority] ?? "P2 - Medium"}
</Badge>
</TableCell>
<TableCell className="whitespace-nowrap">