mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[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:
co-authored by
Backend Developer 2
Backend Developer 1
Frontend Developer 1
Frontend Developer 2
Renn F
parent
e71a746499
commit
818f2ac7a6
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 — {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>
|
||||
|
||||
@@ -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" />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user