[21e195cd] Panel-wide UI standardization and usability pass (#194)

* [4c179e3a] Add git pull, fetch, and rebase backend endpoints (#190)

* [f966f772] feat(git): add pull, fetch, and rebase endpoints with integration tests (#185)

- Add GitPullRequest/Response, GitFetchRequest/Response, GitRebaseRequest/Response schemas
- Add GitService.pull(), fetch(), and rebase() methods using _network_git_timeout()
- Add POST /api/git/pull, /api/git/fetch, /api/git/rebase route handlers
- Rebase detects conflicts via git diff --name-only --diff-filter=U and aborts cleanly
- Integration tests cover success path and GitCommandError→500 for all three endpoints
- Rebase conflict test verifies conflict=True with populated conflicted_files list

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>

* [26e2b7af] test(git): add AsyncMock unit tests for rebase_onto_base conflict-state handling (#186)

New test_git_rebase.py covers three branches of rebase_onto_base:
- success path: rebase exits 0, returns rebased status, abort never called
- conflict path: non-zero exit → diff → abort → returns conflict+files
- resilience: both rebase and abort exit non-zero, still returns conflict dict without exception

All tests use AsyncMock with side_effect sequences to mock _run_git at the service-method level.

Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>

---------

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>

* [551b1dbf] Panel-wide frontend UI standardization and page fixes (#193)

* [1ec787b2] feat(panel): design-system sweep — full-width layouts, scrollbar fix, Secretary button, component audit (#188)

- Settings page: remove max-w-3xl, wrap cards in grid-cols-1 lg:grid-cols-2 two-column layout
- AI Providers page: remove max-w-5xl so AIRoutingCard fills available width
- Journals AgentList: replace ScrollArea with overflow-y-auto div to eliminate nested scrollbar
- Secretary chat input: add items-stretch to flex row so Send/Start button matches Textarea height
- Component audit: replace all raw <button>/<input>/hand-rolled badge spans outside components/ui/ with canonical Button, Checkbox, Badge variants across 15 files:
  - ai-routing-card.tsx: ModeButton → Button, checkbox → Checkbox, badge spans → Badge
  - self-hosted-section.tsx: eye-toggle → Button ghost icon-sm, badge spans → Badge
  - journals/agent-item.tsx, communications/channel-item.tsx → Button ghost
  - kb-search-bar.tsx, kb-filters.tsx → Button ghost
  - kb-category-nav.tsx, git-log-panel.tsx → Button ghost
  - communications/page.tsx (channel + group lists) → Button ghost
  - projects/project-table.tsx, products/product-table.tsx → Button link
  - git-branch-panel.tsx (local + remote lists) → Button ghost
  - tasks/dependency-selector.tsx: Button ghost + Checkbox for visual indicator
  - tasks/task-table.tsx: sortable header + expand toggle → Button ghost
  - business/goals-tab.tsx: hidden button → Button

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

* [435b37b4] feat(metrics,notifications): URL-persisted tab state, semantic chart colors, humanized counts (#187)

- Notifications page: replace useState with useSearchParams/useRouter for
  ?tab= URL parameter (all/unread/pending, default: unread); Suspense wrapper
  with skeleton fallback for SSR compatibility.

- Metrics page: split into Performance tab (Velocity + Task Status + Agent
  Status + Team Health) and Token Usage tab (TokenUsageCostsSection) with
  ?tab= URL parameter (default: performance); Suspense wrapper; Refresh button
  moved inside PerformanceTabContent; humanizeCount() helper applies K/M
  suffixes to all MetricCard numeric values >= 1000.

- Chart components (usage-time-series, agent-usage, team-usage, model-donut):
  replace var(--chart-N) CSS vars with explicit semantic hex colors —
  #3b82f6 blue for informational, #f59e0b amber for warning/pending,
  #22c55e green for success/healthy, #ef4444 red for error/blocked,
  #a855f7 purple for supplemental.

pnpm lint and pnpm typecheck pass with zero new errors.

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

* [ccd256f4] Kanban mobile viewport: 375px layout, column navigation, 44px touch targets (#191)

* [ccd256f4] feat(kanban): mobile 375px layout with column navigator and 44px touch targets

- KanbanBoard: add activeColumnIndex state + mobile prev/next column
  navigator (lg:hidden); existing horizontal-scroll layout hidden on
  mobile (hidden lg:flex). Desktop DnD behavior unchanged.
- KanbanColumn: add optional className prop (cn-based) so mobile view
  can pass w-full/sm:w-full to fill the viewport.
- KanbanCard: bump all action buttons to min-h-11 (44px) touch targets
  (Assign, Pass, Fail, Move-forward).

* [ccd256f4] fix(kanban): change breakpoint from lg to sm for mobile/desktop layout switch

AC3 requires >=640px viewport to show multi-column layout (sm: breakpoint).
Previous impl used lg: (1024px), leaving 640-1023px in single-column mode.

Change:
- Mobile navigator div: lg:hidden → sm:hidden
- Desktop multi-column div: hidden lg:flex → hidden sm:flex

At <640px: single-column with prev/next navigator (375px mobile use case).
At >=640px: full horizontal-scroll multi-column layout (per AC3).
DnD behavior and all other layout unchanged.

---------

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

* [23f02af4] Agents page On-Demand section + Board composition; Overview Quick Actions visibility + Team Health Intake/Secretary (#189)

* [23f02af4] feat(agents,overview): On-Demand section, Board composition fix, Intake/Secretary in Quick Actions + Team Health

- agent-definitions.ts: remove AgentRole.MAIN_PM from getBoardAgents
  (Main PM has its own dedicated section; including it there was redundant).
  Add getOnDemandAgents() that catches agents not in any standard team
  (board/main_pm/backend/frontend/ux_ui/marketing) and not a standard cell
  role — surfaces prompter/intake agents that the API may return.

- agents/page.tsx: import getOnDemandAgents; add a conditional
  'On-Demand Agents' AgentGrid section (only rendered when the API returns
  at least one matching agent, e.g. the Intake interviewer).

- quick-actions-bar.tsx: add 'Task Intake' button (→/prompter, Sparkles
  icon) and 'Secretary' button (→/business?tab=secretary, Bot icon)
  alongside existing quick actions so operators can reach on-demand agents
  from the Overview in one click.

- team-health-cards.tsx: add OnDemandAgentCard sub-component (link card
  with On-Demand badge) and render static cards for 'Task Intake' and
  'Secretary' appended after the API-driven TeamHealthCard list, giving
  them equal visual presence in the Team Health section.

pnpm lint and pnpm typecheck pass with zero new errors.

* [23f02af4] fix(agents,overview): QA revision — enum entries, QuickActions placement, On-Demand title, Board PR_REVIEWER

AC3: types/index.ts AgentRole enum adds PR_REVIEWER, PROMPTER, SECRETARY.
     agent-selector.tsx ROLE_LABELS exhaustive Record updated accordingly.

AC4: command-center.tsx QuickActionsBar moved to after Team Health section,
     before CEO Approval Queue and data-heavy grid rows — visible without
     scrolling on a 900px viewport.

AC1: agents/page.tsx On-Demand AgentGrid title fixed to 'On-Demand'
     (was 'On-Demand Agents' in prior commit).

AC2: agent-definitions.ts getBoardAgents adds explicit PR_REVIEWER inclusion
     and uses inclusion-based getOnDemandAgents (PROMPTER|SECRETARY roles).

AC5: team-health-cards.tsx static OnDemandAgentCard implementation refined
     with correct fallback rendering when no API team data.

AC6: pnpm lint and pnpm typecheck (src only) pass with zero new errors.

---------

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

* [b1c59206] Git page: Pull, Fetch, Rebase buttons wired to backend; Rebase destructive confirmation dialog (#192)

* [b1c59206] feat(git): add Pull, Fetch, Rebase operations to Git page with destructive confirmation dialog for Rebase

- Add GitPullRequest/Response, GitFetchRequest/Response, GitRebaseRequest/Response types
- Add gitApi.pull(), gitApi.fetch(), gitApi.rebase() with mock stubs for /git/pull, /git/fetch, /git/rebase
- Add useGitPull, useGitFetch, useGitRebase mutation hooks with cache invalidation; exported via useGitOperations
- Add Pull (Download icon), Fetch (RefreshCcw icon), Rebase (GitGraph icon) buttons to GitActionsPanel
- Rebase button triggers AlertDialog with destructive confirmation before calling API
- Wire handlePull, handleFetch, handleRebase handlers in git-browser.tsx with toast feedback

* [b1c59206] fix(git): add destructive styling and branch name to Rebase AlertDialog

- Add className='border-destructive bg-destructive/5' to AlertDialogContent
  so the dialog container has the required red-tinted styling (AC3)
- Update AlertDialogDescription to interpolate status?.current_branch so
  the dialog body explicitly names the branch being rebased (AC3)

---------

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>

* [3f305ed9] Frontend: Fix git control contract, complete Secretary restyling, and apply polish (CEO revision) (#199)

* [72de8a65] fix(git): correct Pull/Fetch/Rebase types, API mocks, request fields, and toast handlers (#197)

- types/git.ts: GitPullResponse and GitFetchResponse now have current_branch,
  has_changes, staged_files, unstaged_files, untracked_files, ahead, behind
  (matching backend GitStatusResponse); removed nonexistent commits_received/
  refs_updated/remote fields
- types/git.ts: GitRebaseRequest now uses target_branch: string (not onto?: string);
  GitRebaseResponse now has conflict: boolean and conflicted_files: string[]
  (removed branch/onto/commits_rebased); task_id made optional on all three
  request types
- lib/api/git.ts: Updated mock returns for pull/fetch/rebase to match new types
- git-actions-panel.tsx: onRebase prop now (targetBranch: string) => void;
  Rebase AlertDialog now contains an Input for target_branch; AlertDialogAction
  disabled when targetBranch empty and passes the value to onRebase
- git-browser.tsx: handlePull and handleFetch toast references result.current_branch;
  handleRebase accepts targetBranch, sends target_branch in payload, toasts
  result.conflict and result.conflicted_files; no 'manual' task_id for any
  pull/fetch/rebase operation

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

* [be6a17fc] feat(ui): design-system polish — chart tokens, KB aria-label, Kanban touch targets (#196)

- kb-search-bar.tsx: add aria-label="Clear search" to the clear (X) button
- model-usage-donut.tsx: replace hex CHART_COLORS with var(--chart-1)…var(--chart-5)
- usage-time-series-chart.tsx: replace hex stopColor/stroke with var(--chart-1)/var(--chart-2)
- agent-usage-chart.tsx: Bar fill hex → var(--chart-1)
- team-usage-chart.tsx: Bar fill hex → var(--chart-1)
- kanban-card.tsx: min-h-11 → max-sm:min-h-11 (44px touch target mobile-only, 3 buttons)
- secretary-tab.tsx: already compliant (Button + design-system tokens), no change needed

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>

* [d62036bd] Backend: Fix git endpoint schemas, add safety gates, and unit tests (CEO revision) (#200)

* [d0593fe3] feat(git): remove agent_id from schemas and add service-layer safety gates (#195)

- Remove agent_id field from all 9 git request schemas (GitCreateBranchRequest, GitCheckoutRequest, GitCommitRequest, GitPushRequest, GitCreatePRRequest, GitMergePRRequest, GitPullRequest, GitFetchRequest, GitRebaseRequest); agent identity comes from JWT auth context
- Make task_id Optional[UUID]=None in GitPullRequest, GitFetchRequest, GitRebaseRequest
- Add field_validator to GitRebaseRequest rejecting target_branch starting with '-' or equal to 'master'/'main'
- Add lightweight PullRequest, FetchRequest, RebaseRequest schemas for gateway layer
- Add dirty-workspace check to GitService.pull() (raises ValidationError if porcelain output)
- Switch GitService.pull() to --ff-only; raises ValidationError with diverged-branch message on non-zero exit
- Add master/main guard to GitService.rebase() for both head_branch and target_branch
- Update callers: routes/tasks.py (2x), services/task.py, tests/unit/services/test_git.py (2x)

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>

* [a2f96961] Add role-gated rebase endpoint and unit tests (test_git_rebase.py) (#198)

* [a2f96961] feat(git): add role-gated rebase endpoint and unit tests

Add role-gate to POST /rebase restricting access to DEVELOPER and
CELL_PM roles; add master/main protected-branch guard to
GitService.rebase() before any git subprocess runs; add 4 unit tests
in tests/unit/services/test_git_rebase.py covering both
target-branch and head-branch REBASE_FORBIDDEN cases

* [a2f96961] fix(git): invert rebase role gate, add ownership check, schema validator, and missing tests

- _REBASE_ALLOWED_ROLES changed from {DEVELOPER, CELL_PM} to {CEO, CELL_PM, MAIN_PM}
  so developers correctly receive 403 per AC1/AC2
- rebase_branch() now verifies task ownership for non-CEO PM callers: if task_id
  is provided and the task is not assigned to the calling agent, returns 403/404
- GitRebaseRequest.target_branch gets a @field_validator rejecting '-' prefix
  names and protected branch names (main, master, develop)
- GitService.pull() gains pre-flight safety gates: raises ValidationError
  DIRTY_TREE when staged/unstaged changes exist, DIVERGED_BRANCH when
  ahead > 0 and behind > 0
- test_git_rebase.py adds 9 new tests: pull() dirty-tree ValidationError,
  pull() diverged-branch ValidationError, pull() success path, schema
  validator for '-' prefix and protected names, and route-level tests
  confirming HTTP 403 for DEVELOPER and HTTP 200 for CELL_PM on POST /rebase

* [a2f96961] fix(tests): add type annotations for tuple variables in test_git_rebase.py

mypy needs explicit tuple type annotations when assigning bare tuples
to variables used as mock side_effect return values — fixes var-annotated
error caught by the server-side quality gate

---------

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

---------

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

* [94015c6d] Frontend R3: Fix legacy git taskId coercion + rebase placeholder + phantom fields (#204)

* [401ddb40] fix(git): remove phantom fields from GitPullRequest/GitFetchRequest and make task_id optional in write request interfaces; use taskId || undefined in git-browser.tsx handlers to avoid 422 errors when no task context is active (#201)

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

* [cca8d0c0] fix(git): fix rebase placeholder and surface backend error in toast (#202)

git-actions-panel.tsx: change rebase target_branch Input placeholder
from "e.g. main or origin/main" to "Remote ref (e.g. origin/HEAD)" so
no default branch name (main/master/develop) is suggested.

git-browser.tsx: import getErrorMessage from @/lib/api/client and use
it in handleRebase catch block instead of the hardcoded string "Failed
to rebase". getErrorMessage extracts the real detail from
AxiosError.response.data.detail and falls back to a non-empty generic
message, satisfying both the detail-surfacing and fallback criteria.

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>

* [1ea0fbcb] Backend R3: Relax legacy git schemas + fix integration tests (#206)

* [219c539b] Make task_id Optional in git request schemas and update service methods (#205)

* [219c539b] feat(git): make task_id Optional in git schemas and add None-guards in service methods

- GitCommitRequest, GitPushRequest, GitCreatePRRequest, GitMergePRRequest now have task_id: UUID | None = None
- commit_for_task, push_for_task, create_pr_for_task, merge_pr_for_task skip ownership/state checks when task_id is None and proceed to the git operation
- Added 16 unit tests in tests/unit/api/routes/test_git_optional_task_id.py covering schema validation and HTTP endpoint responses
- Added 4 integration tests in tests/integration/test_git_routes.py for no-422 behaviour
- All quality gates pass: ruff format, ruff check, mypy, pytest

* [219c539b] fix(tests): remove unused type-ignore comments, redundant cast, and invalid agent_id kwarg in git_optional_task_id unit tests

---------

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>

* [de95ce94] test(git): fix 3 rebase integration tests to use non-protected target_branch (#203)

- Add pm_git_client fixture (CELL_PM role) needed for the role-gated rebase endpoint
- Change target_branch from 'main' to 'develop' in test_rebase_success, test_rebase_conflict, and test_rebase_git_command_error
- Remove task_id from request bodies (optional field; random UUIDs trigger 404)
- Switch all 3 rebase tests to use pm_git_client instead of git_client

Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>

---------

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>

* chore: ruff format test_agent_image_registry.py (unblock quality gate)

---------

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Backend Developer 2 <be-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>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-06-17 06:36:28 +02:00
committed by GitHub
co-authored by Backend Developer 2 Backend Developer 1 Frontend Developer 1 Frontend Developer 2 Renn F
parent e71a746499
commit 818f2ac7a6
48 changed files with 2867 additions and 575 deletions
@@ -33,11 +33,14 @@ const ROLE_LABELS: Record<AgentRole, string> = {
[AgentRole.PRODUCT_OWNER]: "Product Owner",
[AgentRole.HEAD_MARKETING]: "Head Marketing",
[AgentRole.AUDITOR]: "Auditor",
[AgentRole.PR_REVIEWER]: "PR Reviewer",
[AgentRole.MAIN_PM]: "Main PM",
[AgentRole.CELL_PM]: "Cell PM",
[AgentRole.DEVELOPER]: "Developer",
[AgentRole.QA]: "QA",
[AgentRole.DOCUMENTER]: "Documenter",
[AgentRole.PROMPTER]: "Prompter",
[AgentRole.SECRETARY]: "Secretary",
};
export function AgentSelector({
+2 -2
View File
@@ -341,8 +341,8 @@ function GoalsForm({ goals, refetch }: GoalsFormProps) {
)}
</div>
{/* Hidden — just to make the refetch prop used */}
<button type="button" className="hidden" onClick={refetch} />
{/* Hidden — keeps the refetch prop wired up for future use */}
<Button type="button" className="hidden" onClick={refetch} />
</CardContent>
</Card>
);
@@ -245,7 +245,7 @@ export function SecretaryTab() {
</CardHeader>
<CardContent className="flex flex-1 flex-col gap-4">
<ChatMessages messages={messages} streaming={streaming} />
<div className="flex gap-2">
<div className="flex items-stretch gap-2">
<Textarea
value={input}
onChange={(e) => setInput(e.target.value)}
@@ -2,6 +2,7 @@
import { Channel } from "@/types";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Hash, Lock } from "lucide-react";
import { cn } from "@/lib/utils";
@@ -19,12 +20,13 @@ export function ChannelItem({
unreadCount = 0,
}: ChannelItemProps) {
return (
<button
<Button
onClick={onClick}
variant="ghost"
className={cn(
"w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-left transition-colors",
"w-full h-auto justify-start gap-2 px-2 py-1.5 font-normal whitespace-normal",
isSelected
? "bg-primary/10 text-primary"
? "bg-primary/10 text-primary hover:bg-primary/10 hover:text-primary"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
)}
>
@@ -39,6 +41,6 @@ export function ChannelItem({
{unreadCount}
</Badge>
)}
</button>
</Button>
);
}
@@ -72,6 +72,14 @@ export function CommandCenter() {
/>
</section>
{/* Quick Actions — placed immediately after Team Health so it is
visible without scrolling on a 900px-tall viewport, before the
data-heavy grid rows below. */}
<section>
<h2 className="text-lg font-semibold mb-4">Quick Actions</h2>
<QuickActionsBar />
</section>
{/* CEO Approval Queue + Strategy Signals - side-by-side on lg+ */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<CeoApprovalQueue />
@@ -99,12 +107,6 @@ export function CommandCenter() {
isLoading={loadingActivity}
/>
</div>
{/* Quick Actions */}
<section className="pt-4 border-t">
<h2 className="text-lg font-semibold mb-4">Quick Actions</h2>
<QuickActionsBar />
</section>
</div>
);
}
@@ -2,7 +2,7 @@
import { Button } from "@/components/ui/button";
import { CreateTaskDialog } from "@/components/tasks/create-task-dialog";
import { Users, Megaphone, BookOpen, Shield } from "lucide-react";
import { Users, Megaphone, BookOpen, Shield, Sparkles, Bot } from "lucide-react";
import Link from "next/link";
export function QuickActionsBar() {
@@ -17,6 +17,20 @@ export function QuickActionsBar() {
</Button>
</Link>
<Link href="/prompter">
<Button variant="outline">
<Sparkles className="h-4 w-4 mr-2" />
Task Intake
</Button>
</Link>
<Link href="/business?tab=secretary">
<Button variant="outline">
<Bot className="h-4 w-4 mr-2" />
Secretary
</Button>
</Link>
<Link href="/communications">
<Button variant="outline">
<Megaphone className="h-4 w-4 mr-2" />
@@ -3,12 +3,43 @@
import { TeamHealth } from "@/types";
import { TeamHealthCard } from "./team-health-card";
import { Skeleton } from "@/components/ui/skeleton";
import { Sparkles, Bot } from "lucide-react";
import Link from "next/link";
interface TeamHealthCardsProps {
teams: TeamHealth[] | undefined;
isLoading: boolean;
}
/** Static link-card for on-demand agents (Intake, Secretary) that are not
* part of any standing team and therefore never appear in the API health data. */
function OnDemandAgentCard({
title,
href,
icon: Icon,
description,
}: {
title: string;
href: string;
icon: React.ElementType;
description: string;
}) {
return (
<Link href={href} className="block">
<div className="rounded-lg border bg-card p-4 hover:bg-accent/50 transition-colors h-full flex flex-col gap-2">
<div className="flex items-center gap-2">
<Icon className="h-4 w-4 text-muted-foreground" />
<span className="font-medium text-sm">{title}</span>
<span className="ml-auto text-xs rounded-full bg-secondary px-2 py-0.5 text-secondary-foreground">
On-Demand
</span>
</div>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
</Link>
);
}
export function TeamHealthCards({ teams, isLoading }: TeamHealthCardsProps) {
if (isLoading) {
return (
@@ -20,19 +51,33 @@ export function TeamHealthCards({ teams, isLoading }: TeamHealthCardsProps) {
);
}
if (!teams || teams.length === 0) {
return (
<div className="text-center py-8 text-muted-foreground">
No team health data available
</div>
);
}
const hasTeams = teams && teams.length > 0;
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-6 gap-4">
{teams.map((health) => (
<TeamHealthCard key={health.team} health={health} />
))}
{hasTeams ? (
teams.map((health) => (
<TeamHealthCard key={health.team} health={health} />
))
) : (
<div className="col-span-full text-center py-8 text-muted-foreground">
No team health data available
</div>
)}
{/* Static on-demand agent cards — always visible regardless of API data */}
<OnDemandAgentCard
title="Task Intake"
href="/prompter"
icon={Sparkles}
description="Intake interviewer — chat with CEO to draft and submit new tasks"
/>
<OnDemandAgentCard
title="Secretary"
href="/business?tab=secretary"
icon={Bot}
description="Secretary agent — manage business goals, pitches, and notes"
/>
</div>
);
}
@@ -15,6 +15,17 @@ import {
DialogFooter,
DialogTrigger,
} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import {
GitCommit,
Upload,
@@ -22,6 +33,9 @@ import {
GitMerge,
RefreshCw,
ArrowUp,
Download,
RefreshCcw,
GitGraph,
} from "lucide-react";
interface GitActionsPanelProps {
@@ -33,10 +47,16 @@ interface GitActionsPanelProps {
onPush: (force?: boolean) => void;
onCreatePR: (title: string, body: string) => void;
onMergePR: (prNumber: number) => void;
onPull: () => void;
onFetch: () => void;
onRebase: (targetBranch: string) => void;
isCommitting: boolean;
isPushing: boolean;
isCreatingPR: boolean;
isMerging: boolean;
isPulling: boolean;
isFetching: boolean;
isRebasing: boolean;
}
export function GitActionsPanel({
@@ -48,10 +68,16 @@ export function GitActionsPanel({
onPush,
onCreatePR,
onMergePR,
onPull,
onFetch,
onRebase,
isCommitting,
isPushing,
isCreatingPR,
isMerging,
isPulling,
isFetching,
isRebasing,
}: GitActionsPanelProps) {
void _agentId; // Reserved for future use
const [showCommitDialog, setShowCommitDialog] = useState(false);
@@ -61,6 +87,7 @@ export function GitActionsPanel({
const [prTitle, setPrTitle] = useState("");
const [prBody, setPrBody] = useState("");
const [mergePrNumber, setMergePrNumber] = useState("");
const [rebaseTargetBranch, setRebaseTargetBranch] = useState("");
const hasStagedChanges = (status?.staged_files.length ?? 0) > 0;
const hasUnpushedCommits = (status?.ahead ?? 0) > 0;
@@ -302,6 +329,88 @@ export function GitActionsPanel({
</DialogContent>
</Dialog>
{/* Pull Action */}
<Button
className="w-full justify-start"
variant="outline"
disabled={isPulling}
onClick={onPull}
>
{isPulling ? (
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
) : (
<Download className="h-4 w-4 mr-2" />
)}
Pull from Remote
</Button>
{/* Fetch Action */}
<Button
className="w-full justify-start"
variant="outline"
disabled={isFetching}
onClick={onFetch}
>
{isFetching ? (
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
) : (
<RefreshCcw className="h-4 w-4 mr-2" />
)}
Fetch Remote
</Button>
{/* Rebase Action — destructive, requires confirmation */}
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
className="w-full justify-start"
variant="outline"
disabled={isRebasing}
>
{isRebasing ? (
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
) : (
<GitGraph className="h-4 w-4 mr-2" />
)}
Rebase onto Remote
</Button>
</AlertDialogTrigger>
<AlertDialogContent className="border-destructive bg-destructive/5">
<AlertDialogHeader>
<AlertDialogTitle>Rebase onto target branch?</AlertDialogTitle>
<AlertDialogDescription>
This will rewrite the commit history of branch{" "}
<strong>{status?.current_branch}</strong> by replaying commits
on top of the specified target branch. A force-push will be
required afterward. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="px-6 py-2 space-y-2">
<label className="text-sm font-medium">Target branch</label>
<Input
placeholder="Remote ref (e.g. origin/HEAD)"
value={rebaseTargetBranch}
onChange={(e) => setRebaseTargetBranch(e.target.value)}
/>
</div>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setRebaseTargetBranch("")}>
Cancel
</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
disabled={!rebaseTargetBranch.trim()}
onClick={() => {
onRebase(rebaseTargetBranch.trim());
setRebaseTargetBranch("");
}}
>
Rebase
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Status Summary */}
{status && (
<div className="pt-2 border-t text-xs text-muted-foreground space-y-1">
@@ -164,14 +164,15 @@ export function GitBranchPanel({
</h4>
<div className="space-y-0.5">
{localBranches.map((branch) => (
<button
<Button
key={branch.name}
onClick={() => !branch.is_current && onCheckout(branch.name)}
disabled={branch.is_current || isCheckingOut}
variant="ghost"
className={
"w-full flex items-center justify-between px-2 py-1.5 rounded text-sm text-left transition-colors " +
"w-full h-auto justify-between px-2 py-1.5 text-sm font-normal whitespace-normal " +
(branch.is_current
? "bg-primary/10 text-primary"
? "bg-primary/10 text-primary hover:bg-primary/10 hover:text-primary"
: "hover:bg-muted")
}
>
@@ -188,7 +189,7 @@ export function GitBranchPanel({
{branch.last_commit.slice(0, 7)}
</span>
)}
</button>
</Button>
))}
</div>
</div>
@@ -202,16 +203,17 @@ export function GitBranchPanel({
</h4>
<div className="space-y-0.5">
{remoteBranches.map((branch) => (
<button
<Button
key={branch.name}
onClick={() => onCheckout(branch.name)}
disabled={isCheckingOut}
className="w-full flex items-center justify-between px-2 py-1.5 rounded text-sm text-left transition-colors hover:bg-muted"
variant="ghost"
className="w-full h-auto justify-start px-2 py-1.5 text-sm font-normal whitespace-normal hover:bg-muted"
>
<span className="truncate font-mono text-xs text-muted-foreground">
{branch.name}
</span>
</button>
</Button>
))}
</div>
</div>
+56 -5
View File
@@ -29,6 +29,7 @@ import { GitDiffViewer } from "./git-diff-viewer";
import { GitActionsPanel } from "./git-actions-panel";
import { GitBranch, RefreshCw, FolderGit2 } from "lucide-react";
import { toast } from "sonner";
import { getErrorMessage } from "@/lib/api/client";
function GitBrowserContent() {
const router = useRouter();
@@ -49,7 +50,7 @@ function GitBrowserContent() {
const { data: unstagedDiff, isLoading: loadingUnstagedDiff } = useGitDiff(projectSlug, false, undefined, !!projectSlug);
// Git operations
const { commit, push, createBranch, checkout, createPR, mergePR } = useGitOperations();
const { commit, push, createBranch, checkout, createPR, mergePR, pull, fetch, rebase } = useGitOperations();
// Update URL params
const updateParams = useCallback(
@@ -115,7 +116,7 @@ function GitBrowserContent() {
const result = await commit.mutateAsync({
project_slug: projectSlug,
message,
task_id: taskId || "manual",
task_id: taskId || undefined,
agent_id: "ceo",
});
toast.success(`Committed: ${result.commit_hash.slice(0, 7)}`);
@@ -128,7 +129,7 @@ function GitBrowserContent() {
try {
const result = await push.mutateAsync({
project_slug: projectSlug,
task_id: taskId || "manual",
task_id: taskId || undefined,
agent_id: "ceo",
force,
});
@@ -142,7 +143,7 @@ function GitBrowserContent() {
try {
const result = await createPR.mutateAsync({
project_slug: projectSlug,
task_id: taskId || "manual",
task_id: taskId || undefined,
title,
body,
agent_id: "ceo",
@@ -165,7 +166,7 @@ function GitBrowserContent() {
const result = await mergePR.mutateAsync({
project_slug: projectSlug,
pr_number: prNumber,
task_id: taskId || "manual",
task_id: taskId || undefined,
agent_id: "ceo",
});
toast.success(`Merged PR #${result.pr_number}${result.target_branch}`);
@@ -174,6 +175,50 @@ function GitBrowserContent() {
}
};
const handlePull = async () => {
try {
const result = await pull.mutateAsync({
project_slug: projectSlug,
task_id: taskId || undefined,
});
toast.success(`Pulled: now on ${result.current_branch}`);
} catch {
toast.error("Failed to pull from remote");
}
};
const handleFetch = async () => {
try {
const result = await fetch.mutateAsync({
project_slug: projectSlug,
task_id: taskId || undefined,
});
toast.success(`Fetched: now on ${result.current_branch}`);
} catch {
toast.error("Failed to fetch from remote");
}
};
const handleRebase = async (targetBranch: string) => {
try {
const result = await rebase.mutateAsync({
project_slug: projectSlug,
target_branch: targetBranch,
task_id: taskId || undefined,
agent_id: "ceo",
});
if (result.conflict) {
toast.warning(
`Rebase conflicts in: ${result.conflicted_files.join(", ") || "unknown files"}`
);
} else {
toast.success("Rebase completed successfully");
}
} catch (error) {
toast.error(getErrorMessage(error));
}
};
// Check offline
const isOffline = projectsError && (
projectsError.message?.includes("Network Error") ||
@@ -258,10 +303,16 @@ function GitBrowserContent() {
onPush={handlePush}
onCreatePR={handleCreatePR}
onMergePR={handleMergePR}
onPull={handlePull}
onFetch={handleFetch}
onRebase={handleRebase}
isCommitting={commit.isPending}
isPushing={push.isPending}
isCreatingPR={createPR.isPending}
isMerging={mergePR.isPending}
isPulling={pull.isPending}
isFetching={fetch.isPending}
isRebasing={rebase.isPending}
/>
</div>
+9 -8
View File
@@ -3,8 +3,10 @@
import { GitLogResponse, CommitInfo } from "@/types/git";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { GitCommit, User, Calendar } from "lucide-react";
import { formatDistanceToNow } from "date-fns";
@@ -68,15 +70,14 @@ export function GitLogPanel({
<ScrollArea className="h-80">
<div className="p-4 space-y-0">
{log.commits.map((commit, index) => (
<button
<Button
key={commit.hash}
onClick={() => onSelectCommit?.(commit)}
className={
"w-full text-left p-3 rounded-lg transition-colors relative " +
(selectedHash === commit.hash
? "bg-primary/10"
: "hover:bg-muted")
}
variant="ghost"
className={cn(
"w-full h-auto justify-start text-left p-3 font-normal whitespace-normal relative",
selectedHash === commit.hash ? "bg-primary/10 hover:bg-primary/10" : ""
)}
>
{/* Timeline line */}
{index < log.commits.length - 1 && (
@@ -123,7 +124,7 @@ export function GitLogPanel({
</div>
</div>
</div>
</button>
</Button>
))}
</div>
</ScrollArea>
+6 -4
View File
@@ -4,6 +4,7 @@ import { Agent } from "@/types";
import { User } from "lucide-react";
import { cn } from "@/lib/utils";
import { getAgentDisplayName } from "@/lib/agent-utils";
import { Button } from "@/components/ui/button";
interface AgentItemProps {
agent: Agent;
@@ -14,12 +15,13 @@ interface AgentItemProps {
export function AgentItem({ agent, isSelected, onClick, hasEntries }: AgentItemProps) {
return (
<button
<Button
onClick={onClick}
variant="ghost"
className={cn(
"w-full flex items-center gap-3 p-2 rounded-lg text-left transition-colors",
"w-full h-auto justify-start gap-3 p-2 font-normal whitespace-normal",
isSelected
? "bg-primary/10 border border-primary/30"
? "bg-primary/10 border border-primary/30 hover:bg-primary/10"
: "hover:bg-muted/50"
)}
>
@@ -37,6 +39,6 @@ export function AgentItem({ agent, isSelected, onClick, hasEntries }: AgentItemP
{agent.role.replace(/_/g, " ")}
</p>
</div>
</button>
</Button>
);
}
+2 -3
View File
@@ -1,7 +1,6 @@
"use client";
import { Agent, Team, AgentRole } from "@/types";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Skeleton } from "@/components/ui/skeleton";
import { AgentItem } from "./agent-item";
@@ -84,7 +83,7 @@ export function AgentList({
const grouped = groupByTeam(agents);
return (
<ScrollArea className="h-[calc(100vh-200px)]">
<div className="overflow-y-auto max-h-[calc(100vh-200px)]">
<div className="p-2 space-y-4">
{Object.entries(grouped).map(([teamKey, teamAgents]) => {
if (teamAgents.length === 0) return null;
@@ -108,6 +107,6 @@ export function AgentList({
);
})}
</div>
</ScrollArea>
</div>
);
}
@@ -11,7 +11,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { RefreshCw } from "lucide-react";
import { ChevronLeft, ChevronRight, RefreshCw } from "lucide-react";
import { toast } from "sonner";
import {
DndContext,
@@ -64,6 +64,7 @@ export function KanbanBoard({
const updateTask = useUpdateTask();
const [activeTask, setActiveTask] = useState<Task | null>(null);
const [pendingNotesAction, setPendingNotesAction] = useState<PendingNotesAction | null>(null);
const [activeColumnIndex, setActiveColumnIndex] = useState(0);
const sensors = useSensors(
useSensor(PointerSensor, {
@@ -293,7 +294,56 @@ export function KanbanBoard({
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="flex gap-4 overflow-x-auto pb-4">
{/* Mobile: single-column view with prev/next navigator (hidden on sm+) */}
<div className="sm:hidden">
<div className="flex items-center gap-2 mb-4">
<Button
variant="outline"
size="icon"
className="min-h-11 min-w-11 shrink-0"
onClick={() => setActiveColumnIndex((i) => Math.max(0, i - 1))}
disabled={activeColumnIndex === 0}
aria-label="Previous column"
>
<ChevronLeft className="h-5 w-5" />
</Button>
<p className="flex-1 text-center text-sm font-semibold">
<span className="text-muted-foreground font-normal text-xs mr-1">
{activeColumnIndex + 1}/{columns.length}
</span>
{columns[activeColumnIndex]?.title}
</p>
<Button
variant="outline"
size="icon"
className="min-h-11 min-w-11 shrink-0"
onClick={() =>
setActiveColumnIndex((i) => Math.min(columns.length - 1, i + 1))
}
disabled={activeColumnIndex === columns.length - 1}
aria-label="Next column"
>
<ChevronRight className="h-5 w-5" />
</Button>
</div>
{columns[activeColumnIndex] && (
<KanbanColumn
key={columns[activeColumnIndex].id}
id={columns[activeColumnIndex].id}
title={columns[activeColumnIndex].title}
status={columns[activeColumnIndex].status}
tasks={tasksByStatus[columns[activeColumnIndex].status] || []}
color={columns[activeColumnIndex].color}
isLoading={isLoading}
onAction={handleAction}
showQaActions={showQaActions}
className="w-full sm:w-full"
/>
)}
</div>
{/* Desktop: horizontal scrolling layout (shown at sm+, i.e. >= 640px) */}
<div className="hidden sm:flex gap-4 overflow-x-auto pb-4">
{columns.map((col) => (
<KanbanColumn
key={col.id}
@@ -183,7 +183,7 @@ export function KanbanCard({ task, onAction, showQaActions, isDragging: isDraggi
<Button
variant="ghost"
size="sm"
className="h-7 text-muted-foreground hover:text-foreground"
className="max-sm:min-h-11 text-muted-foreground hover:text-foreground"
onClick={(e) => e.stopPropagation()}
disabled={isBacklog}
>
@@ -210,7 +210,7 @@ export function KanbanCard({ task, onAction, showQaActions, isDragging: isDraggi
<Button
variant="ghost"
size="sm"
className="h-7 text-green-600 hover:text-green-700 hover:bg-green-50"
className="max-sm:min-h-11 text-green-600 hover:text-green-700 hover:bg-green-50"
onClick={(e) => {
e.stopPropagation();
onAction("pass-qa", task.id);
@@ -222,7 +222,7 @@ export function KanbanCard({ task, onAction, showQaActions, isDragging: isDraggi
<Button
variant="ghost"
size="sm"
className="h-7 text-red-600 hover:text-red-700 hover:bg-red-50"
className="max-sm:min-h-11 text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={(e) => {
e.stopPropagation();
onAction("fail-qa", task.id);
@@ -238,7 +238,7 @@ export function KanbanCard({ task, onAction, showQaActions, isDragging: isDraggi
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
className="h-11 w-11"
onClick={(e) => {
e.stopPropagation();
onAction("move-forward", task.id);
@@ -6,6 +6,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import { ScrollArea } from "@/components/ui/scroll-area";
import { KanbanCard } from "./kanban-card";
import { useDroppable } from "@dnd-kit/core";
import { cn } from "@/lib/utils";
interface KanbanColumnProps {
id: string;
@@ -16,6 +17,8 @@ interface KanbanColumnProps {
isLoading: boolean;
onAction?: (action: string, taskId: string) => void;
showQaActions?: boolean;
/** Extra Tailwind classes forwarded to the root element (e.g. w-full for mobile). */
className?: string;
}
export function KanbanColumn({
@@ -27,6 +30,7 @@ export function KanbanColumn({
isLoading,
onAction,
showQaActions,
className,
}: KanbanColumnProps) {
void _id; // Reserved for future use
const { setNodeRef, isOver } = useDroppable({
@@ -36,9 +40,12 @@ export function KanbanColumn({
return (
<div
ref={setNodeRef}
className={`flex flex-col rounded-lg p-3 w-72 shrink-0 sm:w-80 ${color} ${
isOver ? "ring-2 ring-primary ring-offset-2" : ""
}`}
className={cn(
"flex flex-col rounded-lg p-3 w-72 shrink-0 sm:w-80",
color,
isOver && "ring-2 ring-primary ring-offset-2",
className,
)}
>
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-sm text-gray-800 dark:text-gray-100">{title}</h3>
@@ -3,6 +3,8 @@
import { KBIndexType, KBStats } from "@/types";
import { FileText, MessageSquare, BookOpen, ChevronRight, AlertTriangle, Scale, GitBranch, ClipboardCheck, Lightbulb } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
const categoryConfig: Record<KBIndexType, { label: string; description: string; icon: React.ReactNode }> = {
[KBIndexType.DOCUMENTATION]: {
@@ -90,17 +92,19 @@ export function KBCategoryNav({
const isSelected = selectedCategory === type;
return (
<button
<Button
key={type}
onClick={() => onSelectCategory(type)}
className={`w-full flex items-center gap-3 p-3 rounded-lg border transition-colors text-left ${
variant="outline"
className={cn(
"w-full h-auto justify-start gap-3 p-3 font-normal whitespace-normal",
isSelected
? "bg-primary/10 border-primary"
: "hover:bg-muted/50 border-transparent hover:border-border"
}`}
)}
>
<div className="shrink-0">{config.icon}</div>
<div className="flex-1 min-w-0">
<div className="flex-1 min-w-0 text-left">
<div className="flex items-center justify-between">
<span className="font-medium text-sm">{config.label}</span>
<span className="text-xs text-muted-foreground">
@@ -109,8 +113,8 @@ export function KBCategoryNav({
</div>
<p className="text-xs text-muted-foreground truncate">{config.description}</p>
</div>
<ChevronRight className={`h-4 w-4 text-muted-foreground shrink-0 transition-transform ${isSelected ? "rotate-90" : ""}`} />
</button>
<ChevronRight className={cn("h-4 w-4 text-muted-foreground shrink-0 transition-transform", isSelected && "rotate-90")} />
</Button>
);
})}
</div>
@@ -2,6 +2,7 @@
import { KBIndexType } from "@/types";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import { FileText, MessageSquare, BookOpen, AlertTriangle, Scale, GitBranch, ClipboardCheck, Lightbulb } from "lucide-react";
const indexTypeConfig: Record<KBIndexType, { label: string; icon: React.ReactNode }> = {
@@ -36,12 +37,14 @@ export function KBFilters({ selectedTypes, onTypesChange }: KBFiltersProps) {
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Filter by type</span>
{selectedTypes.length > 0 && (
<button
<Button
variant="ghost"
size="sm"
onClick={() => onTypesChange([])}
className="text-xs text-muted-foreground hover:text-foreground"
className="h-auto py-0 px-1 text-xs text-muted-foreground hover:text-foreground"
>
Clear
</button>
</Button>
)}
</div>
<div className="space-y-2">
@@ -64,12 +64,15 @@ export function KBSearchBar({
className="pl-9 pr-9"
/>
{localValue && (
<button
<Button
variant="ghost"
size="icon-sm"
onClick={handleClear}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label="Clear search"
className="absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="h-4 w-4" />
</button>
</Button>
)}
</div>
{onSearch && (
@@ -12,6 +12,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import type { ModelUsageSlice } from "@/types";
// Design-system chart tokens — resolves to theme-aware palette
const CHART_COLORS = [
"var(--chart-1)",
"var(--chart-2)",
@@ -66,7 +66,7 @@ export function TeamUsageChart({ data, isLoading }: TeamUsageChartProps) {
]}
contentStyle={{ fontSize: 12 }}
/>
<Bar dataKey="Tokens" fill="var(--chart-2)" radius={[3, 3, 0, 0]} />
<Bar dataKey="Tokens" fill="var(--chart-1)" radius={[3, 3, 0, 0]} />
</BarChart>
</ResponsiveContainer>
)}
@@ -60,12 +60,13 @@ export function ProductTable({ products, isLoading }: ProductTableProps) {
<TableRow key={product.id}>
<TableCell>
<div>
<button
<Button
onClick={() => setEditingProductId(product.id)}
className="font-medium hover:underline text-left"
variant="link"
className="h-auto p-0 font-medium text-foreground"
>
{product.name}
</button>
</Button>
<p className="text-xs text-muted-foreground font-mono">{product.slug}</p>
</div>
</TableCell>
@@ -129,12 +129,13 @@ export function ProjectTable({ projects, isLoading }: ProjectTableProps) {
<TableRow key={project.id}>
<TableCell>
<div>
<button
<Button
onClick={() => setEditingProjectId(project.id)}
className="font-medium hover:underline text-left"
variant="link"
className="h-auto p-0 font-medium text-foreground"
>
{project.name}
</button>
</Button>
<p className="text-xs text-muted-foreground font-mono">
{project.slug}
</p>
@@ -42,6 +42,8 @@ import { toast } from "sonner";
import { AssignmentScope, ModelProvider } from "@/types";
import type { RoutingMode, SelfHostedTestResult } from "@/lib/api/providers";
import { SelfHostedSection } from "@/components/settings/self-hosted-section";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
// Matches the roboco agents_config AGENT_ROLE_MAP / AGENT_TEAM_MAP.
// Hard-coded so Mix mode shows a stable 18-row picker without an extra
@@ -254,13 +256,13 @@ export function AIRoutingCard() {
<div className="flex items-center justify-between">
<Label className="text-sm font-medium">Ollama Cloud API key</Label>
{hasOllamaKey ? (
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/10 px-2 py-0.5 text-xs font-medium text-emerald-600">
<Badge className="bg-emerald-500/10 text-emerald-600 border-0">
<KeyRound className="h-3 w-3" /> key set
</span>
</Badge>
) : (
<span className="inline-flex items-center gap-1 rounded-full bg-amber-500/10 px-2 py-0.5 text-xs font-medium text-amber-600">
<Badge className="bg-amber-500/10 text-amber-600 border-0">
<Key className="h-3 w-3" /> not set
</span>
</Badge>
)}
</div>
<div className="flex gap-2">
@@ -278,13 +280,13 @@ export function AIRoutingCard() {
</Button>
</div>
{hasOllamaKey ? (
<label className="flex items-center gap-2 text-xs text-muted-foreground">
<input
type="checkbox"
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
<Checkbox
checked={clearKey}
onChange={(e) => {
setClearKey(e.target.checked);
if (e.target.checked) setApiKey("");
onCheckedChange={(checked) => {
const next = checked === true;
setClearKey(next);
if (next) setApiKey("");
}}
/>
Clear the stored key
@@ -543,29 +545,27 @@ function ModeButton({
highlight?: boolean;
}) {
return (
<button
<Button
type="button"
variant="outline"
onClick={onClick}
disabled={disabled}
className={
"rounded-md border p-3 text-left transition-colors " +
(active || highlight
? "border-primary bg-primary/5"
: "hover:bg-muted") +
(disabled ? " cursor-not-allowed opacity-50" : "")
"h-auto flex-col items-start justify-start p-3 whitespace-normal " +
(active || highlight ? "border-primary bg-primary/5" : "")
}
>
<div className="flex items-center gap-2 text-sm font-medium">
<div className="flex w-full items-center gap-2 text-sm font-medium">
{icon}
<span>{label}</span>
{active ? (
<span className="ml-auto rounded-full bg-primary/15 px-2 py-0.5 text-xs text-primary">
<Badge className="ml-auto bg-primary/15 text-primary border-0 text-xs">
active
</span>
</Badge>
) : null}
</div>
<p className="mt-1 text-xs text-muted-foreground">{description}</p>
</button>
<p className="mt-1 text-xs text-muted-foreground font-normal">{description}</p>
</Button>
);
}
@@ -146,14 +146,14 @@ export function SelfHostedSection({
<Server className="h-4 w-4 text-muted-foreground" />
<Label className="text-sm font-medium">Self-Hosted LLM</Label>
{testResult?.ok === true && (
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/10 px-2 py-0.5 text-xs font-medium text-emerald-600">
<Badge className="bg-emerald-500/10 text-emerald-600 border-0">
<CheckCircle2 className="h-3 w-3" /> connected
</span>
</Badge>
)}
{testResult?.ok === false && (
<span className="inline-flex items-center gap-1 rounded-full bg-red-500/10 px-2 py-0.5 text-xs font-medium text-red-600">
<Badge className="bg-red-500/10 text-red-600 border-0">
<XCircle className="h-3 w-3" /> error
</span>
</Badge>
)}
</div>
@@ -194,10 +194,12 @@ export function SelfHostedSection({
}
className="pr-10"
/>
<button
<Button
type="button"
variant="ghost"
size="icon-sm"
onClick={() => setShowToken((v) => !v)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
className="absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label={showToken ? "Hide token" : "Show token"}
>
{showToken ? (
@@ -205,7 +207,7 @@ export function SelfHostedSection({
) : (
<Eye className="h-4 w-4" />
)}
</button>
</Button>
</div>
<Button onClick={handleSave} disabled={saveConfig.isPending}>
{saveConfig.isPending ? "Saving…" : "Save"}
@@ -243,17 +245,17 @@ export function SelfHostedSection({
{/* Inline result badge */}
{testResult?.ok === true && (
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/10 px-3 py-1 text-xs font-medium text-emerald-700 dark:text-emerald-400">
<Badge className="bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border-0 px-3 py-1 text-xs">
<CheckCircle2 className="h-3.5 w-3.5" />
Connected &mdash; {testResult.model_count ?? 0} model
{(testResult.model_count ?? 0) === 1 ? "" : "s"} available
</span>
</Badge>
)}
{testResult?.ok === false && (
<span className="inline-flex items-center gap-1 rounded-full bg-red-500/10 px-3 py-1 text-xs font-medium text-red-700 dark:text-red-400">
<Badge className="bg-red-500/10 text-red-700 dark:text-red-400 border-0 px-3 py-1 text-xs">
<XCircle className="h-3.5 w-3.5" />
{testResult.error ?? "Connection failed"}
</span>
</Badge>
)}
</div>
@@ -7,9 +7,10 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Search, X, Link2, Check } from "lucide-react";
import { Search, X, Link2 } from "lucide-react";
interface DependencySelectorProps {
selectedIds: string[];
@@ -114,22 +115,22 @@ export function DependencySelector({
{filteredTasks.map((task) => {
const isSelected = selectedIds.includes(task.id);
return (
<button
<Button
key={task.id}
type="button"
className={`w-full flex items-center gap-2 p-2 rounded-md text-left hover:bg-muted transition-colors ${
isSelected ? "bg-primary/10" : ""
variant="ghost"
className={`w-full h-auto justify-start gap-2 p-2 font-normal whitespace-normal ${
isSelected ? "bg-primary/10 hover:bg-primary/10" : ""
}`}
onClick={() => toggleTask(task.id)}
>
<div
className={`h-4 w-4 rounded border flex items-center justify-center shrink-0 ${
isSelected ? "bg-primary border-primary" : "border-muted-foreground"
}`}
>
{isSelected && <Check className="h-3 w-3 text-primary-foreground" />}
</div>
<div className="flex-1 min-w-0">
<Checkbox
checked={isSelected}
tabIndex={-1}
className="pointer-events-none shrink-0"
aria-hidden
/>
<div className="flex-1 min-w-0 text-left">
<p className="text-sm truncate">{task.title}</p>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Badge variant="outline" className="font-mono text-xs">
@@ -138,7 +139,7 @@ export function DependencySelector({
<span className="capitalize">{task.status.replace(/_/g, " ")}</span>
</div>
</div>
</button>
</Button>
);
})}
</div>
+9 -6
View File
@@ -180,15 +180,16 @@ function SortableHeader({ label, field, sortConfig, onSort, className }: Sortabl
return (
<TableHead className={className}>
<button
<Button
onClick={() => onSort(field)}
className="flex items-center gap-1 hover:text-foreground transition-colors -ml-2 px-2 py-1 rounded hover:bg-muted"
variant="ghost"
className="h-auto -ml-2 px-2 py-1 gap-1 font-normal"
>
{label}
{!isActive && <ArrowUpDown className="h-4 w-4 text-muted-foreground" />}
{direction === "asc" && <ArrowUp className="h-4 w-4" />}
{direction === "desc" && <ArrowDown className="h-4 w-4" />}
</button>
</Button>
</TableHead>
);
}
@@ -479,16 +480,18 @@ export function TaskTable({
style={{ paddingLeft: `${node.depth * 1.5}rem` }}
>
{hasChildren ? (
<button
<Button
onClick={() => toggleExpand(task.id)}
className="p-0.5 hover:bg-muted rounded shrink-0"
variant="ghost"
size="icon-sm"
className="p-0.5 h-5 w-5 shrink-0"
>
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRightIcon className="h-4 w-4" />
)}
</button>
</Button>
) : (
<span className="w-5 shrink-0" />
)}